#!/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. If $PILOT_HOME/pilotdeck.yaml already exists -> skip config bootstrap.
* 3. Otherwise 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 { cpSync, existsSync, mkdirSync, 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
router:
scenarios:
default: _placeholder/_placeholder
fallback:
default:
- _placeholder/_placeholder
zeroUsageRetry:
enabled: false
maxAttempts: 2
tokenSaver:
enabled: false
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: false
triggerTiers:
- complex
slimSystemPrompt: true
allowedTools:
- agent
- read_file
- grep
- glob
- read_skill
subagentMaxTokens: 48000
stats:
enabled: false
`;
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 };
}
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)) {
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();