| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- #!/usr/bin/env node
- /**
- * Bootstrap ~/.pilotdeck/pilotdeck.yaml when it doesn't exist yet, so the
- * gateway can boot and the Web UI can run the onboarding flow that fills in
- * real provider details. On every startup, also sync repo-provided skills
- * into ~/.pilotdeck/skills without overwriting existing targets.
- *
- * Behaviour:
- * 1. Every run: discover repo skills and copy missing slugs into
- * $PILOT_HOME/skills, skipping existing targets.
- * 2. Every run: if pilotdeck.yaml exists but is missing known sections
- * (e.g. adapters), append the default snippet so new features are
- * discoverable without requiring users to recreate the config.
- * 3. If $PILOT_HOME/pilotdeck.yaml does not exist, write a minimal V2 yaml that:
- * - has a valid agent.model that resolves to a catalog provider/model,
- * so the engine's parseModelConfig won't crash on startup
- * - uses a sentinel apiKey ("PLACEHOLDER_RUN_ONBOARDING_TO_REPLACE")
- * that hasUsablePilotDeckConfig() recognises as "not ready" so the
- * UI redirects to onboarding instead of pretending it's configured.
- *
- * Override the target via $PILOT_HOME (same env var the engine reads).
- * Skip the whole step via $PILOTDECK_SKIP_BOOTSTRAP=1.
- */
- import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
- import { homedir } from 'node:os';
- import { basename, dirname, join, resolve } from 'node:path';
- import { fileURLToPath } from 'node:url';
- const SENTINEL_API_KEY = 'PLACEHOLDER_RUN_ONBOARDING_TO_REPLACE';
- const __filename = fileURLToPath(import.meta.url);
- const REPO_ROOT = resolve(dirname(__filename), '..');
- const BOOTSTRAP_YAML = `# Auto-generated by scripts/bootstrap-pilotdeck-config.mjs.
- # Open the Web UI to finish onboarding — choose a provider, paste an API key,
- # and pick a model. The gateway boots with this placeholder config so the
- # server can start, but apiKey="${SENTINEL_API_KEY}" tells the UI
- # to redirect to the onboarding screen.
- schemaVersion: 1
- agent:
- model: _placeholder/_placeholder
- model:
- providers:
- _placeholder:
- protocol: openai
- url: https://placeholder.invalid
- apiKey: ${SENTINEL_API_KEY}
- models:
- _placeholder:
- capabilities:
- maxOutputTokens: 16384
- adapters:
- feishu:
- enabled: false
- appId: ""
- appSecret: ""
- # connectionMode: stream
- # domainName: feishu
- router:
- enabled: true
- scenarios:
- default: _placeholder/_placeholder
- fallback:
- default:
- - _placeholder/_placeholder
- zeroUsageRetry:
- enabled: true
- maxAttempts: 2
- tokenSaver:
- enabled: true
- judge: _placeholder/_placeholder
- defaultTier: medium
- judgeTimeoutMs: 15000
- tiers:
- simple:
- model: _placeholder/_placeholder
- description: "Simple greetings, confirmations, single-step Q&A, trivial file writes, remembering rules"
- medium:
- model: _placeholder/_placeholder
- description: "Single tool call, short text generation, 1-2 file read/write, code generation"
- complex:
- model: _placeholder/_placeholder
- description: "Needs sub-agent orchestration: parallel workstreams, delegation to specialized agents"
- reasoning:
- model: _placeholder/_placeholder
- description: "Deep single-agent work: multi-file operations, data analysis, multi-step workflows, web research, structured reports from many sources"
- rules:
- - "complex is ONLY for tasks that need sub-agent orchestration or parallel delegation — do NOT use it for single-agent multi-step work"
- - "Multi-file operations, data analysis, and multi-step workflows without orchestration should be reasoning"
- - "Simple file creation (1-2 files) or single code generation is medium"
- - "Trivial greetings, confirmations, remembering rules, or reading one file and answering a short question is simple"
- autoOrchestrate:
- enabled: true
- triggerTiers:
- - complex
- slimSystemPrompt: true
- allowedTools:
- - agent
- - read_file
- - grep
- - glob
- - read_skill
- subagentMaxTokens: 48000
- stats:
- enabled: true
- cron:
- enabled: true
- timezone: Asia/Shanghai
- maxConcurrentRuns: 2
- `;
- function resolvePilotHome() {
- if (process.env.PILOT_HOME) return process.env.PILOT_HOME;
- return join(homedir(), '.pilotdeck');
- }
- function discoverRepoSkillDirs(skillsRoot) {
- if (!existsSync(skillsRoot)) {
- return [];
- }
- const discovered = [];
- const pending = [skillsRoot];
- while (pending.length > 0) {
- const current = pending.pop();
- const entries = readdirSync(current, { withFileTypes: true });
- if (entries.some((entry) => entry.isFile() && /^skill\.md$/i.test(entry.name))) {
- discovered.push(current);
- continue;
- }
- for (const entry of entries) {
- if (entry.isDirectory()) {
- pending.push(join(current, entry.name));
- }
- }
- }
- discovered.sort((left, right) => left.localeCompare(right));
- return discovered;
- }
- function syncRepoSkillsToPilotHome(pilotHome) {
- const repoSkillsRoot = join(REPO_ROOT, 'skills');
- const skillDirs = discoverRepoSkillDirs(repoSkillsRoot);
- if (skillDirs.length === 0) {
- return { created: 0, skippedExisting: 0, skippedDuplicateSlug: 0 };
- }
- const targetRoot = join(pilotHome, 'skills');
- mkdirSync(targetRoot, { recursive: true });
- let created = 0;
- let skippedExisting = 0;
- let skippedDuplicateSlug = 0;
- const seenSlugs = new Map();
- for (const sourceDir of skillDirs) {
- const slug = basename(sourceDir);
- const previous = seenSlugs.get(slug);
- if (previous) {
- skippedDuplicateSlug += 1;
- console.warn(
- `[pilotdeck] Skipping repo skill '${slug}' at ${sourceDir}; slug already claimed by ${previous}.`,
- );
- continue;
- }
- seenSlugs.set(slug, sourceDir);
- const targetPath = join(targetRoot, slug);
- if (existsSync(targetPath)) {
- skippedExisting += 1;
- continue;
- }
- try {
- cpSync(sourceDir, targetPath, { recursive: true });
- created += 1;
- } catch (error) {
- console.warn(
- `[pilotdeck] Could not import repo skill '${slug}' into ${targetPath}: ${
- error instanceof Error ? error.message : String(error)
- }`,
- );
- }
- }
- return { created, skippedExisting, skippedDuplicateSlug };
- }
- const DEFAULT_ADAPTERS_SNIPPET = `
- adapters:
- feishu:
- enabled: false
- appId: ""
- appSecret: ""
- # connectionMode: stream
- # domainName: feishu
- `;
- const DEFAULT_CRON_SNIPPET = `
- cron:
- enabled: true
- timezone: Asia/Shanghai
- maxConcurrentRuns: 2
- `;
- const PATCH_SECTIONS = [
- { key: 'adapters', snippet: DEFAULT_ADAPTERS_SNIPPET },
- { key: 'cron', snippet: DEFAULT_CRON_SNIPPET },
- ];
- function patchMissingSections(configPath) {
- let content;
- try {
- content = readFileSync(configPath, 'utf8');
- } catch {
- return;
- }
- let patched = false;
- for (const { key, snippet } of PATCH_SECTIONS) {
- const pattern = new RegExp(`^${key}\\s*:`, 'm');
- if (!pattern.test(content)) {
- try {
- appendFileSync(configPath, snippet, 'utf8');
- patched = true;
- console.log(`[pilotdeck] Appended missing "${key}" section to ${configPath}.`);
- } catch (error) {
- console.warn(
- `[pilotdeck] Could not append "${key}" to ${configPath}: ${
- error instanceof Error ? error.message : String(error)
- }`,
- );
- }
- }
- }
- return patched;
- }
- function main() {
- if (process.env.PILOTDECK_SKIP_BOOTSTRAP === '1') {
- return;
- }
- const pilotHome = resolvePilotHome();
- const configPath = join(pilotHome, 'pilotdeck.yaml');
- const skillSync = syncRepoSkillsToPilotHome(pilotHome);
- if (skillSync.created > 0 || skillSync.skippedExisting > 0 || skillSync.skippedDuplicateSlug > 0) {
- console.log(
- `[pilotdeck] Synced repo skills into ${join(pilotHome, 'skills')}: ` +
- `${skillSync.created} copied, ${skillSync.skippedExisting} skipped existing, ` +
- `${skillSync.skippedDuplicateSlug} skipped duplicate slug.`,
- );
- }
- if (existsSync(configPath)) {
- patchMissingSections(configPath);
- return;
- }
- try {
- mkdirSync(dirname(configPath), { recursive: true });
- writeFileSync(configPath, BOOTSTRAP_YAML, 'utf8');
- console.log(
- `[pilotdeck] No config at ${configPath}; wrote a placeholder so the gateway can boot.`,
- );
- console.log('[pilotdeck] Open the Web UI to finish onboarding (provider + API key).');
- } catch (error) {
- console.warn(
- `[pilotdeck] Could not bootstrap ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
- );
- console.warn('[pilotdeck] You may need to create it manually before running npm run dev.');
- }
- }
- main();
|