popilot 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +372 -0
- package/adapters/claude-code/.claude/commands/_domain.md.hbs +32 -0
- package/adapters/claude-code/.claude/commands/analytics.md.hbs +55 -0
- package/adapters/claude-code/.claude/commands/daily.md.hbs +301 -0
- package/adapters/claude-code/.claude/commands/dev.md.hbs +62 -0
- package/adapters/claude-code/.claude/commands/handoff.md +258 -0
- package/adapters/claude-code/.claude/commands/market.md +120 -0
- package/adapters/claude-code/.claude/commands/metrics.md +123 -0
- package/adapters/claude-code/.claude/commands/oscar-loop.md +436 -0
- package/adapters/claude-code/.claude/commands/party.md +85 -0
- package/adapters/claude-code/.claude/commands/plan.md +43 -0
- package/adapters/claude-code/.claude/commands/research.md +203 -0
- package/adapters/claude-code/.claude/commands/retro.md +68 -0
- package/adapters/claude-code/.claude/commands/save.md +440 -0
- package/adapters/claude-code/.claude/commands/sessions.md +139 -0
- package/adapters/claude-code/.claude/commands/sprint.md +106 -0
- package/adapters/claude-code/.claude/commands/start.md +368 -0
- package/adapters/claude-code/.claude/commands/strategy.md +41 -0
- package/adapters/claude-code/.claude/commands/task.md +220 -0
- package/adapters/claude-code/.claude/commands/tracking.md +116 -0
- package/adapters/claude-code/.claude/commands/validate.md +58 -0
- package/adapters/claude-code/CLAUDE.md.hbs +208 -0
- package/adapters/claude-code/manifest.yaml +36 -0
- package/bin/cli.mjs +218 -0
- package/lib/adapter.mjs +68 -0
- package/lib/doctor.mjs +161 -0
- package/lib/hydrate.mjs +421 -0
- package/lib/prompt.mjs +78 -0
- package/lib/scaffold.mjs +155 -0
- package/lib/setup-wizard.mjs +331 -0
- package/lib/template-engine.mjs +164 -0
- package/lib/yaml-lite.mjs +476 -0
- package/package.json +30 -0
- package/scaffold/.context/.secrets.yaml.example +20 -0
- package/scaffold/.context/WORKFLOW.md.hbs +332 -0
- package/scaffold/.context/agents/TEMPLATE.md +115 -0
- package/scaffold/.context/agents/analyst.md.hbs +362 -0
- package/scaffold/.context/agents/developer.md.hbs +390 -0
- package/scaffold/.context/agents/handoff-specialist.md.hbs +292 -0
- package/scaffold/.context/agents/market-researcher.md.hbs +288 -0
- package/scaffold/.context/agents/ollie.md +323 -0
- package/scaffold/.context/agents/operations.md.hbs +293 -0
- package/scaffold/.context/agents/orchestrator.md.hbs +434 -0
- package/scaffold/.context/agents/planner.md.hbs +405 -0
- package/scaffold/.context/agents/qa.md.hbs +409 -0
- package/scaffold/.context/agents/researcher.md.hbs +330 -0
- package/scaffold/.context/agents/sage.md +349 -0
- package/scaffold/.context/agents/strategist.md.hbs +339 -0
- package/scaffold/.context/agents/tracking-governor.md.hbs +291 -0
- package/scaffold/.context/agents/validator.md.hbs +365 -0
- package/scaffold/.context/integrations/_registry.yaml +38 -0
- package/scaffold/.context/integrations/providers/channel_io.yaml +38 -0
- package/scaffold/.context/integrations/providers/corti.yaml +203 -0
- package/scaffold/.context/integrations/providers/ga4.yaml +116 -0
- package/scaffold/.context/integrations/providers/intercom.yaml +47 -0
- package/scaffold/.context/integrations/providers/linear.yaml +46 -0
- package/scaffold/.context/integrations/providers/mixpanel.yaml +73 -0
- package/scaffold/.context/integrations/providers/notebooklm.yaml +74 -0
- package/scaffold/.context/integrations/providers/notion.yaml +129 -0
- package/scaffold/.context/integrations/providers/prod_db.yaml +183 -0
- package/scaffold/.context/oscar/workflows/multi-agent.md +82 -0
- package/scaffold/.context/oscar/workflows/ollie-sage.md +128 -0
- package/scaffold/.context/oscar/workflows/session-git.md +71 -0
- package/scaffold/.context/oscar/workflows/setup.md +663 -0
- package/scaffold/.context/oscar/workflows/tracking.md +118 -0
- package/scaffold/.context/project.yaml.example +102 -0
- package/scaffold/.context/templates/dev-guide.md +217 -0
- package/scaffold/.context/templates/epic-spec.md +225 -0
- package/scaffold/.context/templates/guardrail.md +94 -0
- package/scaffold/.context/templates/handoff-checklist.md +197 -0
- package/scaffold/.context/templates/prd.md +80 -0
- package/scaffold/.context/templates/retrospective.md +78 -0
- package/scaffold/.context/templates/screen-spec.md +714 -0
- package/scaffold/.context/templates/sprint-plan.md +72 -0
- package/scaffold/.context/templates/sprint-status.yaml +109 -0
- package/scaffold/.context/templates/story-v2.md +228 -0
- package/scaffold/.context/templates/validation-report.md +99 -0
- package/scaffold/.gitignore.append +7 -0
- package/scaffold/spec-site/env.d.ts +7 -0
- package/scaffold/spec-site/index.html +14 -0
- package/scaffold/spec-site/package.json +20 -0
- package/scaffold/spec-site/src/App.vue +27 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +10 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +14 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +21 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +20 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +11 -0
- package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +11 -0
- package/scaffold/spec-site/src/components/Accordion.vue +108 -0
- package/scaffold/spec-site/src/components/AppHeader.vue +304 -0
- package/scaffold/spec-site/src/components/Badge.vue +25 -0
- package/scaffold/spec-site/src/components/CoachingCard.vue +112 -0
- package/scaffold/spec-site/src/components/MemoSidebar.vue +239 -0
- package/scaffold/spec-site/src/components/MockupShell.vue +100 -0
- package/scaffold/spec-site/src/components/RuleTable.vue +99 -0
- package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +103 -0
- package/scaffold/spec-site/src/components/SpecNav.vue +26 -0
- package/scaffold/spec-site/src/components/SpecSection.vue +59 -0
- package/scaffold/spec-site/src/components/SummaryGrid.vue +39 -0
- package/scaffold/spec-site/src/components/VersionBadge.vue +38 -0
- package/scaffold/spec-site/src/composables/useActiveSection.ts +53 -0
- package/scaffold/spec-site/src/composables/useMemo.ts +138 -0
- package/scaffold/spec-site/src/composables/useRetro.ts +313 -0
- package/scaffold/spec-site/src/composables/useScenario.ts +43 -0
- package/scaffold/spec-site/src/composables/useScenarioStore.ts +102 -0
- package/scaffold/spec-site/src/composables/useTurso.ts +160 -0
- package/scaffold/spec-site/src/composables/useUser.ts +25 -0
- package/scaffold/spec-site/src/data/navigation.ts +59 -0
- package/scaffold/spec-site/src/data/types.ts +90 -0
- package/scaffold/spec-site/src/data/wireframeRegistry.ts +25 -0
- package/scaffold/spec-site/src/layouts/SplitPaneLayout.vue +79 -0
- package/scaffold/spec-site/src/main.ts +10 -0
- package/scaffold/spec-site/src/pages/IndexPage.vue +66 -0
- package/scaffold/spec-site/src/pages/PolicyDetail.vue +215 -0
- package/scaffold/spec-site/src/pages/PolicyIndex.vue +74 -0
- package/scaffold/spec-site/src/pages/retro/RetroActions.vue +191 -0
- package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +192 -0
- package/scaffold/spec-site/src/pages/retro/RetroCard.vue +131 -0
- package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +287 -0
- package/scaffold/spec-site/src/pages/retro/RetroPage.vue +178 -0
- package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +34 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderContent.vue +22 -0
- package/scaffold/spec-site/src/pages/shared/PlaceholderSpecPanel.vue +16 -0
- package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +145 -0
- package/scaffold/spec-site/src/pages/wireframe/WireframeShell.vue +151 -0
- package/scaffold/spec-site/src/router.ts +85 -0
- package/scaffold/spec-site/src/styles/base.css +21 -0
- package/scaffold/spec-site/src/styles/split-pane.css +143 -0
- package/scaffold/spec-site/src/styles/variables.css +47 -0
- package/scaffold/spec-site/src/utils/markdown.ts +197 -0
- package/scaffold/spec-site/tsconfig.json +20 -0
- package/scaffold/spec-site/vite.config.ts +18 -0
package/lib/prompt.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dep interactive prompt helpers using Node.js readline/promises.
|
|
3
|
+
* All functions accept an optional `rl` parameter for testing/injection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { stdin, stdout } from 'node:process';
|
|
8
|
+
|
|
9
|
+
/** Create a shared readline interface */
|
|
10
|
+
export function createPrompt() {
|
|
11
|
+
return createInterface({ input: stdin, output: stdout });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ask a single question, return the answer (or default).
|
|
16
|
+
* @param {import('node:readline/promises').Interface} rl
|
|
17
|
+
* @param {string} question
|
|
18
|
+
* @param {string} [defaultValue]
|
|
19
|
+
* @returns {Promise<string>}
|
|
20
|
+
*/
|
|
21
|
+
export async function ask(rl, question, defaultValue) {
|
|
22
|
+
const suffix = defaultValue != null ? ` [${defaultValue}]` : '';
|
|
23
|
+
const answer = (await rl.question(` ${question}${suffix}: `)).trim();
|
|
24
|
+
return answer || defaultValue || '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Y/n confirmation.
|
|
29
|
+
* @param {import('node:readline/promises').Interface} rl
|
|
30
|
+
* @param {string} question
|
|
31
|
+
* @param {boolean} [defaultYes=false]
|
|
32
|
+
* @returns {Promise<boolean>}
|
|
33
|
+
*/
|
|
34
|
+
export async function confirm(rl, question, defaultYes = false) {
|
|
35
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
36
|
+
const answer = (await rl.question(` ${question} (${hint}): `)).trim().toLowerCase();
|
|
37
|
+
if (answer === '') return defaultYes;
|
|
38
|
+
return answer === 'y' || answer === 'yes';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Numbered single-select menu.
|
|
43
|
+
* @param {import('node:readline/promises').Interface} rl
|
|
44
|
+
* @param {string} question
|
|
45
|
+
* @param {{ label: string, value: any }[]} options
|
|
46
|
+
* @param {number} [defaultIndex=0] - 0-based default index
|
|
47
|
+
* @returns {Promise<any>} selected value
|
|
48
|
+
*/
|
|
49
|
+
export async function select(rl, question, options, defaultIndex = 0) {
|
|
50
|
+
console.log(` ${question}`);
|
|
51
|
+
options.forEach((opt, i) => {
|
|
52
|
+
console.log(` ${i + 1}. ${opt.label}`);
|
|
53
|
+
});
|
|
54
|
+
const answer = (await rl.question(` Select [${defaultIndex + 1}]: `)).trim();
|
|
55
|
+
const idx = answer ? parseInt(answer, 10) - 1 : defaultIndex;
|
|
56
|
+
if (idx >= 0 && idx < options.length) return options[idx].value;
|
|
57
|
+
return options[defaultIndex].value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Numbered multi-select menu (comma-separated input).
|
|
62
|
+
* @param {import('node:readline/promises').Interface} rl
|
|
63
|
+
* @param {string} question
|
|
64
|
+
* @param {{ label: string, value: any }[]} options
|
|
65
|
+
* @returns {Promise<any[]>} selected values
|
|
66
|
+
*/
|
|
67
|
+
export async function multiSelect(rl, question, options) {
|
|
68
|
+
console.log(` ${question}`);
|
|
69
|
+
options.forEach((opt, i) => {
|
|
70
|
+
console.log(` ${i + 1}. ${opt.label}`);
|
|
71
|
+
});
|
|
72
|
+
const answer = (await rl.question(' Select (comma-separated, Enter to skip): ')).trim();
|
|
73
|
+
if (!answer) return [];
|
|
74
|
+
const indices = answer.split(',').map(s => parseInt(s.trim(), 10) - 1);
|
|
75
|
+
return indices
|
|
76
|
+
.filter(i => i >= 0 && i < options.length)
|
|
77
|
+
.map(i => options[i].value);
|
|
78
|
+
}
|
package/lib/scaffold.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile, mkdir, stat, access } from 'node:fs/promises';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { getAdapterDir, getDefaultAdapter } from './adapter.mjs';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const SCAFFOLD_DIR = join(__dirname, '..', 'scaffold');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Copy scaffold directory to target, preserving structure.
|
|
11
|
+
* Two-pass: 1) core scaffold, 2) adapter overlay.
|
|
12
|
+
*
|
|
13
|
+
* - .hbs files are copied as-is (hydration happens in Setup Wizard)
|
|
14
|
+
* - .append files are returned for post-processing
|
|
15
|
+
* - All other files copied directly
|
|
16
|
+
*
|
|
17
|
+
* @param {string} targetDir - Destination directory
|
|
18
|
+
* @param {object} options - { skipSpecSite, overwriteExisting, platform }
|
|
19
|
+
* @returns {Promise<{ copied: string[], overwritten: string[], skipped: string[], appends: { file: string, content: string }[] }>}
|
|
20
|
+
*/
|
|
21
|
+
export async function copyScaffold(targetDir, options = {}) {
|
|
22
|
+
const copied = [];
|
|
23
|
+
const overwritten = [];
|
|
24
|
+
const skipped = [];
|
|
25
|
+
const appends = [];
|
|
26
|
+
|
|
27
|
+
// Pass 1: Core scaffold
|
|
28
|
+
await walk(SCAFFOLD_DIR, targetDir, targetDir, options, copied, overwritten, skipped, appends);
|
|
29
|
+
|
|
30
|
+
// Pass 2: Adapter overlay
|
|
31
|
+
const platform = options.platform || getDefaultAdapter();
|
|
32
|
+
const adapterDir = getAdapterDir(platform);
|
|
33
|
+
try {
|
|
34
|
+
await access(adapterDir);
|
|
35
|
+
await walk(adapterDir, targetDir, targetDir, options, copied, overwritten, skipped, appends);
|
|
36
|
+
} catch {
|
|
37
|
+
// Adapter dir not found — skip
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { copied, overwritten, skipped, appends };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function walk(srcDir, destDir, targetDir, options, copied, overwritten, skipped, appends) {
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = await readdir(srcDir, { withFileTypes: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
const srcPath = join(srcDir, entry.name);
|
|
53
|
+
const relPath = relative(srcDir, srcPath);
|
|
54
|
+
|
|
55
|
+
// Skip spec-site if requested
|
|
56
|
+
if (options.skipSpecSite && relPath.startsWith('spec-site')) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Skip manifest.yaml (adapter metadata, not a project file)
|
|
61
|
+
if (entry.name === 'manifest.yaml') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
const destPath = join(destDir, entry.name);
|
|
67
|
+
await mkdir(destPath, { recursive: true });
|
|
68
|
+
await walk(srcPath, destPath, targetDir, options, copied, overwritten, skipped, appends);
|
|
69
|
+
} else if (entry.name.endsWith('.append')) {
|
|
70
|
+
const content = await readFile(srcPath, 'utf-8');
|
|
71
|
+
appends.push({ file: entry.name.replace('.append', ''), content });
|
|
72
|
+
} else {
|
|
73
|
+
const destPath = join(destDir, entry.name);
|
|
74
|
+
await mkdir(dirname(destPath), { recursive: true });
|
|
75
|
+
|
|
76
|
+
// Existing file handling
|
|
77
|
+
try {
|
|
78
|
+
await access(destPath);
|
|
79
|
+
if (!options.overwriteExisting) {
|
|
80
|
+
skipped.push(relative(targetDir, destPath));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const content = await readFile(srcPath);
|
|
84
|
+
await writeFile(destPath, content);
|
|
85
|
+
overwritten.push(relative(targetDir, destPath));
|
|
86
|
+
continue;
|
|
87
|
+
} catch {
|
|
88
|
+
// File doesn't exist — copy
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const content = await readFile(srcPath);
|
|
92
|
+
await writeFile(destPath, content);
|
|
93
|
+
copied.push(relative(targetDir, destPath));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Append content to a file (create if doesn't exist)
|
|
100
|
+
*/
|
|
101
|
+
export async function appendToFile(filePath, content) {
|
|
102
|
+
let existing = '';
|
|
103
|
+
try {
|
|
104
|
+
existing = await readFile(filePath, 'utf-8');
|
|
105
|
+
} catch {
|
|
106
|
+
// File doesn't exist
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Avoid duplicate lines
|
|
110
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
111
|
+
const newLines = lines.filter(l => !existing.includes(l));
|
|
112
|
+
|
|
113
|
+
if (newLines.length > 0) {
|
|
114
|
+
const separator = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
115
|
+
await writeFile(filePath, existing + separator + newLines.join('\n') + '\n');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if target directory already has Popilot structure.
|
|
121
|
+
* Uses adapter detection markers if available.
|
|
122
|
+
*/
|
|
123
|
+
export async function detectExisting(targetDir, platform) {
|
|
124
|
+
let markers;
|
|
125
|
+
if (platform) {
|
|
126
|
+
try {
|
|
127
|
+
const { loadManifest } = await import('./adapter.mjs');
|
|
128
|
+
const manifest = await loadManifest(platform);
|
|
129
|
+
markers = manifest.detection_markers || [];
|
|
130
|
+
} catch {
|
|
131
|
+
markers = [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!markers || markers.length === 0) {
|
|
136
|
+
markers = ['.claude', '.context', 'CLAUDE.md'];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Always include .context
|
|
140
|
+
if (!markers.includes('.context')) {
|
|
141
|
+
markers.push('.context');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const found = [];
|
|
145
|
+
for (const marker of markers) {
|
|
146
|
+
try {
|
|
147
|
+
await stat(join(targetDir, marker));
|
|
148
|
+
found.push(marker);
|
|
149
|
+
} catch {
|
|
150
|
+
// Not found
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive setup wizard — terminal interview → project.yaml + related files.
|
|
3
|
+
*
|
|
4
|
+
* Collects only essential config. AI deep interview is deferred to Claude Code
|
|
5
|
+
* via `_meta.needs_deep_interview: true`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { ask, confirm, select, createPrompt } from './prompt.mjs';
|
|
11
|
+
import { parse as parseYaml, stringify as stringifyYaml } from './yaml-lite.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run the interactive setup wizard.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} targetDir - Project root (scaffold already copied)
|
|
17
|
+
* @param {object} [opts]
|
|
18
|
+
* @param {import('node:readline/promises').Interface} [opts.rl] - Inject readline for testing
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
export async function runSetupWizard(targetDir, opts = {}) {
|
|
22
|
+
const rl = opts.rl || createPrompt();
|
|
23
|
+
const ownsRl = !opts.rl;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(' ──────────────────────────────────────');
|
|
28
|
+
console.log(' 📝 Project Setup');
|
|
29
|
+
console.log(' ──────────────────────────────────────');
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
// ── Phase 0: User preferences ──────────────────────
|
|
33
|
+
console.log(' 👤 User Preferences');
|
|
34
|
+
console.log();
|
|
35
|
+
const userName = await ask(rl, 'What should the agents call you?', 'there');
|
|
36
|
+
const userRole = await ask(rl, 'Your role/title (optional)');
|
|
37
|
+
const preferredLanguage = await select(rl, 'Preferred response language:', [
|
|
38
|
+
{ label: 'Korean (ko)', value: 'ko' },
|
|
39
|
+
{ label: 'English (en)', value: 'en' },
|
|
40
|
+
{ label: 'Japanese (ja)', value: 'ja' },
|
|
41
|
+
], 0);
|
|
42
|
+
const communicationStyle = await select(rl, 'Communication style:', [
|
|
43
|
+
{ label: 'Concise', value: 'concise' },
|
|
44
|
+
{ label: 'Detailed', value: 'detailed' },
|
|
45
|
+
{ label: 'Casual', value: 'casual' },
|
|
46
|
+
], 0);
|
|
47
|
+
|
|
48
|
+
console.log();
|
|
49
|
+
|
|
50
|
+
// ── Phase 1: Basic info ──────────────────────────
|
|
51
|
+
const projectName = await ask(rl, 'Project name', getDefaultProjectName(targetDir));
|
|
52
|
+
const tagline = await ask(rl, 'Tagline (optional)');
|
|
53
|
+
const projectType = await select(rl, 'Project type:', [
|
|
54
|
+
{ label: 'brownfield (existing codebase)', value: 'brownfield' },
|
|
55
|
+
{ label: 'greenfield (new project)', value: 'greenfield' },
|
|
56
|
+
], 0);
|
|
57
|
+
|
|
58
|
+
console.log();
|
|
59
|
+
|
|
60
|
+
// ── Phase 2: Domains ─────────────────────────────
|
|
61
|
+
const hasDomains = await confirm(rl, 'Do you have work domains?', false);
|
|
62
|
+
let domains = [];
|
|
63
|
+
if (hasDomains) {
|
|
64
|
+
const domainsInput = await ask(rl, 'Domains (id:name, comma-separated)', '');
|
|
65
|
+
domains = parseDomainInput(domainsInput);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log();
|
|
69
|
+
|
|
70
|
+
// ── Phase 3: Dev scope ───────────────────────────
|
|
71
|
+
const hasDevRepo = await confirm(rl, 'Do you have a separate dev repo?', false);
|
|
72
|
+
let devScope = { repo_name: '', service_repo: '' };
|
|
73
|
+
if (hasDevRepo) {
|
|
74
|
+
devScope.repo_name = await ask(rl, 'Dashboard repo name');
|
|
75
|
+
devScope.service_repo = await ask(rl, 'Service repo name');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log();
|
|
79
|
+
|
|
80
|
+
// ── Phase 4: Integrations ────────────────────────
|
|
81
|
+
const integrations = await collectIntegrations(rl, targetDir);
|
|
82
|
+
|
|
83
|
+
// ── Generate files ───────────────────────────────
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
// project.yaml
|
|
87
|
+
const projectYaml = buildProjectYaml({
|
|
88
|
+
projectName, tagline, projectType,
|
|
89
|
+
domains, devScope, integrations,
|
|
90
|
+
platform: opts.platform || null,
|
|
91
|
+
});
|
|
92
|
+
const contextDir = join(targetDir, '.context');
|
|
93
|
+
await mkdir(contextDir, { recursive: true });
|
|
94
|
+
await writeFile(join(contextDir, 'project.yaml'), stringifyYaml(projectYaml));
|
|
95
|
+
|
|
96
|
+
// user-context.yaml
|
|
97
|
+
const userContext = {
|
|
98
|
+
user: {
|
|
99
|
+
name: userName,
|
|
100
|
+
},
|
|
101
|
+
_meta: {
|
|
102
|
+
source: 'cli',
|
|
103
|
+
created_at: new Date().toISOString(),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
if (userRole) userContext.user.role = userRole;
|
|
107
|
+
if (preferredLanguage) userContext.user.language = preferredLanguage;
|
|
108
|
+
if (communicationStyle) userContext.user.communication_style = communicationStyle;
|
|
109
|
+
|
|
110
|
+
await writeFile(join(contextDir, 'user-context.yaml'), stringifyYaml(userContext));
|
|
111
|
+
|
|
112
|
+
// sessions/index.yaml
|
|
113
|
+
const sessionsDir = join(contextDir, 'sessions');
|
|
114
|
+
await mkdir(join(sessionsDir, 'active'), { recursive: true });
|
|
115
|
+
await mkdir(join(sessionsDir, 'archive'), { recursive: true });
|
|
116
|
+
await writeFile(join(sessionsDir, 'index.yaml'), stringifyYaml({
|
|
117
|
+
sessions: [],
|
|
118
|
+
_meta: { last_updated: new Date().toISOString() },
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// sprints/s1/context.md
|
|
122
|
+
const sprintDir = join(contextDir, 'sprints', 's1');
|
|
123
|
+
await mkdir(sprintDir, { recursive: true });
|
|
124
|
+
await writeFile(join(sprintDir, 'context.md'), `# Sprint 1\n\n> Write your sprint context here.\n`);
|
|
125
|
+
|
|
126
|
+
// Domain folders
|
|
127
|
+
if (domains.length > 0) {
|
|
128
|
+
for (const d of domains) {
|
|
129
|
+
const domainDir = join(contextDir, 'domains', d.id);
|
|
130
|
+
await mkdir(domainDir, { recursive: true });
|
|
131
|
+
await writeFile(join(domainDir, 'index.md'), `# ${d.name}\n\n> Write your domain context here.\n`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Ensure standard directories
|
|
136
|
+
for (const dir of ['global/product', 'global/database', 'global/tracking', 'metrics', 'daily']) {
|
|
137
|
+
await mkdir(join(contextDir, dir), { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
} finally {
|
|
141
|
+
if (ownsRl) rl.close();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Integration collection ──────────────────────────────
|
|
146
|
+
|
|
147
|
+
async function collectIntegrations(rl, targetDir) {
|
|
148
|
+
const registryPath = join(targetDir, '.context', 'integrations', '_registry.yaml');
|
|
149
|
+
const providersDir = join(targetDir, '.context', 'integrations', 'providers');
|
|
150
|
+
|
|
151
|
+
let registry;
|
|
152
|
+
try {
|
|
153
|
+
registry = parseYaml(await readFile(registryPath, 'utf-8'));
|
|
154
|
+
} catch {
|
|
155
|
+
return {}; // No registry — skip integrations
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Load all provider definitions
|
|
159
|
+
const providerFiles = await safeReaddir(providersDir);
|
|
160
|
+
const providers = [];
|
|
161
|
+
for (const file of providerFiles) {
|
|
162
|
+
if (!file.endsWith('.yaml') || file.startsWith('_')) continue;
|
|
163
|
+
try {
|
|
164
|
+
const yaml = parseYaml(await readFile(join(providersDir, file), 'utf-8'));
|
|
165
|
+
providers.push(yaml);
|
|
166
|
+
} catch { /* skip */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(' ──────────────────────────────────────');
|
|
170
|
+
console.log(' 🔌 Integrations');
|
|
171
|
+
console.log(' ──────────────────────────────────────');
|
|
172
|
+
console.log();
|
|
173
|
+
|
|
174
|
+
const integrations = {};
|
|
175
|
+
const categories = registry.categories || {};
|
|
176
|
+
|
|
177
|
+
// Group providers by category
|
|
178
|
+
for (const [catId, cat] of Object.entries(categories)) {
|
|
179
|
+
const catProviders = providers.filter(p => p.category === catId);
|
|
180
|
+
if (catProviders.length === 0) continue;
|
|
181
|
+
|
|
182
|
+
const options = [
|
|
183
|
+
...catProviders.map(p => ({ label: `${p.name}`, value: p.id })),
|
|
184
|
+
{ label: 'None', value: null },
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
const selected = await select(rl, `[${cat.label}]`, options, options.length - 1);
|
|
188
|
+
console.log();
|
|
189
|
+
|
|
190
|
+
if (!selected) continue;
|
|
191
|
+
|
|
192
|
+
const provider = catProviders.find(p => p.id === selected);
|
|
193
|
+
if (!provider) continue;
|
|
194
|
+
|
|
195
|
+
// Ask setup questions
|
|
196
|
+
const config = { enabled: true };
|
|
197
|
+
const questions = provider.setup_questions || [];
|
|
198
|
+
for (const q of questions) {
|
|
199
|
+
if (q.type === 'object_list') {
|
|
200
|
+
// Special: collect table/risk/reason list
|
|
201
|
+
const items = await collectObjectList(rl, q);
|
|
202
|
+
config[q.key] = items;
|
|
203
|
+
} else {
|
|
204
|
+
const hint = q.example ? ` (e.g. ${q.example})` : '';
|
|
205
|
+
const label = q.required ? `${q.label}${hint}` : `${q.label}${hint} (optional)`;
|
|
206
|
+
const answer = await ask(rl, label);
|
|
207
|
+
if (answer) config[q.key] = answer;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
integrations[selected] = config;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return integrations;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function collectObjectList(rl, question) {
|
|
218
|
+
const items = [];
|
|
219
|
+
const fields = question.fields || [];
|
|
220
|
+
const hasMore = await confirm(rl, `Add ${question.label} entries?`, false);
|
|
221
|
+
if (!hasMore) return items;
|
|
222
|
+
|
|
223
|
+
let adding = true;
|
|
224
|
+
while (adding) {
|
|
225
|
+
const item = {};
|
|
226
|
+
for (const field of fields) {
|
|
227
|
+
item[field] = await ask(rl, ` ${field}`);
|
|
228
|
+
}
|
|
229
|
+
items.push(item);
|
|
230
|
+
adding = await confirm(rl, ' Add another?', false);
|
|
231
|
+
}
|
|
232
|
+
return items;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── project.yaml builder ────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function buildProjectYaml({ projectName, tagline, projectType, domains, devScope, integrations, platform }) {
|
|
238
|
+
// Build the full integrations block with all known providers
|
|
239
|
+
const allProviders = ['ga4', 'mixpanel', 'notion', 'linear', 'channel_io', 'intercom', 'prod_db', 'notebooklm', 'corti'];
|
|
240
|
+
const integrationsBlock = {};
|
|
241
|
+
|
|
242
|
+
for (const id of allProviders) {
|
|
243
|
+
if (integrations[id]) {
|
|
244
|
+
integrationsBlock[id] = integrations[id];
|
|
245
|
+
} else {
|
|
246
|
+
integrationsBlock[id] = { enabled: false };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
project: {
|
|
252
|
+
name: projectName,
|
|
253
|
+
tagline: tagline || '',
|
|
254
|
+
type: projectType,
|
|
255
|
+
},
|
|
256
|
+
problem: {
|
|
257
|
+
core: '',
|
|
258
|
+
target: '',
|
|
259
|
+
alternatives: [],
|
|
260
|
+
timing: '',
|
|
261
|
+
},
|
|
262
|
+
solution: {
|
|
263
|
+
approach: '',
|
|
264
|
+
differentiation: '',
|
|
265
|
+
outcome: [],
|
|
266
|
+
},
|
|
267
|
+
current_state: {
|
|
268
|
+
stage: '',
|
|
269
|
+
focus: '',
|
|
270
|
+
uncertainty: '',
|
|
271
|
+
next_milestone: '',
|
|
272
|
+
},
|
|
273
|
+
validation: {
|
|
274
|
+
confirmed: [],
|
|
275
|
+
unknown: [],
|
|
276
|
+
customer_feedback: '',
|
|
277
|
+
},
|
|
278
|
+
operations: {
|
|
279
|
+
sprint: {
|
|
280
|
+
enabled: true,
|
|
281
|
+
current: 1,
|
|
282
|
+
duration_weeks: 2,
|
|
283
|
+
d_day: '',
|
|
284
|
+
},
|
|
285
|
+
domains: domains.map(d => ({
|
|
286
|
+
id: d.id,
|
|
287
|
+
name: d.name,
|
|
288
|
+
path: `domains/${d.id}/`,
|
|
289
|
+
})),
|
|
290
|
+
integrations: integrationsBlock,
|
|
291
|
+
dev_scope: devScope,
|
|
292
|
+
spec_site: {
|
|
293
|
+
title: `${projectName} Spec`,
|
|
294
|
+
deploy_url: '',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
_meta: {
|
|
298
|
+
created_at: new Date().toISOString(),
|
|
299
|
+
created_by: 'popilot init',
|
|
300
|
+
needs_deep_interview: true,
|
|
301
|
+
last_interview: null,
|
|
302
|
+
version: '1.0.0',
|
|
303
|
+
...(platform ? { platform } : {}),
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
function getDefaultProjectName(targetDir) {
|
|
311
|
+
const parts = targetDir.split('/');
|
|
312
|
+
const last = parts[parts.length - 1];
|
|
313
|
+
return last === '.' ? parts[parts.length - 2] || 'my-project' : last;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function parseDomainInput(input) {
|
|
317
|
+
if (!input.trim()) return [];
|
|
318
|
+
return input.split(',').map(s => {
|
|
319
|
+
const trimmed = s.trim();
|
|
320
|
+
const colonIdx = trimmed.indexOf(':');
|
|
321
|
+
if (colonIdx > 0) {
|
|
322
|
+
return { id: trimmed.slice(0, colonIdx).trim(), name: trimmed.slice(colonIdx + 1).trim() };
|
|
323
|
+
}
|
|
324
|
+
// No colon — use as both id and name
|
|
325
|
+
return { id: trimmed, name: trimmed };
|
|
326
|
+
}).filter(d => d.id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function safeReaddir(dir) {
|
|
330
|
+
try { return await readdir(dir); } catch { return []; }
|
|
331
|
+
}
|