@cofoundr/init 1.5.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.
Files changed (74) hide show
  1. package/README.md +140 -0
  2. package/bin/cofoundr.mjs +10 -0
  3. package/content/.claude-plugin/plugin.json +18 -0
  4. package/content/README.md +227 -0
  5. package/content/agents/brand-intake.md +129 -0
  6. package/content/agents/consolidate.md +154 -0
  7. package/content/agents/launch-kit-detect.md +109 -0
  8. package/content/agents/spec-phase.md +131 -0
  9. package/content/commands/audit.md +137 -0
  10. package/content/commands/constitution.md +107 -0
  11. package/content/commands/document.md +155 -0
  12. package/content/commands/implement.md +108 -0
  13. package/content/commands/new-feature.md +188 -0
  14. package/content/commands/new-project.md +243 -0
  15. package/content/commands/next.md +129 -0
  16. package/content/commands/onboard.md +176 -0
  17. package/content/commands/plan.md +138 -0
  18. package/content/commands/quick-brief.md +95 -0
  19. package/content/commands/resume.md +99 -0
  20. package/content/commands/review.md +76 -0
  21. package/content/commands/rules-check.md +54 -0
  22. package/content/commands/scope-guard.md +33 -0
  23. package/content/commands/setup-skills.md +109 -0
  24. package/content/commands/specify.md +53 -0
  25. package/content/commands/tasks.md +91 -0
  26. package/content/commands/translate.md +197 -0
  27. package/content/manifest.json +59 -0
  28. package/content/scaffold/.cofoundr/README.md +67 -0
  29. package/content/scaffold/.cofoundr/constitution.md.tmpl +54 -0
  30. package/content/scaffold/.cofoundr/manifest.json.tmpl +15 -0
  31. package/content/scaffold/.cofoundr/memory/decisions.md.tmpl +19 -0
  32. package/content/scaffold/.cofoundr/memory/knowledge.md.tmpl +23 -0
  33. package/content/scaffold/.cofoundr/memory/project.md.tmpl +27 -0
  34. package/content/scaffold/.cofoundr/specs/README.md +38 -0
  35. package/content/scaffold/AGENTS.md.tmpl +74 -0
  36. package/content/templates/agents.md +144 -0
  37. package/content/templates/brand.md +180 -0
  38. package/content/templates/feature.md +70 -0
  39. package/content/templates/phases/phase-template/README.md +65 -0
  40. package/content/templates/phases/phase-template/decisions.md +52 -0
  41. package/content/templates/phases/phase-template/research.md +59 -0
  42. package/content/templates/phases/phase-template/spec.md +90 -0
  43. package/content/templates/phases/phase-template/tests.md +65 -0
  44. package/content/templates/phases/phase-template/ui-spec.md +75 -0
  45. package/content/templates/phases.md +234 -0
  46. package/content/templates/prd.md +89 -0
  47. package/content/templates/product.md +73 -0
  48. package/content/templates/rules.md +99 -0
  49. package/content/templates/tech.md +129 -0
  50. package/package.json +39 -0
  51. package/src/adapters/aider.mjs +35 -0
  52. package/src/adapters/claude-code.mjs +114 -0
  53. package/src/adapters/cline.mjs +46 -0
  54. package/src/adapters/codex.mjs +29 -0
  55. package/src/adapters/copilot.mjs +54 -0
  56. package/src/adapters/cursor.mjs +69 -0
  57. package/src/adapters/gemini.mjs +41 -0
  58. package/src/adapters/index.mjs +14 -0
  59. package/src/adapters/windsurf.mjs +69 -0
  60. package/src/cli.mjs +124 -0
  61. package/src/commands/doctor.mjs +90 -0
  62. package/src/commands/init.mjs +190 -0
  63. package/src/commands/list.mjs +28 -0
  64. package/src/commands/onboard.mjs +130 -0
  65. package/src/commands/remove.mjs +89 -0
  66. package/src/commands/sync.mjs +81 -0
  67. package/src/core/detect.mjs +121 -0
  68. package/src/core/fs.mjs +42 -0
  69. package/src/core/license.mjs +170 -0
  70. package/src/core/log.mjs +32 -0
  71. package/src/core/prompts.mjs +62 -0
  72. package/src/core/scaffold.mjs +179 -0
  73. package/src/core/source.mjs +54 -0
  74. package/src/core/version.mjs +10 -0
@@ -0,0 +1,14 @@
1
+ import { adapter as claudeCode } from './claude-code.mjs';
2
+ import { adapter as cursor } from './cursor.mjs';
3
+ import { adapter as windsurf } from './windsurf.mjs';
4
+ import { adapter as cline } from './cline.mjs';
5
+ import { adapter as codex } from './codex.mjs';
6
+ import { adapter as aider } from './aider.mjs';
7
+ import { adapter as copilot } from './copilot.mjs';
8
+ import { adapter as gemini } from './gemini.mjs';
9
+
10
+ export const adapters = [claudeCode, cursor, windsurf, cline, codex, aider, copilot, gemini];
11
+
12
+ export function getAdapter(id) {
13
+ return adapters.find((a) => a.id === id);
14
+ }
@@ -0,0 +1,69 @@
1
+ // Windsurf adapter.
2
+ // Windsurf supports two formats: legacy `.windsurfrules` (single text file at the repo root)
3
+ // and the newer `.windsurf/rules/*.md`. We write both for maximum compatibility.
4
+
5
+ import { join } from 'node:path';
6
+ import { writeIfChanged } from '../core/fs.mjs';
7
+
8
+ export const adapter = {
9
+ id: 'windsurf',
10
+ name: 'Windsurf',
11
+
12
+ async apply({ repoPath, manifest, opts }) {
13
+ const written = [];
14
+ const skipped = [];
15
+
16
+ const body = renderRule(manifest);
17
+
18
+ bump(await writeIfChanged(join(repoPath, '.windsurf', 'rules', 'cofoundr.md'), body, opts), written, skipped);
19
+ bump(await writeIfChanged(join(repoPath, '.windsurfrules'), legacyBody(manifest), opts), written, skipped);
20
+
21
+ return { written, skipped };
22
+ },
23
+ };
24
+
25
+ function renderRule(manifest) {
26
+ const cmds = manifest.commands.map((c) => `- \`/cofoundr:${c.id}\` — ${c.summary}`).join('\n');
27
+ return `# CoFoundr workspace
28
+
29
+ This project uses **CoFoundr** — a spec-driven workspace at \`.cofoundr/\`.
30
+
31
+ When the user says \`/cofoundr:<name>\`, \`cofoundr <name>\`, or \`run cofoundr <name>\`:
32
+
33
+ 1. Read \`.cofoundr/commands/<name>.md\`.
34
+ 2. Follow it step-by-step. Do not paraphrase, do not skip steps.
35
+ 3. Honor \`.cofoundr/constitution.md\` (the \`Never\` section is binding).
36
+
37
+ ## Reading order
38
+
39
+ 1. \`AGENTS.md\`
40
+ 2. \`.cofoundr/constitution.md\`
41
+ 3. \`.cofoundr/memory/project.md\`
42
+ 4. \`.cofoundr/memory/decisions.md\`
43
+
44
+ ## Commands
45
+
46
+ ${cmds}
47
+
48
+ Refresh with \`npx @cofoundr/init sync\`.
49
+ `;
50
+ }
51
+
52
+ function legacyBody(manifest) {
53
+ return `# CoFoundr (Windsurf rules)
54
+
55
+ This project uses CoFoundr. Read AGENTS.md and .cofoundr/constitution.md before any change.
56
+
57
+ When the user says /cofoundr:<name> or cofoundr <name>:
58
+ 1. Read .cofoundr/commands/<name>.md.
59
+ 2. Follow it step-by-step.
60
+ 3. Honor .cofoundr/constitution.md.
61
+
62
+ Available commands: ${manifest.commands.map((c) => c.id).join(', ')}.
63
+ `;
64
+ }
65
+
66
+ function bump(r, written, skipped) {
67
+ if (r.wrote) written.push(r.path);
68
+ else skipped.push({ path: r.path, reason: r.reason });
69
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,124 @@
1
+ // Tiny zero-dep argv router. Subcommands live in src/commands/.
2
+
3
+ import { parseArgs } from 'node:util';
4
+ import { c } from './core/log.mjs';
5
+ import { init } from './commands/init.mjs';
6
+ import { sync } from './commands/sync.mjs';
7
+ import { onboard } from './commands/onboard.mjs';
8
+ import { doctor } from './commands/doctor.mjs';
9
+ import { listTools } from './commands/list.mjs';
10
+ import { remove } from './commands/remove.mjs';
11
+ import { version } from './core/version.mjs';
12
+
13
+ const HELP = `
14
+ ${c.bold('cofoundr')} ${c.dim('— universal installer for the CoFoundr starter kit')}
15
+
16
+ Usage:
17
+ ${c.cyan('npx @cofoundr/init')} <command> [options]
18
+
19
+ Commands:
20
+ ${c.green('init')} Scaffold .cofoundr/, AGENTS.md, and per-tool shims into the current repo.
21
+ ${c.green('sync')} Refresh commands/agents/templates without touching project content.
22
+ ${c.green('onboard')} Brownfield: scan an existing codebase and write .cofoundr/memory/codebase.md.
23
+ ${c.green('doctor')} Inspect this project's CoFoundr install. Detect drift, missing shims, version mismatches.
24
+ ${c.green('tools')} List supported tools and which ones are detected here.
25
+ ${c.green('remove')} Remove CoFoundr from this project. Leaves .cofoundr/specs/ and .cofoundr/memory/ alone unless --hard.
26
+
27
+ Common flags:
28
+ ${c.dim('--here')} Operate on the current directory (default).
29
+ ${c.dim('--cwd <path>')} Operate on a different directory.
30
+ ${c.dim('--tools <a,b,c>')} Comma-separated tool ids. Default: detect + prompt.
31
+ ${c.dim('--all-tools')} Install shims for every supported tool.
32
+ ${c.dim('--license <key>')} CoFoundr Starter Kit license key. Cached after first use.
33
+ ${c.dim('--yes')}, ${c.dim('-y')} Non-interactive. Accept defaults.
34
+ ${c.dim('--force')} Overwrite existing .cofoundr files (still preserves memory/, specs/).
35
+ ${c.dim('--dry-run')} Print what would change; write nothing.
36
+ ${c.dim('--debug')} Verbose logging.
37
+ ${c.dim('--version')} Print version.
38
+ ${c.dim('--help')} Print this help.
39
+
40
+ Examples:
41
+ ${c.cyan('npx @cofoundr/init init')} ${c.dim('# detect tools, prompt to confirm')}
42
+ ${c.cyan('npx @cofoundr/init init --tools claude-code,cursor --yes')}
43
+ ${c.cyan('npx @cofoundr/init onboard')} ${c.dim('# map existing codebase')}
44
+ ${c.cyan('npx @cofoundr/init sync --all-tools')} ${c.dim('# refresh every shim')}
45
+
46
+ More: https://cofoundr.ai
47
+ `;
48
+
49
+ export async function run(argv) {
50
+ const top = argv[0];
51
+
52
+ if (!top || top === '--help' || top === '-h' || top === 'help') {
53
+ process.stdout.write(HELP);
54
+ return;
55
+ }
56
+
57
+ if (top === '--version' || top === '-v') {
58
+ process.stdout.write(`${await version()}\n`);
59
+ return;
60
+ }
61
+
62
+ const sub = top;
63
+ const rest = argv.slice(1);
64
+
65
+ const opts = parseSharedFlags(rest);
66
+
67
+ switch (sub) {
68
+ case 'init':
69
+ return init(opts);
70
+ case 'sync':
71
+ return sync(opts);
72
+ case 'onboard':
73
+ return onboard(opts);
74
+ case 'doctor':
75
+ return doctor(opts);
76
+ case 'tools':
77
+ case 'list':
78
+ return listTools(opts);
79
+ case 'remove':
80
+ case 'uninstall':
81
+ return remove(opts);
82
+ default:
83
+ process.stderr.write(`cofoundr: unknown command "${sub}"\n\n`);
84
+ process.stdout.write(HELP);
85
+ process.exitCode = 1;
86
+ }
87
+ }
88
+
89
+ function parseSharedFlags(rest) {
90
+ const { values, positionals } = parseArgs({
91
+ args: rest,
92
+ allowPositionals: true,
93
+ strict: false,
94
+ options: {
95
+ here: { type: 'boolean' },
96
+ cwd: { type: 'string' },
97
+ tools: { type: 'string' },
98
+ 'all-tools': { type: 'boolean' },
99
+ license: { type: 'string' },
100
+ yes: { type: 'boolean', short: 'y' },
101
+ force: { type: 'boolean' },
102
+ 'dry-run': { type: 'boolean' },
103
+ debug: { type: 'boolean' },
104
+ refresh: { type: 'boolean' },
105
+ hard: { type: 'boolean' },
106
+ },
107
+ });
108
+
109
+ if (values.debug) process.env.COFOUNDR_DEBUG = '1';
110
+
111
+ return {
112
+ cwd: values.cwd || process.cwd(),
113
+ tools: values.tools ? values.tools.split(',').map((s) => s.trim()).filter(Boolean) : null,
114
+ allTools: !!values['all-tools'],
115
+ license: values.license || null,
116
+ yes: !!values.yes,
117
+ force: !!values.force,
118
+ dryRun: !!values['dry-run'],
119
+ debug: !!values.debug,
120
+ refresh: !!values.refresh,
121
+ hard: !!values.hard,
122
+ positionals,
123
+ };
124
+ }
@@ -0,0 +1,90 @@
1
+ import { resolve, join } from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { c, log } from '../core/log.mjs';
4
+ import { locateSource, loadManifest } from '../core/source.mjs';
5
+ import { detectTools } from '../core/detect.mjs';
6
+ import { getCachedLicense, verifyLicense, reasonMessage } from '../core/license.mjs';
7
+
8
+ export async function doctor(opts) {
9
+ const repoPath = resolve(opts.cwd);
10
+ log.info(`${c.bold('CoFoundr doctor')} ${c.dim(`→ ${repoPath}`)}`);
11
+
12
+ const projManPath = join(repoPath, '.cofoundr', 'manifest.json');
13
+ if (!existsSync(projManPath)) {
14
+ log.err('No CoFoundr install detected here. Run `npx @cofoundr/init init`.');
15
+ process.exitCode = 1;
16
+ return;
17
+ }
18
+
19
+ const projMan = JSON.parse(readFileSync(projManPath, 'utf8'));
20
+ const source = await locateSource();
21
+ const manifest = await loadManifest(source);
22
+
23
+ log.blank();
24
+ log.info(`${c.bold('Versions')}`);
25
+ log.list(`Project: v${projMan.cofoundrVersion}`);
26
+ log.list(`Latest: v${manifest.version}`);
27
+ if (projMan.cofoundrVersion !== manifest.version) {
28
+ log.warn(`Out of date — run ${c.cyan('npx @cofoundr/init sync')} to refresh.`);
29
+ } else {
30
+ log.ok('Up to date.');
31
+ }
32
+
33
+ log.blank();
34
+ log.info(`${c.bold('Workspace')}`);
35
+ for (const f of [
36
+ '.cofoundr/constitution.md',
37
+ '.cofoundr/memory/project.md',
38
+ '.cofoundr/memory/decisions.md',
39
+ '.cofoundr/specs/README.md',
40
+ 'AGENTS.md',
41
+ ]) {
42
+ const present = existsSync(join(repoPath, f));
43
+ log.list(`${present ? c.green('✓') : c.red('✗')} ${f}`);
44
+ }
45
+
46
+ log.blank();
47
+ log.info(`${c.bold('Per-tool shims')}`);
48
+ const tools = projMan.tools || [];
49
+ for (const id of tools) {
50
+ const target = manifest.targets.find((t) => t.id === id);
51
+ if (!target) {
52
+ log.warn(`Unknown tool id "${id}" recorded in manifest.`);
53
+ continue;
54
+ }
55
+ const allPresent = target.config.every((p) => existsSync(join(repoPath, p)) || existsSync(join(repoPath, p.replace(/\/$/, ''))));
56
+ log.list(`${allPresent ? c.green('✓') : c.yellow('!')} ${target.name} ${c.dim(`(${target.config.join(', ')})`)}`);
57
+ }
58
+
59
+ log.blank();
60
+ const detected = detectTools(repoPath);
61
+ const detectedIds = Object.entries(detected).filter(([, v]) => v.found).map(([id]) => id);
62
+ const missing = detectedIds.filter((id) => !tools.includes(id));
63
+ if (missing.length) {
64
+ log.info(`${c.bold('Tools detected in repo but not yet shimmed:')}`);
65
+ for (const id of missing) {
66
+ const target = manifest.targets.find((t) => t.id === id);
67
+ log.list(`${target?.name ?? id} — add with ${c.cyan(`npx @cofoundr/init sync --tools ${id}`)}`);
68
+ }
69
+ }
70
+
71
+ log.blank();
72
+ log.info(`${c.bold('License')}`);
73
+ const cached = getCachedLicense();
74
+ if (!cached?.key) {
75
+ log.list(c.dim('No cached license. Run `npx @cofoundr/init init --license <key>` to activate.'));
76
+ } else {
77
+ const last4 = cached.key.slice(-4);
78
+ log.list(c.dim(`csk_live_…${last4} plan: ${cached.plan ?? 'starter_kit'} email: ${cached.email ?? '—'}`));
79
+ try {
80
+ const result = await verifyLicense(cached.key, { skipCache: true });
81
+ if (result.valid) {
82
+ log.list(`${c.green('✓')} Server confirms key is active.`);
83
+ } else {
84
+ log.list(`${c.red('✗')} ${reasonMessage(result.reason)}`);
85
+ }
86
+ } catch {
87
+ log.list(c.yellow('Could not reach license server (cached key still trusted within TTL).'));
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,190 @@
1
+ import { resolve, basename } from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { c, log } from '../core/log.mjs';
4
+ import { locateSource, loadManifest } from '../core/source.mjs';
5
+ import { detectTools, detectRuntime } from '../core/detect.mjs';
6
+ import { confirm, multiSelect, text } from '../core/prompts.mjs';
7
+ import { writeScaffold, writeProjectManifest } from '../core/scaffold.mjs';
8
+ import { adapters, getAdapter } from '../adapters/index.mjs';
9
+ import {
10
+ verifyLicense,
11
+ getCachedLicense,
12
+ isValidLicenseFormat,
13
+ reasonMessage,
14
+ PURCHASE_URL,
15
+ RECOVERY_URL,
16
+ } from '../core/license.mjs';
17
+
18
+ export async function init(opts) {
19
+ const repoPath = resolve(opts.cwd);
20
+ log.info(`${c.bold('CoFoundr init')} ${c.dim(`→ ${repoPath}`)}`);
21
+
22
+ if (!existsSync(repoPath)) throw new Error(`Target directory does not exist: ${repoPath}`);
23
+
24
+ // License gate — refuses scaffold without a valid Starter Kit key.
25
+ const licenseOk = await ensureLicense(opts);
26
+ if (!licenseOk) {
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+
31
+ const source = await locateSource();
32
+ const manifest = await loadManifest(source);
33
+ log.debug(`Using source: ${source.root} (${source.mode})`);
34
+
35
+ const detected = detectTools(repoPath);
36
+ const runtime = detectRuntime(repoPath);
37
+
38
+ const detectedIds = Object.entries(detected)
39
+ .filter(([, v]) => v.found)
40
+ .map(([id]) => id);
41
+
42
+ log.blank();
43
+ log.info(`${c.bold('Detected:')} ${runtime.languages.join(', ') || c.dim('no language signal')}`);
44
+ if (runtime.frameworks.length) log.list(`Framework: ${runtime.frameworks.join(', ')}`);
45
+ if (runtime.isMonorepo) log.list('Layout: monorepo');
46
+ log.list(
47
+ `AI tools already in repo: ${
48
+ detectedIds.length
49
+ ? detectedIds.map((id) => manifest.targets.find((t) => t.id === id)?.name ?? id).join(', ')
50
+ : c.dim('none')
51
+ }`
52
+ );
53
+ log.blank();
54
+
55
+ // Pick which tools to install shims for.
56
+ let selected;
57
+ if (opts.allTools) {
58
+ selected = adapters.map((a) => a.id);
59
+ } else if (opts.tools) {
60
+ const valid = new Set(adapters.map((a) => a.id));
61
+ const invalid = opts.tools.filter((t) => !valid.has(t));
62
+ if (invalid.length) throw new Error(`Unknown tool(s): ${invalid.join(', ')}`);
63
+ selected = opts.tools;
64
+ } else if (opts.yes) {
65
+ selected = detectedIds.length ? detectedIds : ['claude-code'];
66
+ } else {
67
+ selected = await multiSelect(
68
+ 'Which AI tools should CoFoundr install shims for?',
69
+ adapters.map((a) => ({
70
+ id: a.id,
71
+ label: c.bold(manifest.targets.find((t) => t.id === a.id)?.name ?? a.id),
72
+ note: detected[a.id]?.found ? c.green('detected in repo') : '',
73
+ })),
74
+ { defaults: detectedIds.length ? detectedIds : ['claude-code'] }
75
+ );
76
+ }
77
+
78
+ if (selected.length === 0) {
79
+ log.warn('No tools selected. AGENTS.md will still be written — every modern coding agent reads it.');
80
+ }
81
+
82
+ // Project name: from package.json if present, else basename, else prompt.
83
+ const projectName = await resolveProjectName(repoPath, opts);
84
+
85
+ // Write the scaffold.
86
+ log.blank();
87
+ log.step(`Writing ${c.bold('.cofoundr/')} workspace and ${c.bold('AGENTS.md')}`);
88
+ const scaffold = await writeScaffold({
89
+ repoPath,
90
+ source,
91
+ manifest,
92
+ projectName,
93
+ selectedTools: selected,
94
+ dryRun: opts.dryRun,
95
+ force: opts.force,
96
+ });
97
+ reportFileResult(scaffold);
98
+
99
+ // Write per-tool shims.
100
+ for (const id of selected) {
101
+ const adapter = getAdapter(id);
102
+ if (!adapter) {
103
+ log.warn(`No adapter for "${id}" — skipping.`);
104
+ continue;
105
+ }
106
+ log.step(`Installing ${c.bold(adapter.name)} shim`);
107
+ const result = await adapter.apply({ repoPath, source, manifest, opts: { dryRun: opts.dryRun, force: opts.force } });
108
+ reportFileResult(result);
109
+ }
110
+
111
+ // Project manifest stamp.
112
+ await writeProjectManifest({ repoPath, manifest, selectedTools: selected, dryRun: opts.dryRun });
113
+
114
+ log.blank();
115
+ log.ok(`CoFoundr v${manifest.version} initialized in ${c.bold(repoPath)}`);
116
+ log.blank();
117
+ log.info(`${c.bold('Next steps:')}`);
118
+ log.list(`Open the repo in any of: ${selected.map((id) => manifest.targets.find((t) => t.id === id)?.name ?? id).join(', ')}`);
119
+ log.list(`Run ${c.cyan('/cofoundr:constitution')} to lock the rules.`);
120
+ log.list(`Run ${c.cyan('/cofoundr:onboard')} if this is an existing codebase, or ${c.cyan('/cofoundr:new-project')} for a greenfield spec.`);
121
+ log.list(`Re-sync after a CoFoundr upgrade with ${c.cyan('npx @cofoundr/init sync')}.`);
122
+ }
123
+
124
+ async function resolveProjectName(repoPath, opts) {
125
+ // Prefer package.json#name.
126
+ const pkgPath = resolve(repoPath, 'package.json');
127
+ if (existsSync(pkgPath)) {
128
+ try {
129
+ const pkg = JSON.parse(await (await import('node:fs/promises')).readFile(pkgPath, 'utf8'));
130
+ if (pkg.name) return pkg.name;
131
+ } catch {}
132
+ }
133
+ if (opts.yes) return basename(repoPath);
134
+ return text('Project name?', { defaultValue: basename(repoPath) });
135
+ }
136
+
137
+ async function ensureLicense(opts) {
138
+ // 1. --license flag, 2. COFOUNDR_LICENSE_KEY env, 3. cached key, 4. prompt.
139
+ let key = opts.license || process.env.COFOUNDR_LICENSE_KEY || null;
140
+ let cachedHit = false;
141
+
142
+ if (!key) {
143
+ const cached = getCachedLicense();
144
+ if (cached?.key && isValidLicenseFormat(cached.key)) {
145
+ key = cached.key;
146
+ cachedHit = true;
147
+ }
148
+ }
149
+
150
+ if (!key) {
151
+ log.blank();
152
+ log.info(`${c.bold('License required.')} Activate the CoFoundr Starter Kit CLI.`);
153
+ log.list(`Buy at ${c.cyan(PURCHASE_URL)} — keys arrive in your purchase email.`);
154
+ log.list(`Lost your key? Recover at ${c.cyan(RECOVERY_URL)}.`);
155
+ log.blank();
156
+ key = (await text('Paste your license key', { defaultValue: '' })).trim();
157
+ }
158
+
159
+ if (!key) {
160
+ log.err('No license key provided. Run with --license <key> or set COFOUNDR_LICENSE_KEY.');
161
+ return false;
162
+ }
163
+
164
+ if (!isValidLicenseFormat(key)) {
165
+ log.err('License key format is invalid. Expected: csk_live_<32 chars>.');
166
+ return false;
167
+ }
168
+
169
+ const result = await verifyLicense(key);
170
+ if (!result.valid) {
171
+ log.err(reasonMessage(result.reason));
172
+ return false;
173
+ }
174
+
175
+ const tag = result.source === 'cache' ? c.dim('(verified from cache)') : c.dim('(verified)');
176
+ log.ok(`License active for ${c.bold(result.email || 'your account')} ${tag}`);
177
+ if (cachedHit && opts.debug) log.list(c.dim('using cached key'));
178
+ return true;
179
+ }
180
+
181
+ function reportFileResult({ written, skipped }) {
182
+ for (const p of written) log.ok(c.dim(p));
183
+ for (const s of skipped) {
184
+ if (s.reason === 'unchanged') {
185
+ log.list(c.dim(`${s.path} ${c.dim('(unchanged)')}`));
186
+ } else {
187
+ log.list(`${c.yellow('skip')} ${s.path} ${c.dim(`(${s.reason}; pass --force to overwrite)`)}`);
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,28 @@
1
+ import { resolve } from 'node:path';
2
+ import { c, log } from '../core/log.mjs';
3
+ import { locateSource, loadManifest } from '../core/source.mjs';
4
+ import { detectTools } from '../core/detect.mjs';
5
+ import { adapters } from '../adapters/index.mjs';
6
+
7
+ export async function listTools(opts) {
8
+ const source = await locateSource();
9
+ const manifest = await loadManifest(source);
10
+ const repoPath = resolve(opts.cwd);
11
+ const detected = detectTools(repoPath);
12
+
13
+ log.info(`${c.bold('Supported AI tools')}`);
14
+ log.blank();
15
+
16
+ const order = adapters.map((a) => a.id);
17
+ for (const id of order) {
18
+ const target = manifest.targets.find((t) => t.id === id);
19
+ if (!target) continue;
20
+ const found = detected[id]?.found;
21
+ const tag = found ? c.green('detected') : c.dim('available');
22
+ const cfg = c.dim(`writes: ${target.config.join(', ')}`);
23
+ process.stdout.write(` ${c.bold(target.name.padEnd(22))} ${tag.padEnd(20)} ${cfg}\n`);
24
+ }
25
+
26
+ log.blank();
27
+ log.info(c.dim('Pass --tools <id1,id2> to init or sync to install only specific shims.'));
28
+ }
@@ -0,0 +1,130 @@
1
+ // Onboard — static-analysis pass that writes a starter `.cofoundr/memory/codebase.md`.
2
+ // The richer pass (route extraction, schema parsing, doc backfill) lives in the in-tool
3
+ // /cofoundr:onboard slash command. This CLI step gives the AI a head start so its first
4
+ // run isn't from scratch.
5
+
6
+ import { resolve, join, basename } from 'node:path';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { c, log } from '../core/log.mjs';
9
+ import { detectTools, detectRuntime, listTopLevel } from '../core/detect.mjs';
10
+ import { writeIfChanged } from '../core/fs.mjs';
11
+
12
+ export async function onboard(opts) {
13
+ const repoPath = resolve(opts.cwd);
14
+ if (!existsSync(repoPath)) throw new Error(`Target directory does not exist: ${repoPath}`);
15
+
16
+ log.info(`${c.bold('CoFoundr onboard (static pass)')} ${c.dim(`→ ${repoPath}`)}`);
17
+
18
+ const runtime = detectRuntime(repoPath);
19
+ const tools = detectTools(repoPath);
20
+ const topLevel = listTopLevel(repoPath, 2);
21
+
22
+ const pkg = readJSON(join(repoPath, 'package.json'));
23
+ const projectName = pkg?.name ?? basename(repoPath);
24
+ const projectDescription = pkg?.description ?? '<!-- no package.json description; fill via /cofoundr:onboard in-tool -->';
25
+
26
+ const summary = renderCodebaseMap({
27
+ projectName,
28
+ projectDescription,
29
+ runtime,
30
+ tools,
31
+ topLevel,
32
+ pkg,
33
+ });
34
+
35
+ const dest = join(repoPath, '.cofoundr', 'memory', 'codebase.md');
36
+ const r = await writeIfChanged(dest, summary, { dryRun: opts.dryRun, force: opts.refresh || opts.force });
37
+ if (r.wrote) {
38
+ log.ok(`Wrote ${dest}`);
39
+ } else {
40
+ log.warn(`${dest} ${c.dim(`(${r.reason}; pass --refresh to overwrite)`)}`);
41
+ }
42
+
43
+ log.blank();
44
+ log.info(c.bold('Next:'));
45
+ log.list(`Open the project in your AI tool and run ${c.cyan('/cofoundr:onboard')} for the deep pass (routes, schema, doc backfill).`);
46
+ log.list(`Then ${c.cyan('/cofoundr:constitution')} to confirm the rules.`);
47
+ log.list(`Then ${c.cyan('/cofoundr:audit')} to find drift.`);
48
+ }
49
+
50
+ function readJSON(path) {
51
+ try {
52
+ return JSON.parse(readFileSync(path, 'utf8'));
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function renderCodebaseMap({ projectName, projectDescription, runtime, tools, topLevel, pkg }) {
59
+ const today = new Date().toISOString().slice(0, 10);
60
+ const langs = runtime.languages.length ? runtime.languages.join(', ') : '<unknown>';
61
+ const fws = runtime.frameworks.length ? runtime.frameworks.join(', ') : '<unknown>';
62
+ const pms = runtime.packageManagers.length ? runtime.packageManagers.join(', ') : '<unknown>';
63
+ const layout = runtime.isMonorepo ? 'monorepo' : 'single project';
64
+
65
+ const aiTools = Object.entries(tools)
66
+ .filter(([, v]) => v.found)
67
+ .map(([id, v]) => `- **${id}** — evidence: ${v.evidence.join(', ')}`)
68
+ .join('\n') || '- _none detected_';
69
+
70
+ const tree = topLevel
71
+ .filter((e) => !e.path.includes('/'))
72
+ .slice(0, 50)
73
+ .map((e) => `- ${e.dir ? `${e.path}/` : e.path}`)
74
+ .join('\n');
75
+
76
+ const deps = pkg
77
+ ? Object.entries({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) })
78
+ .slice(0, 30)
79
+ .map(([k, v]) => `- ${k} \`${v}\``)
80
+ .join('\n')
81
+ : '_no package.json_';
82
+
83
+ return `# Codebase Map — ${projectName}
84
+
85
+ > Generated by \`cofoundr onboard\` (static pass) on ${today}.
86
+ > This is a starting point — run \`/cofoundr:onboard\` inside your AI tool for the deep pass.
87
+
88
+ ## TL;DR
89
+
90
+ ${projectDescription}
91
+
92
+ ## Layout
93
+
94
+ - ${layout}
95
+ - Languages: ${langs}
96
+ - Frameworks (config files seen): ${fws}
97
+ - Package manager(s): ${pms}
98
+
99
+ ## Top-level
100
+
101
+ ${tree || '_empty repo_'}
102
+
103
+ ## Stack signals
104
+
105
+ | Source | Value |
106
+ |--------|-------|
107
+ | package.json#name | ${pkg?.name ?? '—'} |
108
+ | package.json#version | ${pkg?.version ?? '—'} |
109
+ | Node engines | ${pkg?.engines?.node ?? '—'} |
110
+ | Scripts | ${pkg?.scripts ? Object.keys(pkg.scripts).join(', ') : '—'} |
111
+
112
+ ## Top dependencies
113
+
114
+ ${deps}
115
+
116
+ ## AI tools already configured
117
+
118
+ ${aiTools}
119
+
120
+ ## Gaps and Open Questions
121
+
122
+ <!-- GAP: routes — run /cofoundr:onboard inside Claude Code / Cursor / etc. for route enumeration. -->
123
+ <!-- GAP: data model — same. -->
124
+ <!-- GAP: external integrations — same. -->
125
+ <!-- GAP: tests + CI — same. -->
126
+
127
+ ---
128
+ <sub>Refresh with \`cofoundr onboard --refresh\`. The deep pass lives in \`/cofoundr:onboard\`.</sub>
129
+ `;
130
+ }