knowzcode 0.4.0 → 0.5.2

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 (79) hide show
  1. package/.claude-plugin/marketplace.json +61 -61
  2. package/.claude-plugin/plugin.json +8 -8
  3. package/LICENSE +121 -121
  4. package/README.md +379 -354
  5. package/agents/analyst.md +114 -114
  6. package/agents/architect.md +200 -200
  7. package/agents/builder.md +104 -104
  8. package/agents/closer.md +177 -177
  9. package/agents/context-scout.md +54 -54
  10. package/agents/knowledge-migrator.md +349 -349
  11. package/agents/knowz-scout.md +83 -83
  12. package/agents/knowz-scribe.md +180 -180
  13. package/agents/microfix-specialist.md +135 -135
  14. package/agents/project-advisor.md +111 -111
  15. package/agents/reviewer.md +172 -172
  16. package/agents/security-officer.md +194 -194
  17. package/agents/test-advisor.md +162 -162
  18. package/agents/update-coordinator.md +394 -394
  19. package/bin/knowzcode.mjs +1457 -1199
  20. package/commands/audit.md +328 -328
  21. package/commands/connect-mcp.md +574 -549
  22. package/commands/fix.md +107 -107
  23. package/commands/init.md +616 -500
  24. package/commands/learn.md +332 -332
  25. package/commands/plan.md +272 -272
  26. package/commands/register.md +757 -733
  27. package/commands/status.md +338 -309
  28. package/commands/telemetry-setup.md +368 -368
  29. package/commands/telemetry.md +188 -188
  30. package/commands/work.md +1204 -1204
  31. package/knowzcode/automation_manifest.md +59 -59
  32. package/knowzcode/claude_code_execution.md +431 -431
  33. package/knowzcode/copilot_execution.md +231 -231
  34. package/knowzcode/enterprise/compliance_manifest.md +137 -137
  35. package/knowzcode/enterprise/compliance_status.md +30 -30
  36. package/knowzcode/enterprise/guidelines/code-quality.md +67 -67
  37. package/knowzcode/enterprise/guidelines/security.md +355 -355
  38. package/knowzcode/enterprise/templates/guideline-template.md +55 -55
  39. package/knowzcode/gitignore.template +13 -13
  40. package/knowzcode/knowzcode_architecture.md +51 -51
  41. package/knowzcode/knowzcode_log.md +142 -142
  42. package/knowzcode/knowzcode_loop.md +601 -596
  43. package/knowzcode/knowzcode_orchestration.md +66 -66
  44. package/knowzcode/knowzcode_project.md +48 -48
  45. package/knowzcode/knowzcode_tracker.md +40 -40
  46. package/knowzcode/knowzcode_vaults.md +257 -257
  47. package/knowzcode/mcp_config.md +196 -191
  48. package/knowzcode/planning/Readme.md +6 -6
  49. package/knowzcode/platform_adapters.md +2577 -1260
  50. package/knowzcode/prompts/Execute_Micro_Fix.md +57 -57
  51. package/knowzcode/prompts/Investigate_Codebase.md +227 -227
  52. package/knowzcode/prompts/Migrate_Knowledge.md +301 -301
  53. package/knowzcode/prompts/Refactor_Node.md +72 -72
  54. package/knowzcode/prompts/Spec_Verification_Checkpoint.md +59 -59
  55. package/knowzcode/prompts/[LOOP_1A]__Propose_Change_Set.md +52 -52
  56. package/knowzcode/prompts/[LOOP_1B]__Draft_Specs.md +75 -75
  57. package/knowzcode/prompts/[LOOP_2A]__Implement_Change_Set.md +55 -55
  58. package/knowzcode/prompts/[LOOP_2B]__Verify_Implementation.md +72 -72
  59. package/knowzcode/prompts/[LOOP_3]__Finalize_And_Commit.md +67 -67
  60. package/knowzcode/specs/Readme.md +10 -10
  61. package/knowzcode/telemetry_config.md +89 -89
  62. package/knowzcode/user_preferences.md +120 -120
  63. package/package.json +53 -53
  64. package/skills/alias-resolver.json +15 -15
  65. package/skills/architecture-diff.json +12 -12
  66. package/skills/check-installation-status.json +14 -14
  67. package/skills/continue.md +126 -126
  68. package/skills/environment-guard.json +12 -12
  69. package/skills/generate-workgroup-id.json +25 -25
  70. package/skills/install-knowzcode.json +21 -21
  71. package/skills/load-core-context.json +18 -18
  72. package/skills/log-entry-builder.json +15 -15
  73. package/skills/spec-quality-check.json +14 -14
  74. package/skills/spec-template.json +15 -15
  75. package/skills/spec-validator.json +25 -25
  76. package/skills/start-work.md +224 -224
  77. package/skills/tracker-scan.json +12 -12
  78. package/skills/tracker-update.json +28 -28
  79. package/skills/validate-installation.json +14 -14
package/bin/knowzcode.mjs CHANGED
@@ -1,1199 +1,1457 @@
1
- #!/usr/bin/env node
2
-
3
- // KnowzCode CLI — Zero-dependency Node.js installer
4
- // Usage: npx knowzcode [install|uninstall|upgrade|detect] [options]
5
-
6
- import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'fs';
7
- import { join, resolve, dirname, basename } from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { createInterface } from 'readline/promises';
10
-
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = dirname(__filename);
13
- const PKG_ROOT = resolve(__dirname, '..');
14
- const VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version;
15
-
16
- // ─── Colors ──────────────────────────────────────────────────────────────────
17
-
18
- const c = {
19
- reset: '\x1b[0m',
20
- bold: '\x1b[1m',
21
- dim: '\x1b[2m',
22
- red: '\x1b[31m',
23
- green: '\x1b[32m',
24
- yellow: '\x1b[33m',
25
- blue: '\x1b[34m',
26
- cyan: '\x1b[36m',
27
- };
28
-
29
- const log = {
30
- info: (msg) => console.log(`${c.blue}[INFO]${c.reset} ${msg}`),
31
- ok: (msg) => console.log(`${c.green}[OK]${c.reset} ${msg}`),
32
- warn: (msg) => console.log(`${c.yellow}[WARN]${c.reset} ${msg}`),
33
- err: (msg) => console.error(`${c.red}[ERROR]${c.reset} ${msg}`),
34
- };
35
-
36
- // ─── Platform Definitions ────────────────────────────────────────────────────
37
-
38
- const PLATFORMS = {
39
- claude: {
40
- name: 'Claude Code',
41
- detect: (dir) => existsSync(join(dir, '.claude')) || existsSync(join(dir, '.claude-plugin')),
42
- adapterPath: null, // Claude uses .claude/ dir structure, not a single adapter file
43
- },
44
- codex: {
45
- name: 'OpenAI Codex',
46
- detect: (dir) => existsSync(join(dir, 'AGENTS.md')) || existsSync(join(dir, 'AGENTS.override.md')) || existsSync(join(dir, '.codex')),
47
- adapterPath: (dir) => join(dir, 'AGENTS.md'),
48
- templateHeader: '## OpenAI Codex (AGENTS.md)',
49
- },
50
- gemini: {
51
- name: 'Gemini CLI',
52
- detect: (dir) => existsSync(join(dir, 'GEMINI.md')) || existsSync(join(dir, '.gemini')),
53
- adapterPath: (dir) => join(dir, 'GEMINI.md'),
54
- templateHeader: '## Google Gemini CLI (GEMINI.md)',
55
- },
56
- cursor: {
57
- name: 'Cursor',
58
- detect: (dir) => existsSync(join(dir, '.cursor', 'rules')) || existsSync(join(dir, '.cursorrules')),
59
- adapterPath: (dir) => join(dir, '.cursor', 'rules', 'knowzcode.mdc'),
60
- templateHeader: '## Cursor (`.cursor/rules/knowzcode.mdc`)',
61
- },
62
- copilot: {
63
- name: 'GitHub Copilot',
64
- detect: (dir) => existsSync(join(dir, '.github', 'copilot-instructions.md')) || existsSync(join(dir, '.github')),
65
- adapterPath: (dir) => join(dir, '.github', 'copilot-instructions.md'),
66
- templateHeader: '## GitHub Copilot',
67
- },
68
- windsurf: {
69
- name: 'Windsurf',
70
- detect: (dir) => existsSync(join(dir, '.windsurf', 'rules')) || existsSync(join(dir, '.windsurfrules')),
71
- adapterPath: (dir) => join(dir, '.windsurf', 'rules', 'knowzcode.md'),
72
- templateHeader: '## Windsurf (`.windsurf/rules/knowzcode.md`)',
73
- },
74
- };
75
-
76
- // ─── CLI Argument Parser ─────────────────────────────────────────────────────
77
-
78
- function parseArgs(argv) {
79
- const args = argv.slice(2);
80
- const opts = {
81
- command: null,
82
- target: process.cwd(),
83
- platforms: [],
84
- force: false,
85
- global: false,
86
- verbose: false,
87
- agentTeams: false,
88
- };
89
-
90
- let i = 0;
91
- while (i < args.length) {
92
- const arg = args[i];
93
- if (arg === '--target' && i + 1 < args.length) {
94
- opts.target = resolve(args[++i]);
95
- } else if (arg === '--platforms' && i + 1 < args.length) {
96
- opts.platforms = args[++i].split(',').map((p) => p.trim().toLowerCase());
97
- } else if (arg === '--force') {
98
- opts.force = true;
99
- } else if (arg === '--global') {
100
- opts.global = true;
101
- } else if (arg === '--agent-teams') {
102
- opts.agentTeams = true;
103
- } else if (arg === '--verbose') {
104
- opts.verbose = true;
105
- } else if (arg === '--help' || arg === '-h') {
106
- opts.command = 'help';
107
- } else if (arg === '--version' || arg === '-v') {
108
- opts.command = 'version';
109
- } else if (!arg.startsWith('-') && !opts.command) {
110
- opts.command = arg.toLowerCase();
111
- }
112
- i++;
113
- }
114
-
115
- return opts;
116
- }
117
-
118
- // ─── Platform Detection ──────────────────────────────────────────────────────
119
-
120
- function detectPlatforms(dir) {
121
- const detected = [];
122
- for (const [id, platform] of Object.entries(PLATFORMS)) {
123
- if (platform.detect(dir)) {
124
- detected.push(id);
125
- }
126
- }
127
- return detected;
128
- }
129
-
130
- // ─── Adapter Template Parser ─────────────────────────────────────────────────
131
- // Returns Map<platformId, { primary: string, files: Map<relativePath, { content, lang }> }>
132
-
133
- function injectVersion(content) {
134
- return content.replace(/vX\.Y\.Z/g, `v${VERSION}`);
135
- }
136
-
137
- function extractSection(content, headerIdx) {
138
- const afterHeader = content.slice(headerIdx);
139
- const nextSection = afterHeader.search(/\r?\n---\r?\n\r?\n## /);
140
- return nextSection !== -1 ? afterHeader.slice(0, nextSection) : afterHeader;
141
- }
142
-
143
- function extractFence(text, lang, startFrom = 0) {
144
- const marker = '```' + lang;
145
- const fenceStart = text.indexOf(marker, startFrom);
146
- if (fenceStart === -1) return null;
147
- const contentStart = text.indexOf('\n', fenceStart) + 1;
148
- const fenceEnd = text.indexOf('\n```', contentStart);
149
- if (fenceEnd === -1) return null;
150
- return { content: text.slice(contentStart, fenceEnd), endIdx: fenceEnd + 4 };
151
- }
152
-
153
- function parseCopilotSection(section) {
154
- const files = new Map();
155
-
156
- // Section A: copilot-instructions.md (first ```markdown before ### B.)
157
- const sectionBIdx = section.indexOf('### B.');
158
- const sectionA = sectionBIdx !== -1 ? section.slice(0, sectionBIdx) : section;
159
- const primaryFence = extractFence(sectionA, 'markdown');
160
- if (!primaryFence) return null;
161
-
162
- // Section B: prompt files (#### kc-*.prompt.md headers)
163
- const headerRegex = /#### (kc-[\w-]+\.prompt\.md)/g;
164
- const headers = [];
165
- let match;
166
- while ((match = headerRegex.exec(section)) !== null) {
167
- headers.push({ filename: match[1], index: match.index });
168
- }
169
-
170
- const sectionCIdx = section.indexOf('### C.');
171
- for (let i = 0; i < headers.length; i++) {
172
- const start = headers[i].index;
173
- const end = i + 1 < headers.length
174
- ? headers[i + 1].index
175
- : (sectionCIdx !== -1 && sectionCIdx > start ? sectionCIdx : section.length);
176
- const subSection = section.slice(start, end);
177
-
178
- const fenceOpen = subSection.indexOf('```markdown');
179
- if (fenceOpen === -1) continue;
180
- const contentStart = subSection.indexOf('\n', fenceOpen) + 1;
181
- // Use lastIndexOf to handle prompt files that contain inner code fences
182
- const lastFenceClose = subSection.lastIndexOf('\n```');
183
- if (lastFenceClose <= contentStart) continue;
184
-
185
- files.set(`.github/prompts/${headers[i].filename}`, {
186
- content: subSection.slice(contentStart, lastFenceClose),
187
- lang: 'markdown',
188
- });
189
- }
190
-
191
- // Section C: .vscode/mcp.json
192
- if (sectionCIdx !== -1) {
193
- const sectionDIdx = section.indexOf('### D.', sectionCIdx);
194
- const sectionC = section.slice(sectionCIdx, sectionDIdx !== -1 ? sectionDIdx : section.length);
195
- const jsonFence = extractFence(sectionC, 'json');
196
- if (jsonFence) {
197
- files.set('.vscode/mcp.json', { content: jsonFence.content, lang: 'json' });
198
- }
199
- }
200
-
201
- return { primary: primaryFence.content, files };
202
- }
203
-
204
- function parseGeminiSection(section) {
205
- const files = new Map();
206
-
207
- // Extract TOML blocks: ```toml fences with # .gemini/commands/kc/{name}.toml comment
208
- let searchFrom = 0;
209
- while (true) {
210
- const fenceStart = section.indexOf('```toml', searchFrom);
211
- if (fenceStart === -1) break;
212
- const contentStart = section.indexOf('\n', fenceStart) + 1;
213
- const fenceEnd = section.indexOf('\n```', contentStart);
214
- if (fenceEnd === -1) break;
215
- const tomlContent = section.slice(contentStart, fenceEnd);
216
- const pathMatch = tomlContent.match(/^# (\.gemini\/commands\/kc\/[\w-]+\.toml)/);
217
- if (pathMatch) {
218
- files.set(pathMatch[1], { content: tomlContent, lang: 'toml' });
219
- }
220
- searchFrom = fenceEnd + 4;
221
- }
222
-
223
- // Primary: ```markdown fence (GEMINI.md)
224
- const primaryFence = extractFence(section, 'markdown');
225
- if (!primaryFence) return null;
226
-
227
- return { primary: primaryFence.content, files };
228
- }
229
-
230
- function parseCodexSection(section) {
231
- const files = new Map();
232
-
233
- // Primary: first ```markdown fence (AGENTS.md)
234
- const primaryFence = extractFence(section, 'markdown');
235
- if (!primaryFence) return null;
236
-
237
- // Skill files: #### .codex/skills/kc/{name}.md headers
238
- const headerRegex = /#### (\.codex\/skills\/kc\/[\w-]+\.md)/g;
239
- const headers = [];
240
- let match;
241
- while ((match = headerRegex.exec(section)) !== null) {
242
- headers.push({ filepath: match[1], index: match.index });
243
- }
244
-
245
- for (let i = 0; i < headers.length; i++) {
246
- const start = headers[i].index;
247
- const end = i + 1 < headers.length ? headers[i + 1].index : section.length;
248
- const subSection = section.slice(start, end);
249
- const fence = extractFence(subSection, 'markdown');
250
- if (fence) {
251
- files.set(headers[i].filepath, { content: fence.content, lang: 'markdown' });
252
- }
253
- }
254
-
255
- return { primary: primaryFence.content, files };
256
- }
257
-
258
- function parseSimpleSection(section) {
259
- const primaryFence = extractFence(section, 'markdown');
260
- if (!primaryFence) return null;
261
- return { primary: primaryFence.content, files: new Map() };
262
- }
263
-
264
- function parseAdapterTemplates() {
265
- const adaptersPath = join(PKG_ROOT, 'knowzcode', 'platform_adapters.md');
266
- if (!existsSync(adaptersPath)) {
267
- log.warn('platform_adapters.md not found — adapter generation will be skipped');
268
- return new Map();
269
- }
270
-
271
- const content = readFileSync(adaptersPath, 'utf8');
272
- const templates = new Map();
273
-
274
- for (const [id, platform] of Object.entries(PLATFORMS)) {
275
- if (!platform.templateHeader) continue;
276
-
277
- const headerIdx = content.indexOf(platform.templateHeader);
278
- if (headerIdx === -1) continue;
279
-
280
- const section = extractSection(content, headerIdx);
281
- let result;
282
- switch (id) {
283
- case 'copilot': result = parseCopilotSection(section); break;
284
- case 'gemini': result = parseGeminiSection(section); break;
285
- case 'codex': result = parseCodexSection(section); break;
286
- default: result = parseSimpleSection(section); break;
287
- }
288
- if (result) templates.set(id, result);
289
- }
290
-
291
- return templates;
292
- }
293
-
294
- // ─── File Copy Helpers ───────────────────────────────────────────────────────
295
-
296
- function ensureDir(dir) {
297
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
298
- }
299
-
300
- function copyDirContents(src, dst) {
301
- ensureDir(dst);
302
- if (!existsSync(src)) return;
303
-
304
- for (const entry of readdirSync(src, { withFileTypes: true })) {
305
- const srcPath = join(src, entry.name);
306
- const dstPath = join(dst, entry.name);
307
- if (entry.isDirectory()) {
308
- copyDirContents(srcPath, dstPath);
309
- } else {
310
- writeFileSync(dstPath, readFileSync(srcPath));
311
- }
312
- }
313
- }
314
-
315
- function listFilesRecursive(dir, base = dir) {
316
- const files = [];
317
- if (!existsSync(dir)) return files;
318
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
319
- const full = join(dir, entry.name);
320
- if (entry.isDirectory()) {
321
- files.push(...listFilesRecursive(full, base));
322
- } else {
323
- files.push(full);
324
- }
325
- }
326
- return files;
327
- }
328
-
329
- // ─── Marketplace Config ──────────────────────────────────────────────────────
330
-
331
- function setMarketplaceConfig(claudeDir) {
332
- ensureDir(claudeDir);
333
- const settingsFile = join(claudeDir, 'settings.json');
334
- let settings = {};
335
-
336
- if (existsSync(settingsFile)) {
337
- try {
338
- settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
339
- } catch {
340
- settings = {};
341
- }
342
- }
343
-
344
- if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
345
- settings.extraKnownMarketplaces.knowzcode = {
346
- source: { source: 'github', repo: 'knowz-io/knowzcode' },
347
- };
348
-
349
- writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
350
- }
351
-
352
- function removeMarketplaceConfig(claudeDir) {
353
- const settingsFile = join(claudeDir, 'settings.json');
354
- if (!existsSync(settingsFile)) return;
355
-
356
- try {
357
- const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
358
- if (settings.extraKnownMarketplaces && settings.extraKnownMarketplaces.knowzcode) {
359
- delete settings.extraKnownMarketplaces.knowzcode;
360
- writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
361
- }
362
- } catch {
363
- // Ignore parse errors
364
- }
365
- }
366
-
367
- // ─── Stale File Cleanup ─────────────────────────────────────────────────────
368
-
369
- function removeStaleFiles(sourceDir, targetDir) {
370
- if (!existsSync(targetDir) || !existsSync(sourceDir)) return;
371
-
372
- const sourceFiles = new Set(
373
- readdirSync(sourceDir)
374
- .filter((f) => f.endsWith('.md'))
375
- );
376
-
377
- for (const entry of readdirSync(targetDir)) {
378
- if (entry.endsWith('.md') && !sourceFiles.has(entry)) {
379
- const stale = join(targetDir, entry);
380
- if (existsSync(stale) && statSync(stale).isFile()) {
381
- log.info(`Removing stale file: ${stale}`);
382
- rmSync(stale, { force: true });
383
- }
384
- }
385
- }
386
- }
387
-
388
- // ─── Tracker & Log Initializers ──────────────────────────────────────────────
389
-
390
- function initTracker(filePath) {
391
- writeFileSync(filePath, `# KnowzCode - Status Map
392
-
393
- **Purpose:** This document tracks the development status of all implementable components (NodeIDs) defined in \`knowzcode_architecture.md\`.
394
-
395
- ---
396
- **Progress: 0%**
397
- ---
398
-
399
- | Status | WorkGroupID | Node ID | Label | Dependencies | Logical Grouping | Spec Link | Classification | Notes / Issues |
400
- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
401
- | | | | | | | | | |
402
-
403
- ---
404
- ### Status Legend:
405
-
406
- * ⚪️ **\`[TODO]\`**: Task is defined and ready to be picked up if dependencies are met.
407
- * 📝 **\`[NEEDS_SPEC]\`**: Node has been identified but requires a detailed specification.
408
- * ◆ **\`[WIP]\`**: Work In Progress. The KnowzCode AI Agent is currently working on this node.
409
- * 🟢 **\`[VERIFIED]\`**: Node has been implemented and verified.
410
- * ❗ **\`[ISSUE]\`**: A significant issue or blocker has been identified.
411
-
412
- ---
413
- *(This table will be populated as you define your architecture and NodeIDs.)*
414
- `);
415
- }
416
-
417
- function initLog(filePath) {
418
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
419
- writeFileSync(filePath, `# KnowzCode - Operational Record
420
-
421
- **Purpose:** Chronological record of significant events, decisions, and verification outcomes.
422
-
423
- ---
424
-
425
- ## Section 1: Operational Log
426
-
427
- ---
428
- **[NEWEST ENTRIES APPEAR HERE - DO NOT REMOVE THIS MARKER]**
429
- ---
430
- **Type:** SystemInitialization
431
- **Timestamp:** ${ts}
432
- **NodeID(s):** Project-Wide
433
- **Logged By:** knowzcode-cli
434
- **Details:**
435
- KnowzCode framework installed via \`npx knowzcode\`.
436
- - Framework files initialized
437
- - Ready for first feature
438
- ---
439
-
440
- ## Section 2: Reference Quality Criteria (ARC-Based Verification)
441
-
442
- ### Core Quality Criteria
443
- 1. **Maintainability:** Ease of modification, clarity of code and design.
444
- 2. **Reliability:** Robustness of error handling, fault tolerance.
445
- 3. **Testability:** Adequacy of unit test coverage, ease of testing.
446
- 4. **Performance:** Responsiveness, efficiency in resource utilization.
447
- 5. **Security:** Resistance to common vulnerabilities.
448
-
449
- ### Structural Criteria
450
- 6. **Readability:** Code clarity, adherence to naming conventions.
451
- 7. **Complexity Management:** Avoidance of overly complex logic.
452
- 8. **Modularity:** Adherence to Single Responsibility Principle.
453
- 9. **Code Duplication (DRY):** Minimization of redundant code.
454
- 10. **Standards Compliance:** Adherence to language best practices.
455
-
456
- *(Refer to these criteria during ARC-Based Verification.)*
457
- `);
458
- }
459
-
460
- // ─── Interactive Prompt ──────────────────────────────────────────────────────
461
-
462
- async function promptPlatforms(detected) {
463
- const rl = createInterface({ input: process.stdin, output: process.stdout });
464
- const ids = Object.keys(PLATFORMS);
465
-
466
- console.log('');
467
- console.log(`${c.bold}Select platforms to generate adapters for:${c.reset}`);
468
- console.log('');
469
- ids.forEach((id, i) => {
470
- const p = PLATFORMS[id];
471
- const tag = detected.includes(id) ? ` ${c.green}(detected)${c.reset}` : '';
472
- console.log(` [${i + 1}] ${p.name}${tag}`);
473
- });
474
- console.log(` [A] All platforms`);
475
- console.log(` [S] Skip adapters (core framework only)`);
476
- console.log('');
477
-
478
- const answer = await rl.question('Select platforms (comma-separated, e.g. 1,2): ');
479
- rl.close();
480
-
481
- const trimmed = answer.trim().toUpperCase();
482
- if (trimmed === 'S' || trimmed === '') return [];
483
- if (trimmed === 'A') return ids;
484
-
485
- const selected = [];
486
- for (const part of trimmed.split(',')) {
487
- const num = parseInt(part.trim(), 10);
488
- if (num >= 1 && num <= ids.length) {
489
- selected.push(ids[num - 1]);
490
- }
491
- }
492
- return [...new Set(selected)];
493
- }
494
-
495
- async function promptConfirm(message) {
496
- const rl = createInterface({ input: process.stdin, output: process.stdout });
497
- const answer = await rl.question(`${message} [y/N]: `);
498
- rl.close();
499
- return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
500
- }
501
-
502
- // ─── Agent Teams Enablement ──────────────────────────────────────────────────
503
-
504
- function enableAgentTeams(claudeDir, isGlobal) {
505
- ensureDir(claudeDir);
506
- const settingsFile = join(claudeDir, isGlobal ? 'settings.json' : 'settings.local.json');
507
-
508
- let settings = {};
509
- if (existsSync(settingsFile)) {
510
- try {
511
- settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
512
- } catch {
513
- settings = {};
514
- }
515
- }
516
-
517
- if (!settings.env) settings.env = {};
518
- settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
519
-
520
- writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
521
- log.ok(`Agent Teams enabled in ${settingsFile}`);
522
- }
523
-
524
- // ─── Commands ────────────────────────────────────────────────────────────────
525
-
526
- // DETECT
527
- function cmdDetect(opts) {
528
- const dir = opts.target;
529
- console.log('');
530
- console.log(`${c.bold}KnowzCode Platform Detection${c.reset}`);
531
- console.log(`${c.dim}Scanning: ${dir}${c.reset}`);
532
- console.log('');
533
-
534
- const detected = detectPlatforms(dir);
535
- const hasKnowzcode = existsSync(join(dir, 'knowzcode'));
536
-
537
- console.log(` KnowzCode framework: ${hasKnowzcode ? `${c.green}installed${c.reset}` : `${c.dim}not found${c.reset}`}`);
538
-
539
- if (hasKnowzcode) {
540
- const versionFile = join(dir, 'knowzcode', '.knowzcode-version');
541
- if (existsSync(versionFile)) {
542
- const ver = readFileSync(versionFile, 'utf8').trim();
543
- console.log(` Installed version: ${c.cyan}${ver}${c.reset}`);
544
- }
545
- }
546
-
547
- console.log('');
548
- console.log(` ${c.bold}Platforms:${c.reset}`);
549
-
550
- for (const [id, platform] of Object.entries(PLATFORMS)) {
551
- const found = detected.includes(id);
552
- const indicator = found ? `${c.green}detected${c.reset}` : `${c.dim}not detected${c.reset}`;
553
- console.log(` ${platform.name.padEnd(18)} ${indicator}`);
554
- }
555
-
556
- console.log('');
557
- if (detected.length === 0) {
558
- console.log(` No platforms detected. Run ${c.cyan}npx knowzcode install${c.reset} to set up.`);
559
- } else {
560
- console.log(` ${detected.length} platform(s) detected.`);
561
- }
562
- console.log('');
563
- }
564
-
565
- // INSTALL
566
- async function cmdInstall(opts) {
567
- const dir = opts.target;
568
- const kcDir = join(dir, 'knowzcode');
569
-
570
- console.log('');
571
- console.log(`${c.bold}KnowzCode Install${c.reset}`);
572
- console.log(`${c.dim}Target: ${dir}${c.reset}`);
573
- console.log('');
574
-
575
- // Check for existing installation
576
- if (existsSync(kcDir) && !opts.force) {
577
- log.warn('KnowzCode already installed at ' + kcDir);
578
- log.warn('Use --force to overwrite.');
579
- process.exit(1);
580
- }
581
-
582
- if (!existsSync(dir)) {
583
- log.err('Target directory does not exist: ' + dir);
584
- process.exit(1);
585
- }
586
-
587
- // 1. Copy knowzcode/ template directory
588
- log.info('Installing core framework...');
589
- const srcKc = join(PKG_ROOT, 'knowzcode');
590
- ensureDir(kcDir);
591
- ensureDir(join(kcDir, 'specs'));
592
- ensureDir(join(kcDir, 'workgroups'));
593
- ensureDir(join(kcDir, 'prompts'));
594
-
595
- // Create workgroups/README.md (workgroups/ is gitignored and excluded from npm)
596
- writeFileSync(join(kcDir, 'workgroups', 'README.md'), '# WorkGroups\n\nSession-specific WorkGroup files are stored here.\nThis directory is gitignored — contents are local to each checkout.\n');
597
-
598
- // Copy .md files (skip tracker and log — generate fresh)
599
- for (const entry of readdirSync(srcKc)) {
600
- const srcPath = join(srcKc, entry);
601
- const stat = statSync(srcPath);
602
- if (stat.isFile() && entry.endsWith('.md') && entry !== 'knowzcode_tracker.md' && entry !== 'knowzcode_log.md') {
603
- writeFileSync(join(kcDir, entry), readFileSync(srcPath));
604
- } else if (stat.isFile() && !entry.endsWith('.md')) {
605
- // Copy non-md files, handling gitignore.template → .gitignore rename
606
- if (entry === 'gitignore.template') {
607
- writeFileSync(join(kcDir, '.gitignore'), readFileSync(srcPath));
608
- } else {
609
- writeFileSync(join(kcDir, entry), readFileSync(srcPath));
610
- }
611
- }
612
- }
613
-
614
- // Copy prompts/
615
- if (existsSync(join(srcKc, 'prompts'))) {
616
- copyDirContents(join(srcKc, 'prompts'), join(kcDir, 'prompts'));
617
- }
618
-
619
- // Copy specs readme
620
- if (existsSync(join(srcKc, 'specs', 'Readme.md'))) {
621
- writeFileSync(join(kcDir, 'specs', 'Readme.md'), readFileSync(join(srcKc, 'specs', 'Readme.md')));
622
- }
623
-
624
- // Copy enterprise/ if exists
625
- if (existsSync(join(srcKc, 'enterprise'))) {
626
- copyDirContents(join(srcKc, 'enterprise'), join(kcDir, 'enterprise'));
627
- }
628
-
629
- // Initialize fresh tracker and log
630
- initTracker(join(kcDir, 'knowzcode_tracker.md'));
631
- initLog(join(kcDir, 'knowzcode_log.md'));
632
-
633
- // Write version marker
634
- writeFileSync(join(kcDir, '.knowzcode-version'), VERSION + '\n');
635
-
636
- log.ok('Core framework installed');
637
-
638
- // 2. Platform detection + selection
639
- const detected = detectPlatforms(dir);
640
- let selectedPlatforms;
641
-
642
- if (opts.platforms.length > 0) {
643
- if (opts.platforms.includes('all')) {
644
- selectedPlatforms = Object.keys(PLATFORMS);
645
- } else {
646
- selectedPlatforms = opts.platforms.filter((p) => p in PLATFORMS);
647
- }
648
- } else if (opts.force) {
649
- // Non-interactive mode with --force: install for detected platforms only
650
- selectedPlatforms = detected;
651
- } else {
652
- selectedPlatforms = await promptPlatforms(detected);
653
- }
654
-
655
- // 3. Generate adapters
656
- const templates = parseAdapterTemplates();
657
- const adapterFiles = [];
658
-
659
- for (const platformId of selectedPlatforms) {
660
- const platform = PLATFORMS[platformId];
661
-
662
- if (platformId === 'claude') {
663
- // Claude Code: copy agents, commands, skills
664
- const claudeDir = opts.global ? join(process.env.HOME || process.env.USERPROFILE || '~', '.claude') : join(dir, '.claude');
665
-
666
- log.info(`Installing Claude Code components to ${claudeDir}/`);
667
-
668
- // Remove stale files before copying on --force
669
- if (opts.force) {
670
- removeStaleFiles(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
671
- removeStaleFiles(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
672
- removeStaleFiles(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
673
- }
674
-
675
- copyDirContents(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
676
- copyDirContents(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
677
- copyDirContents(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
678
-
679
- // Pre-register marketplace in settings.json
680
- setMarketplaceConfig(claudeDir);
681
-
682
- adapterFiles.push(claudeDir + '/commands/', claudeDir + '/agents/', claudeDir + '/skills/');
683
- } else {
684
- // Other platforms: extract template and write adapter + additional files
685
- const templateSet = templates.get(platformId);
686
- if (!templateSet) {
687
- log.warn(`No adapter template found for ${platform.name} — skipping`);
688
- continue;
689
- }
690
-
691
- // Write primary adapter file
692
- const adapterFile = platform.adapterPath(dir);
693
- ensureDir(dirname(adapterFile));
694
- writeFileSync(adapterFile, injectVersion(templateSet.primary));
695
- adapterFiles.push(adapterFile);
696
- log.ok(`${platform.name} adapter: ${adapterFile}`);
697
-
698
- // Write additional files (prompts, TOMLs, skills)
699
- for (const [relativePath, { content }] of templateSet.files) {
700
- let filePath;
701
- if (platformId === 'codex' && opts.global && relativePath.startsWith('.codex/skills/')) {
702
- const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
703
- filePath = join(homeDir, relativePath);
704
- } else {
705
- filePath = join(dir, relativePath);
706
- }
707
- ensureDir(dirname(filePath));
708
- writeFileSync(filePath, injectVersion(content));
709
- adapterFiles.push(filePath);
710
- }
711
- if (templateSet.files.size > 0) {
712
- log.ok(` + ${templateSet.files.size} additional file(s)`);
713
- }
714
- }
715
- }
716
-
717
- // 4. Agent Teams enablement
718
- const agentTeamsClaudeDir = opts.global
719
- ? join(process.env.HOME || process.env.USERPROFILE || '~', '.claude')
720
- : join(dir, '.claude');
721
- let agentTeamsEnabled = false;
722
- if (opts.agentTeams) {
723
- enableAgentTeams(agentTeamsClaudeDir, opts.global);
724
- agentTeamsEnabled = true;
725
- } else if (selectedPlatforms.includes('claude') && !opts.force) {
726
- // Interactive prompt for Claude Code users
727
- console.log('');
728
- console.log(`${c.bold}Agent Teams${c.reset} enables multi-agent coordination where specialized`);
729
- console.log(`teammates handle each workflow phase. ${c.dim}(experimental)${c.reset}`);
730
- const wantTeams = await promptConfirm('Enable Agent Teams? (recommended for Claude Code)');
731
- if (wantTeams) {
732
- enableAgentTeams(agentTeamsClaudeDir, opts.global);
733
- agentTeamsEnabled = true;
734
- }
735
- }
736
-
737
- // 5. Summary
738
- console.log('');
739
- console.log(`${c.green}${c.bold}Installation complete!${c.reset}`);
740
- console.log('');
741
- console.log(' Framework: ' + kcDir + '/');
742
- if (adapterFiles.length > 0) {
743
- console.log(' Adapters:');
744
- for (const f of adapterFiles) {
745
- console.log(' ' + f);
746
- }
747
- }
748
- if (agentTeamsEnabled) {
749
- console.log(' Agent Teams: enabled');
750
- }
751
- console.log('');
752
- console.log(`${c.bold}Next steps:${c.reset}`);
753
- console.log(' 1. Edit knowzcode/knowzcode_project.md set project name, stack, standards');
754
- console.log(' 2. Edit knowzcode/environment_context.md — configure build/test commands');
755
- if (selectedPlatforms.includes('claude')) {
756
- console.log(' 3. Install the KnowzCode plugin (recommended):');
757
- console.log(' /plugin install kc@knowzcode');
758
- console.log(' 4. Start building:');
759
- console.log(' /kc:work "Your first feature"');
760
- console.log('');
761
- console.log(' Note: Commands also work without plugin as /work, /plan, /fix, etc.');
762
- } else {
763
- console.log(' 3. Start building: use knowzcode/prompts/[LOOP_1A]__Propose_Change_Set.md');
764
- }
765
- console.log('');
766
- }
767
-
768
- // UNINSTALL
769
- async function cmdUninstall(opts) {
770
- const dir = opts.target;
771
- const kcDir = join(dir, 'knowzcode');
772
-
773
- console.log('');
774
- console.log(`${c.bold}KnowzCode Uninstall${c.reset}`);
775
- console.log(`${c.dim}Target: ${dir}${c.reset}`);
776
- console.log('');
777
-
778
- // Scan for installed components
779
- const components = [];
780
-
781
- if (existsSync(kcDir)) {
782
- components.push({ label: 'Core framework', path: kcDir });
783
- }
784
-
785
- // Claude Code components
786
- const claudeDir = join(dir, '.claude');
787
- for (const sub of ['commands', 'agents', 'skills']) {
788
- const p = join(claudeDir, sub);
789
- if (existsSync(p)) {
790
- components.push({ label: `Claude Code ${sub}`, path: p });
791
- }
792
- }
793
-
794
- // Platform adapter files
795
- const adapterChecks = {
796
- codex: join(dir, 'AGENTS.md'),
797
- gemini: join(dir, 'GEMINI.md'),
798
- cursor: join(dir, '.cursor', 'rules', 'knowzcode.mdc'),
799
- copilot: join(dir, '.github', 'copilot-instructions.md'),
800
- windsurf: join(dir, '.windsurf', 'rules', 'knowzcode.md'),
801
- };
802
-
803
- for (const [id, path] of Object.entries(adapterChecks)) {
804
- if (existsSync(path)) {
805
- components.push({ label: `${PLATFORMS[id].name} adapter`, path });
806
- }
807
- }
808
-
809
- // Additional platform-specific files/directories
810
- const copilotPromptsDir = join(dir, '.github', 'prompts');
811
- if (existsSync(copilotPromptsDir)) {
812
- for (const f of readdirSync(copilotPromptsDir)) {
813
- if (f.startsWith('kc-') && f.endsWith('.prompt.md')) {
814
- components.push({ label: `Copilot prompt: ${f}`, path: join(copilotPromptsDir, f) });
815
- }
816
- }
817
- }
818
- const vscodeMcp = join(dir, '.vscode', 'mcp.json');
819
- if (existsSync(vscodeMcp)) {
820
- components.push({ label: 'VS Code MCP config', path: vscodeMcp });
821
- }
822
- const geminiCmdDir = join(dir, '.gemini', 'commands', 'kc');
823
- if (existsSync(geminiCmdDir)) {
824
- components.push({ label: 'Gemini commands (kc/)', path: geminiCmdDir });
825
- }
826
- const codexSkillDir = join(dir, '.codex', 'skills', 'kc');
827
- if (existsSync(codexSkillDir)) {
828
- components.push({ label: 'Codex skills (kc/)', path: codexSkillDir });
829
- }
830
- const globalCodexSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.codex', 'skills', 'kc');
831
- if (existsSync(globalCodexSkillDir)) {
832
- components.push({ label: 'Codex skills — global (~/.codex/skills/kc/)', path: globalCodexSkillDir });
833
- }
834
-
835
- if (components.length === 0) {
836
- log.info('No KnowzCode installation found.');
837
- return;
838
- }
839
-
840
- console.log(' Components found:');
841
- for (const comp of components) {
842
- console.log(` ${comp.label}: ${comp.path}`);
843
- }
844
- console.log('');
845
-
846
- // Ask about preserving user data
847
- let preserveUserData = false;
848
- if (existsSync(kcDir) && !opts.force) {
849
- preserveUserData = await promptConfirm('Preserve user data (specs/, architecture, tracker, log)?');
850
- }
851
-
852
- if (!opts.force) {
853
- const confirmed = await promptConfirm('Remove all listed components?');
854
- if (!confirmed) {
855
- log.info('Uninstall cancelled.');
856
- return;
857
- }
858
- }
859
-
860
- const removed = [];
861
-
862
- // Remove components
863
- for (const comp of components) {
864
- if (comp.path === kcDir && preserveUserData) {
865
- // Selective removal — keep user data
866
- const preserve = ['specs', 'knowzcode_architecture.md', 'knowzcode_tracker.md', 'knowzcode_log.md', 'knowzcode_project.md'];
867
-
868
- for (const entry of readdirSync(kcDir)) {
869
- if (preserve.includes(entry)) continue;
870
- const entryPath = join(kcDir, entry);
871
- rmSync(entryPath, { recursive: true, force: true });
872
- }
873
- removed.push(comp.label + ' (user data preserved)');
874
- } else {
875
- rmSync(comp.path, { recursive: true, force: true });
876
- removed.push(comp.label);
877
- }
878
- }
879
-
880
- // Clean up marketplace config from settings.json
881
- removeMarketplaceConfig(claudeDir);
882
-
883
- console.log('');
884
- log.ok('Uninstall complete');
885
- console.log(' Removed:');
886
- for (const r of removed) {
887
- console.log(` ${r}`);
888
- }
889
- console.log('');
890
- }
891
-
892
- // UPGRADE
893
- async function cmdUpgrade(opts) {
894
- const dir = opts.target;
895
- const kcDir = join(dir, 'knowzcode');
896
-
897
- console.log('');
898
- console.log(`${c.bold}KnowzCode Upgrade${c.reset}`);
899
- console.log(`${c.dim}Target: ${dir}${c.reset}`);
900
- console.log('');
901
-
902
- if (!existsSync(kcDir)) {
903
- log.err('No KnowzCode installation found. Run `npx knowzcode install` first.');
904
- process.exit(1);
905
- }
906
-
907
- // Read current version
908
- const versionFile = join(kcDir, '.knowzcode-version');
909
- const currentVersion = existsSync(versionFile) ? readFileSync(versionFile, 'utf8').trim() : 'unknown';
910
-
911
- if (currentVersion === VERSION && !opts.force) {
912
- log.info(`Already at version ${VERSION}. Use --force to reinstall.`);
913
- return;
914
- }
915
-
916
- log.info(`Upgrading: ${currentVersion} ${VERSION}`);
917
-
918
- // Files to preserve (never overwrite)
919
- const preserveFiles = new Set([
920
- 'knowzcode_tracker.md',
921
- 'knowzcode_log.md',
922
- 'knowzcode_architecture.md',
923
- 'knowzcode_project.md',
924
- 'environment_context.md',
925
- 'user_preferences.md',
926
- ]);
927
- const preserveDirs = new Set(['specs', 'workgroups']);
928
-
929
- // Files to replace (always update)
930
- const srcKc = join(PKG_ROOT, 'knowzcode');
931
-
932
- // Update .md files
933
- for (const entry of readdirSync(srcKc)) {
934
- const srcPath = join(srcKc, entry);
935
- const dstPath = join(kcDir, entry);
936
- const stat = statSync(srcPath);
937
-
938
- if (stat.isFile()) {
939
- if (preserveFiles.has(entry)) {
940
- if (opts.verbose) log.info(`Preserved: ${entry}`);
941
- continue;
942
- }
943
- // Handle gitignore.template .gitignore rename
944
- if (entry === 'gitignore.template') {
945
- writeFileSync(join(kcDir, '.gitignore'), readFileSync(srcPath));
946
- if (opts.verbose) log.info('Updated: .gitignore (from gitignore.template)');
947
- } else {
948
- writeFileSync(dstPath, readFileSync(srcPath));
949
- if (opts.verbose) log.info(`Updated: ${entry}`);
950
- }
951
- }
952
- }
953
-
954
- // Update prompts/ (always replace)
955
- if (existsSync(join(srcKc, 'prompts'))) {
956
- const promptsDst = join(kcDir, 'prompts');
957
- // Remove old prompts, copy new ones
958
- if (existsSync(promptsDst)) rmSync(promptsDst, { recursive: true, force: true });
959
- copyDirContents(join(srcKc, 'prompts'), promptsDst);
960
- if (opts.verbose) log.info('Updated: prompts/');
961
- }
962
-
963
- // Update enterprise/ (always replace)
964
- if (existsSync(join(srcKc, 'enterprise'))) {
965
- const entDst = join(kcDir, 'enterprise');
966
- if (existsSync(entDst)) rmSync(entDst, { recursive: true, force: true });
967
- copyDirContents(join(srcKc, 'enterprise'), entDst);
968
- if (opts.verbose) log.info('Updated: enterprise/');
969
- }
970
-
971
- // Update Claude Code components if present
972
- const claudeDir = join(dir, '.claude');
973
- if (existsSync(join(claudeDir, 'commands')) || existsSync(join(claudeDir, 'agents'))) {
974
- log.info('Updating Claude Code components...');
975
- // Remove stale files before copying
976
- removeStaleFiles(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
977
- removeStaleFiles(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
978
- removeStaleFiles(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
979
- copyDirContents(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
980
- copyDirContents(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
981
- copyDirContents(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
982
- // Ensure marketplace config is up to date
983
- setMarketplaceConfig(claudeDir);
984
- }
985
-
986
- // Regenerate adapters for detected platforms
987
- const detected = detectPlatforms(dir);
988
- const templates = parseAdapterTemplates();
989
- const regenerated = [];
990
-
991
- for (const platformId of detected) {
992
- if (platformId === 'claude') continue; // Already handled above
993
- const platform = PLATFORMS[platformId];
994
- if (!platform.adapterPath) continue;
995
-
996
- const adapterFile = platform.adapterPath(dir);
997
- if (!existsSync(adapterFile)) continue; // Only update existing adapters
998
-
999
- const templateSet = templates.get(platformId);
1000
- if (!templateSet) continue;
1001
-
1002
- // Update primary adapter file
1003
- writeFileSync(adapterFile, injectVersion(templateSet.primary));
1004
- regenerated.push(platform.name);
1005
-
1006
- // Regenerate additional files
1007
- const currentPaths = new Set();
1008
- for (const [relativePath, { content }] of templateSet.files) {
1009
- const filePath = join(dir, relativePath);
1010
- ensureDir(dirname(filePath));
1011
- writeFileSync(filePath, injectVersion(content));
1012
- currentPaths.add(relativePath);
1013
- }
1014
-
1015
- // Stale file cleanup for platform-owned directories
1016
- if (platformId === 'copilot') {
1017
- const promptsDir = join(dir, '.github', 'prompts');
1018
- if (existsSync(promptsDir)) {
1019
- for (const f of readdirSync(promptsDir)) {
1020
- if (f.startsWith('kc-') && f.endsWith('.prompt.md') && !currentPaths.has(`.github/prompts/${f}`)) {
1021
- log.info(`Removing stale prompt: ${f}`);
1022
- rmSync(join(promptsDir, f), { force: true });
1023
- }
1024
- }
1025
- }
1026
- } else if (platformId === 'gemini') {
1027
- const tomlDir = join(dir, '.gemini', 'commands', 'kc');
1028
- if (existsSync(tomlDir)) {
1029
- for (const f of readdirSync(tomlDir)) {
1030
- if (f.endsWith('.toml') && !currentPaths.has(`.gemini/commands/kc/${f}`)) {
1031
- log.info(`Removing stale command: ${f}`);
1032
- rmSync(join(tomlDir, f), { force: true });
1033
- }
1034
- }
1035
- }
1036
- } else if (platformId === 'codex') {
1037
- const skillDir = join(dir, '.codex', 'skills', 'kc');
1038
- if (existsSync(skillDir)) {
1039
- for (const f of readdirSync(skillDir)) {
1040
- if (f.endsWith('.md') && !currentPaths.has(`.codex/skills/kc/${f}`)) {
1041
- log.info(`Removing stale skill: ${f}`);
1042
- rmSync(join(skillDir, f), { force: true });
1043
- }
1044
- }
1045
- }
1046
- }
1047
- }
1048
-
1049
- // Check for global codex skills
1050
- const globalCodexSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.codex', 'skills', 'kc');
1051
- if (existsSync(globalCodexSkillDir)) {
1052
- const codexTemplateSet = templates.get('codex');
1053
- if (codexTemplateSet) {
1054
- const currentPaths = new Set([...codexTemplateSet.files.keys()]);
1055
- // Stale cleanup
1056
- for (const f of readdirSync(globalCodexSkillDir)) {
1057
- if (f.endsWith('.md') && !currentPaths.has(`.codex/skills/kc/${f}`)) {
1058
- log.info(`Removing stale global skill: ${f}`);
1059
- rmSync(join(globalCodexSkillDir, f), { force: true });
1060
- }
1061
- }
1062
- // Regenerate global skills
1063
- for (const [relativePath, { content }] of codexTemplateSet.files) {
1064
- if (relativePath.startsWith('.codex/skills/')) {
1065
- const filePath = join(process.env.HOME || process.env.USERPROFILE || '~', relativePath);
1066
- ensureDir(dirname(filePath));
1067
- writeFileSync(filePath, injectVersion(content));
1068
- }
1069
- }
1070
- log.info('Updated global Codex skills');
1071
- }
1072
- }
1073
-
1074
- // Write new version
1075
- writeFileSync(versionFile, VERSION + '\n');
1076
-
1077
- console.log('');
1078
- log.ok(`Upgraded to ${VERSION}`);
1079
- console.log('');
1080
- console.log(` ${c.bold}Preserved:${c.reset} specs/, tracker, log, architecture, project config`);
1081
- console.log(` ${c.bold}Updated:${c.reset} loop, prompts, adapters, enterprise templates`);
1082
- if (regenerated.length > 0) {
1083
- console.log(` ${c.bold}Adapters:${c.reset} ${regenerated.join(', ')}`);
1084
- }
1085
- console.log('');
1086
- }
1087
-
1088
- // HELP
1089
- function cmdHelp() {
1090
- console.log(`
1091
- ${c.bold}KnowzCode CLI${c.reset} v${VERSION}
1092
- Platform-agnostic AI development methodology
1093
-
1094
- ${c.bold}Usage:${c.reset}
1095
- npx knowzcode Interactive mode
1096
- npx knowzcode install [options] Install to current/target directory
1097
- npx knowzcode uninstall [options] Remove KnowzCode
1098
- npx knowzcode upgrade [options] Upgrade preserving user data
1099
- npx knowzcode detect Show detected platforms (dry run)
1100
-
1101
- ${c.bold}Options:${c.reset}
1102
- --target <path> Target directory (default: current directory)
1103
- --platforms <list> Comma-separated: claude,codex,gemini,cursor,copilot,windsurf,all
1104
- --force Skip confirmation prompts
1105
- --global Install Claude Code to ~/.claude/, Codex skills to ~/.codex/
1106
- --agent-teams Enable Agent Teams in .claude/settings.local.json
1107
- --verbose Show detailed output
1108
- -h, --help Show this help
1109
- -v, --version Show version
1110
-
1111
- ${c.bold}Examples:${c.reset}
1112
- npx knowzcode install --platforms claude,cursor
1113
- npx knowzcode install --platforms all --force
1114
- npx knowzcode upgrade --target ./my-project
1115
- npx knowzcode uninstall --force
1116
- npx knowzcode detect
1117
- `);
1118
- }
1119
-
1120
- // INTERACTIVE
1121
- async function cmdInteractive(opts) {
1122
- console.log('');
1123
- console.log(`${c.bold} ╔═══════════════════════════════════════╗${c.reset}`);
1124
- console.log(`${c.bold} ║ KnowzCode v${VERSION.padEnd(14)} ║${c.reset}`);
1125
- console.log(`${c.bold} ║ AI Development Methodology Installer ║${c.reset}`);
1126
- console.log(`${c.bold} ╚═══════════════════════════════════════╝${c.reset}`);
1127
- console.log('');
1128
-
1129
- const dir = opts.target;
1130
- const kcDir = join(dir, 'knowzcode');
1131
- const detected = detectPlatforms(dir);
1132
-
1133
- if (detected.length > 0) {
1134
- log.info('Detected platforms: ' + detected.map((d) => PLATFORMS[d].name).join(', '));
1135
- }
1136
-
1137
- if (existsSync(kcDir)) {
1138
- const versionFile = join(kcDir, '.knowzcode-version');
1139
- const currentVersion = existsSync(versionFile) ? readFileSync(versionFile, 'utf8').trim() : 'unknown';
1140
-
1141
- if (currentVersion !== VERSION) {
1142
- log.info(`Installed version: ${currentVersion}, available: ${VERSION}`);
1143
- const doUpgrade = await promptConfirm('Upgrade to latest version?');
1144
- if (doUpgrade) {
1145
- return cmdUpgrade(opts);
1146
- }
1147
- } else {
1148
- log.info(`KnowzCode ${currentVersion} already installed.`);
1149
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1150
- console.log('');
1151
- console.log(' [1] Reinstall (overwrite)');
1152
- console.log(' [2] Uninstall');
1153
- console.log(' [3] Detect platforms');
1154
- console.log(' [4] Exit');
1155
- console.log('');
1156
- const answer = await rl.question('Select action: ');
1157
- rl.close();
1158
-
1159
- const choice = answer.trim();
1160
- if (choice === '1') return cmdInstall({ ...opts, force: true });
1161
- if (choice === '2') return cmdUninstall(opts);
1162
- if (choice === '3') return cmdDetect(opts);
1163
- return;
1164
- }
1165
- } else {
1166
- log.info('No existing installation found. Starting install...');
1167
- console.log('');
1168
- return cmdInstall(opts);
1169
- }
1170
- }
1171
-
1172
- // ─── Main ────────────────────────────────────────────────────────────────────
1173
-
1174
- async function main() {
1175
- const opts = parseArgs(process.argv);
1176
-
1177
- switch (opts.command) {
1178
- case 'install':
1179
- return cmdInstall(opts);
1180
- case 'uninstall':
1181
- return cmdUninstall(opts);
1182
- case 'upgrade':
1183
- return cmdUpgrade(opts);
1184
- case 'detect':
1185
- return cmdDetect(opts);
1186
- case 'help':
1187
- return cmdHelp();
1188
- case 'version':
1189
- console.log(VERSION);
1190
- return;
1191
- default:
1192
- return cmdInteractive(opts);
1193
- }
1194
- }
1195
-
1196
- main().catch((err) => {
1197
- log.err(err.message);
1198
- process.exit(1);
1199
- });
1
+ #!/usr/bin/env node
2
+
3
+ // KnowzCode CLI — Zero-dependency Node.js installer
4
+ // Usage: npx knowzcode [install|uninstall|upgrade|detect] [options]
5
+
6
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'fs';
7
+ import { join, resolve, dirname, basename } from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { createInterface } from 'readline/promises';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ const PKG_ROOT = resolve(__dirname, '..');
14
+ const VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version;
15
+
16
+ // ─── Colors ──────────────────────────────────────────────────────────────────
17
+
18
+ const c = {
19
+ reset: '\x1b[0m',
20
+ bold: '\x1b[1m',
21
+ dim: '\x1b[2m',
22
+ red: '\x1b[31m',
23
+ green: '\x1b[32m',
24
+ yellow: '\x1b[33m',
25
+ blue: '\x1b[34m',
26
+ cyan: '\x1b[36m',
27
+ };
28
+
29
+ const log = {
30
+ info: (msg) => console.log(`${c.blue}[INFO]${c.reset} ${msg}`),
31
+ ok: (msg) => console.log(`${c.green}[OK]${c.reset} ${msg}`),
32
+ warn: (msg) => console.log(`${c.yellow}[WARN]${c.reset} ${msg}`),
33
+ err: (msg) => console.error(`${c.red}[ERROR]${c.reset} ${msg}`),
34
+ };
35
+
36
+ // ─── Platform Definitions ────────────────────────────────────────────────────
37
+
38
+ const PLATFORMS = {
39
+ claude: {
40
+ name: 'Claude Code',
41
+ detect: (dir) => existsSync(join(dir, '.claude')) || existsSync(join(dir, '.claude-plugin')),
42
+ adapterPath: null, // Claude uses .claude/ dir structure, not a single adapter file
43
+ },
44
+ codex: {
45
+ name: 'OpenAI Codex',
46
+ detect: (dir) => existsSync(join(dir, 'AGENTS.md')) || existsSync(join(dir, 'AGENTS.override.md')) || existsSync(join(dir, '.codex')) || existsSync(join(dir, '.agents')),
47
+ adapterPath: (dir) => join(dir, 'AGENTS.md'),
48
+ templateHeader: '## OpenAI Codex (AGENTS.md)',
49
+ },
50
+ gemini: {
51
+ name: 'Gemini CLI',
52
+ detect: (dir) => existsSync(join(dir, 'GEMINI.md')) || existsSync(join(dir, '.gemini')),
53
+ adapterPath: (dir) => join(dir, 'GEMINI.md'),
54
+ templateHeader: '## Google Gemini CLI (GEMINI.md)',
55
+ },
56
+ cursor: {
57
+ name: 'Cursor',
58
+ detect: (dir) => existsSync(join(dir, '.cursor', 'rules')) || existsSync(join(dir, '.cursorrules')),
59
+ adapterPath: (dir) => join(dir, '.cursor', 'rules', 'knowzcode.mdc'),
60
+ templateHeader: '## Cursor (`.cursor/rules/knowzcode.mdc`)',
61
+ },
62
+ copilot: {
63
+ name: 'GitHub Copilot',
64
+ detect: (dir) => existsSync(join(dir, '.github', 'copilot-instructions.md')) || existsSync(join(dir, '.github')),
65
+ adapterPath: (dir) => join(dir, '.github', 'copilot-instructions.md'),
66
+ templateHeader: '## GitHub Copilot',
67
+ },
68
+ windsurf: {
69
+ name: 'Windsurf',
70
+ detect: (dir) => existsSync(join(dir, '.windsurf', 'rules')) || existsSync(join(dir, '.windsurfrules')),
71
+ adapterPath: (dir) => join(dir, '.windsurf', 'rules', 'knowzcode.md'),
72
+ templateHeader: '## Windsurf (`.windsurf/rules/knowzcode.md`)',
73
+ },
74
+ };
75
+
76
+ // ─── CLI Argument Parser ─────────────────────────────────────────────────────
77
+
78
+ function parseArgs(argv) {
79
+ const args = argv.slice(2);
80
+ const opts = {
81
+ command: null,
82
+ target: process.cwd(),
83
+ platforms: [],
84
+ force: false,
85
+ global: false,
86
+ verbose: false,
87
+ agentTeams: false,
88
+ };
89
+
90
+ let i = 0;
91
+ while (i < args.length) {
92
+ const arg = args[i];
93
+ if (arg === '--target' && i + 1 < args.length) {
94
+ opts.target = resolve(args[++i]);
95
+ } else if (arg === '--platforms' && i + 1 < args.length) {
96
+ opts.platforms = args[++i].split(',').map((p) => p.trim().toLowerCase());
97
+ } else if (arg === '--force') {
98
+ opts.force = true;
99
+ } else if (arg === '--global') {
100
+ opts.global = true;
101
+ } else if (arg === '--agent-teams') {
102
+ opts.agentTeams = true;
103
+ } else if (arg === '--verbose') {
104
+ opts.verbose = true;
105
+ } else if (arg === '--help' || arg === '-h') {
106
+ opts.command = 'help';
107
+ } else if (arg === '--version' || arg === '-v') {
108
+ opts.command = 'version';
109
+ } else if (!arg.startsWith('-') && !opts.command) {
110
+ opts.command = arg.toLowerCase();
111
+ }
112
+ i++;
113
+ }
114
+
115
+ return opts;
116
+ }
117
+
118
+ // ─── Platform Detection ──────────────────────────────────────────────────────
119
+
120
+ function detectPlatforms(dir) {
121
+ const detected = [];
122
+ for (const [id, platform] of Object.entries(PLATFORMS)) {
123
+ if (platform.detect(dir)) {
124
+ detected.push(id);
125
+ }
126
+ }
127
+ return detected;
128
+ }
129
+
130
+ // ─── Adapter Template Parser ─────────────────────────────────────────────────
131
+ // Returns Map<platformId, { primary: string, files: Map<relativePath, { content, lang }> }>
132
+
133
+ function injectVersion(content) {
134
+ return content.replace(/vX\.Y\.Z/g, `v${VERSION}`);
135
+ }
136
+
137
+ function extractSection(content, headerIdx) {
138
+ const afterHeader = content.slice(headerIdx);
139
+ const nextSection = afterHeader.search(/\r?\n---\r?\n\r?\n## /);
140
+ return nextSection !== -1 ? afterHeader.slice(0, nextSection) : afterHeader;
141
+ }
142
+
143
+ function extractFence(text, lang, startFrom = 0) {
144
+ const marker = '```' + lang;
145
+ const fenceStart = text.indexOf(marker, startFrom);
146
+ if (fenceStart === -1) return null;
147
+ const contentStart = text.indexOf('\n', fenceStart) + 1;
148
+ // Track nested fences to find the matching closing fence
149
+ let depth = 0;
150
+ let pos = contentStart;
151
+ while (pos < text.length) {
152
+ const nextFence = text.indexOf('\n```', pos);
153
+ if (nextFence === -1) return null;
154
+ const afterBackticks = nextFence + 4;
155
+ const charAfter = afterBackticks < text.length ? text[afterBackticks] : undefined;
156
+ if (charAfter && /\w/.test(charAfter)) {
157
+ // Opening fence (```bash, ```json, etc.)
158
+ depth++;
159
+ } else {
160
+ // Closing fence (``` followed by whitespace/newline/EOF)
161
+ if (depth === 0) {
162
+ return { content: text.slice(contentStart, nextFence), endIdx: afterBackticks };
163
+ }
164
+ depth--;
165
+ }
166
+ pos = afterBackticks;
167
+ }
168
+ return null;
169
+ }
170
+
171
+ function parseCopilotSection(section) {
172
+ const files = new Map();
173
+
174
+ // Section A: copilot-instructions.md (first ```markdown before ### B.)
175
+ const sectionBIdx = section.indexOf('### B.');
176
+ const sectionA = sectionBIdx !== -1 ? section.slice(0, sectionBIdx) : section;
177
+ const primaryFence = extractFence(sectionA, 'markdown');
178
+ if (!primaryFence) return null;
179
+
180
+ // Section B: prompt files (#### kc-*.prompt.md headers)
181
+ const headerRegex = /#### (kc-[\w-]+\.prompt\.md)/g;
182
+ const headers = [];
183
+ let match;
184
+ while ((match = headerRegex.exec(section)) !== null) {
185
+ headers.push({ filename: match[1], index: match.index });
186
+ }
187
+
188
+ const sectionCIdx = section.indexOf('### C.');
189
+ for (let i = 0; i < headers.length; i++) {
190
+ const start = headers[i].index;
191
+ const end = i + 1 < headers.length
192
+ ? headers[i + 1].index
193
+ : (sectionCIdx !== -1 && sectionCIdx > start ? sectionCIdx : section.length);
194
+ const subSection = section.slice(start, end);
195
+
196
+ const fenceOpen = subSection.indexOf('```markdown');
197
+ if (fenceOpen === -1) continue;
198
+ const contentStart = subSection.indexOf('\n', fenceOpen) + 1;
199
+ // Use lastIndexOf to handle prompt files that contain inner code fences
200
+ const lastFenceClose = subSection.lastIndexOf('\n```');
201
+ if (lastFenceClose <= contentStart) continue;
202
+
203
+ files.set(`.github/prompts/${headers[i].filename}`, {
204
+ content: subSection.slice(contentStart, lastFenceClose),
205
+ lang: 'markdown',
206
+ });
207
+ }
208
+
209
+ // Section C: .vscode/mcp.json
210
+ if (sectionCIdx !== -1) {
211
+ const sectionDIdx = section.indexOf('### D.', sectionCIdx);
212
+ const sectionC = section.slice(sectionCIdx, sectionDIdx !== -1 ? sectionDIdx : section.length);
213
+ const jsonFence = extractFence(sectionC, 'json');
214
+ if (jsonFence) {
215
+ files.set('.vscode/mcp.json', { content: jsonFence.content, lang: 'json' });
216
+ }
217
+ }
218
+
219
+ return { primary: primaryFence.content, files };
220
+ }
221
+
222
+ function parseGeminiSection(section) {
223
+ const files = new Map();
224
+
225
+ // Extract TOML blocks: ```toml fences with # .gemini/commands/kc/{name}.toml comment
226
+ let searchFrom = 0;
227
+ while (true) {
228
+ const fenceStart = section.indexOf('```toml', searchFrom);
229
+ if (fenceStart === -1) break;
230
+ const contentStart = section.indexOf('\n', fenceStart) + 1;
231
+ const fenceEnd = section.indexOf('\n```', contentStart);
232
+ if (fenceEnd === -1) break;
233
+ const tomlContent = section.slice(contentStart, fenceEnd);
234
+ const pathMatch = tomlContent.match(/^# (\.gemini\/commands\/kc\/[\w-]+\.toml)/);
235
+ if (pathMatch) {
236
+ files.set(pathMatch[1], { content: tomlContent, lang: 'toml' });
237
+ }
238
+ searchFrom = fenceEnd + 4;
239
+ }
240
+
241
+ // Skill files: #### .gemini/skills/kc-{name}/SKILL.md headers
242
+ const skillRegex = /#### (\.gemini\/skills\/kc-[\w-]+\/SKILL\.md)/g;
243
+ const skillHeaders = [];
244
+ let skillMatch;
245
+ while ((skillMatch = skillRegex.exec(section)) !== null) {
246
+ skillHeaders.push({ filepath: skillMatch[1], index: skillMatch.index });
247
+ }
248
+ // Subagent files: #### .gemini/agents/kc-{name}.md headers
249
+ const agentRegex = /#### (\.gemini\/agents\/kc-[\w-]+\.md)/g;
250
+ const agentHeaders = [];
251
+ let agentMatch;
252
+ while ((agentMatch = agentRegex.exec(section)) !== null) {
253
+ agentHeaders.push({ filepath: agentMatch[1], index: agentMatch.index });
254
+ }
255
+ // Combine all subsection headers for boundary detection
256
+ const allSubHeaders = [...skillHeaders, ...agentHeaders].sort((a, b) => a.index - b.index);
257
+
258
+ for (let i = 0; i < allSubHeaders.length; i++) {
259
+ const start = allSubHeaders[i].index;
260
+ const end = i + 1 < allSubHeaders.length ? allSubHeaders[i + 1].index : section.length;
261
+ const subSection = section.slice(start, end);
262
+ const fence = extractFence(subSection, 'markdown');
263
+ if (fence) {
264
+ files.set(allSubHeaders[i].filepath, { content: fence.content, lang: 'markdown' });
265
+ }
266
+ }
267
+
268
+ // Primary: ```markdown fence (GEMINI.md) — extract from content BEFORE first skill/subagent header
269
+ const firstSubHeader = allSubHeaders.length > 0 ? allSubHeaders[0].index : section.length;
270
+ const primarySection = section.slice(0, firstSubHeader);
271
+ const primaryFence = extractFence(primarySection, 'markdown');
272
+ if (!primaryFence) return null;
273
+
274
+ return { primary: primaryFence.content, files };
275
+ }
276
+
277
+ function parseCodexSection(section) {
278
+ const files = new Map();
279
+
280
+ // Primary: first ```markdown fence (AGENTS.md)
281
+ const primaryFence = extractFence(section, 'markdown');
282
+ if (!primaryFence) return null;
283
+
284
+ // Skill files: #### .agents/skills/kc-{name}/SKILL.md headers
285
+ const headerRegex = /#### (\.agents\/skills\/kc-[\w-]+\/SKILL\.md)/g;
286
+ const headers = [];
287
+ let match;
288
+ while ((match = headerRegex.exec(section)) !== null) {
289
+ headers.push({ filepath: match[1], index: match.index });
290
+ }
291
+
292
+ for (let i = 0; i < headers.length; i++) {
293
+ const start = headers[i].index;
294
+ const end = i + 1 < headers.length ? headers[i + 1].index : section.length;
295
+ const subSection = section.slice(start, end);
296
+ const fence = extractFence(subSection, 'markdown');
297
+ if (fence) {
298
+ files.set(headers[i].filepath, { content: fence.content, lang: 'markdown' });
299
+ }
300
+ }
301
+
302
+ return { primary: primaryFence.content, files };
303
+ }
304
+
305
+ function parseSimpleSection(section) {
306
+ const primaryFence = extractFence(section, 'markdown');
307
+ if (!primaryFence) return null;
308
+ return { primary: primaryFence.content, files: new Map() };
309
+ }
310
+
311
+ function parseAdapterTemplates() {
312
+ const adaptersPath = join(PKG_ROOT, 'knowzcode', 'platform_adapters.md');
313
+ if (!existsSync(adaptersPath)) {
314
+ log.warn('platform_adapters.md not found — adapter generation will be skipped');
315
+ return new Map();
316
+ }
317
+
318
+ const content = readFileSync(adaptersPath, 'utf8');
319
+ const templates = new Map();
320
+
321
+ for (const [id, platform] of Object.entries(PLATFORMS)) {
322
+ if (!platform.templateHeader) continue;
323
+
324
+ const headerIdx = content.indexOf(platform.templateHeader);
325
+ if (headerIdx === -1) continue;
326
+
327
+ const section = extractSection(content, headerIdx);
328
+ let result;
329
+ switch (id) {
330
+ case 'copilot': result = parseCopilotSection(section); break;
331
+ case 'gemini': result = parseGeminiSection(section); break;
332
+ case 'codex': result = parseCodexSection(section); break;
333
+ default: result = parseSimpleSection(section); break;
334
+ }
335
+ if (result) templates.set(id, result);
336
+ }
337
+
338
+ return templates;
339
+ }
340
+
341
+ // ─── File Copy Helpers ───────────────────────────────────────────────────────
342
+
343
+ function ensureDir(dir) {
344
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
345
+ }
346
+
347
+ function copyDirContents(src, dst) {
348
+ ensureDir(dst);
349
+ if (!existsSync(src)) return;
350
+
351
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
352
+ const srcPath = join(src, entry.name);
353
+ const dstPath = join(dst, entry.name);
354
+ if (entry.isDirectory()) {
355
+ copyDirContents(srcPath, dstPath);
356
+ } else {
357
+ writeFileSync(dstPath, readFileSync(srcPath));
358
+ }
359
+ }
360
+ }
361
+
362
+ function listFilesRecursive(dir, base = dir) {
363
+ const files = [];
364
+ if (!existsSync(dir)) return files;
365
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
366
+ const full = join(dir, entry.name);
367
+ if (entry.isDirectory()) {
368
+ files.push(...listFilesRecursive(full, base));
369
+ } else {
370
+ files.push(full);
371
+ }
372
+ }
373
+ return files;
374
+ }
375
+
376
+ // ─── Marketplace Config ──────────────────────────────────────────────────────
377
+
378
+ function setMarketplaceConfig(claudeDir) {
379
+ ensureDir(claudeDir);
380
+ const settingsFile = join(claudeDir, 'settings.json');
381
+ let settings = {};
382
+
383
+ if (existsSync(settingsFile)) {
384
+ try {
385
+ settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
386
+ } catch {
387
+ settings = {};
388
+ }
389
+ }
390
+
391
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
392
+ settings.extraKnownMarketplaces.knowzcode = {
393
+ source: { source: 'github', repo: 'knowz-io/knowzcode' },
394
+ };
395
+
396
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
397
+ }
398
+
399
+ function removeMarketplaceConfig(claudeDir) {
400
+ const settingsFile = join(claudeDir, 'settings.json');
401
+ if (!existsSync(settingsFile)) return;
402
+
403
+ try {
404
+ const settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
405
+ if (settings.extraKnownMarketplaces && settings.extraKnownMarketplaces.knowzcode) {
406
+ delete settings.extraKnownMarketplaces.knowzcode;
407
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
408
+ }
409
+ } catch {
410
+ // Ignore parse errors
411
+ }
412
+ }
413
+
414
+ // ─── Gemini MCP Config Helpers ────────────────────────────────────────────────
415
+
416
+ function writeGeminiMcpConfig(settingsPath, apiKey, projectPath, endpoint = 'https://mcp.knowz.io/mcp') {
417
+ ensureDir(dirname(settingsPath));
418
+ let settings = {};
419
+ if (existsSync(settingsPath)) {
420
+ try {
421
+ settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
422
+ } catch {
423
+ settings = {};
424
+ }
425
+ }
426
+ if (!settings.mcpServers) settings.mcpServers = {};
427
+ settings.mcpServers.knowz = {
428
+ url: endpoint,
429
+ headers: {
430
+ 'Authorization': `Bearer ${apiKey}`,
431
+ 'X-Project-Path': projectPath,
432
+ },
433
+ };
434
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
435
+ }
436
+
437
+ function removeGeminiMcpConfig(settingsPath) {
438
+ if (!existsSync(settingsPath)) return false;
439
+ try {
440
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
441
+ if (settings.mcpServers && settings.mcpServers.knowz) {
442
+ delete settings.mcpServers.knowz;
443
+ if (Object.keys(settings.mcpServers).length === 0) {
444
+ delete settings.mcpServers;
445
+ }
446
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
447
+ return true;
448
+ }
449
+ } catch {
450
+ // Ignore parse errors
451
+ }
452
+ return false;
453
+ }
454
+
455
+ function hasGeminiMcpConfig(settingsPath) {
456
+ if (!existsSync(settingsPath)) return false;
457
+ try {
458
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
459
+ return !!(settings.mcpServers && settings.mcpServers.knowz);
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
464
+
465
+ // ─── Stale File Cleanup ─────────────────────────────────────────────────────
466
+
467
+ function removeStaleFiles(sourceDir, targetDir) {
468
+ if (!existsSync(targetDir) || !existsSync(sourceDir)) return;
469
+
470
+ const sourceFiles = new Set(
471
+ readdirSync(sourceDir)
472
+ .filter((f) => f.endsWith('.md'))
473
+ );
474
+
475
+ for (const entry of readdirSync(targetDir)) {
476
+ if (entry.endsWith('.md') && !sourceFiles.has(entry)) {
477
+ const stale = join(targetDir, entry);
478
+ if (existsSync(stale) && statSync(stale).isFile()) {
479
+ log.info(`Removing stale file: ${stale}`);
480
+ rmSync(stale, { force: true });
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ // ─── Tracker & Log Initializers ──────────────────────────────────────────────
487
+
488
+ function initTracker(filePath) {
489
+ writeFileSync(filePath, `# KnowzCode - Status Map
490
+
491
+ **Purpose:** This document tracks the development status of all implementable components (NodeIDs) defined in \`knowzcode_architecture.md\`.
492
+
493
+ ---
494
+ **Progress: 0%**
495
+ ---
496
+
497
+ | Status | WorkGroupID | Node ID | Label | Dependencies | Logical Grouping | Spec Link | Classification | Notes / Issues |
498
+ | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
499
+ | | | | | | | | | |
500
+
501
+ ---
502
+ ### Status Legend:
503
+
504
+ * ⚪️ **\`[TODO]\`**: Task is defined and ready to be picked up if dependencies are met.
505
+ * 📝 **\`[NEEDS_SPEC]\`**: Node has been identified but requires a detailed specification.
506
+ * ◆ **\`[WIP]\`**: Work In Progress. The KnowzCode AI Agent is currently working on this node.
507
+ * 🟢 **\`[VERIFIED]\`**: Node has been implemented and verified.
508
+ * ❗ **\`[ISSUE]\`**: A significant issue or blocker has been identified.
509
+
510
+ ---
511
+ *(This table will be populated as you define your architecture and NodeIDs.)*
512
+ `);
513
+ }
514
+
515
+ function initLog(filePath) {
516
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
517
+ writeFileSync(filePath, `# KnowzCode - Operational Record
518
+
519
+ **Purpose:** Chronological record of significant events, decisions, and verification outcomes.
520
+
521
+ ---
522
+
523
+ ## Section 1: Operational Log
524
+
525
+ ---
526
+ **[NEWEST ENTRIES APPEAR HERE - DO NOT REMOVE THIS MARKER]**
527
+ ---
528
+ **Type:** SystemInitialization
529
+ **Timestamp:** ${ts}
530
+ **NodeID(s):** Project-Wide
531
+ **Logged By:** knowzcode-cli
532
+ **Details:**
533
+ KnowzCode framework installed via \`npx knowzcode\`.
534
+ - Framework files initialized
535
+ - Ready for first feature
536
+ ---
537
+
538
+ ## Section 2: Reference Quality Criteria (ARC-Based Verification)
539
+
540
+ ### Core Quality Criteria
541
+ 1. **Maintainability:** Ease of modification, clarity of code and design.
542
+ 2. **Reliability:** Robustness of error handling, fault tolerance.
543
+ 3. **Testability:** Adequacy of unit test coverage, ease of testing.
544
+ 4. **Performance:** Responsiveness, efficiency in resource utilization.
545
+ 5. **Security:** Resistance to common vulnerabilities.
546
+
547
+ ### Structural Criteria
548
+ 6. **Readability:** Code clarity, adherence to naming conventions.
549
+ 7. **Complexity Management:** Avoidance of overly complex logic.
550
+ 8. **Modularity:** Adherence to Single Responsibility Principle.
551
+ 9. **Code Duplication (DRY):** Minimization of redundant code.
552
+ 10. **Standards Compliance:** Adherence to language best practices.
553
+
554
+ *(Refer to these criteria during ARC-Based Verification.)*
555
+ `);
556
+ }
557
+
558
+ // ─── Interactive Prompt ──────────────────────────────────────────────────────
559
+
560
+ async function promptPlatforms(detected) {
561
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
562
+ const ids = Object.keys(PLATFORMS);
563
+
564
+ console.log('');
565
+ console.log(`${c.bold}Select platforms to generate adapters for:${c.reset}`);
566
+ console.log('');
567
+ ids.forEach((id, i) => {
568
+ const p = PLATFORMS[id];
569
+ const tag = detected.includes(id) ? ` ${c.green}(detected)${c.reset}` : '';
570
+ console.log(` [${i + 1}] ${p.name}${tag}`);
571
+ });
572
+ console.log(` [A] All platforms`);
573
+ console.log(` [S] Skip adapters (core framework only)`);
574
+ console.log('');
575
+
576
+ const answer = await rl.question('Select platforms (comma-separated, e.g. 1,2): ');
577
+ rl.close();
578
+
579
+ const trimmed = answer.trim().toUpperCase();
580
+ if (trimmed === 'S' || trimmed === '') return [];
581
+ if (trimmed === 'A') return ids;
582
+
583
+ const selected = [];
584
+ for (const part of trimmed.split(',')) {
585
+ const num = parseInt(part.trim(), 10);
586
+ if (num >= 1 && num <= ids.length) {
587
+ selected.push(ids[num - 1]);
588
+ }
589
+ }
590
+ return [...new Set(selected)];
591
+ }
592
+
593
+ async function promptConfirm(message) {
594
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
595
+ const answer = await rl.question(`${message} [y/N]: `);
596
+ rl.close();
597
+ return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
598
+ }
599
+
600
+ // ─── Agent Teams Enablement ──────────────────────────────────────────────────
601
+
602
+ function enableAgentTeams(claudeDir, isGlobal) {
603
+ ensureDir(claudeDir);
604
+ const settingsFile = join(claudeDir, isGlobal ? 'settings.json' : 'settings.local.json');
605
+
606
+ let settings = {};
607
+ if (existsSync(settingsFile)) {
608
+ try {
609
+ settings = JSON.parse(readFileSync(settingsFile, 'utf8'));
610
+ } catch {
611
+ settings = {};
612
+ }
613
+ }
614
+
615
+ if (!settings.env) settings.env = {};
616
+ settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1';
617
+
618
+ writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
619
+ log.ok(`Agent Teams enabled in ${settingsFile}`);
620
+ }
621
+
622
+ // ─── Commands ────────────────────────────────────────────────────────────────
623
+
624
+ // DETECT
625
+ function cmdDetect(opts) {
626
+ const dir = opts.target;
627
+ console.log('');
628
+ console.log(`${c.bold}KnowzCode Platform Detection${c.reset}`);
629
+ console.log(`${c.dim}Scanning: ${dir}${c.reset}`);
630
+ console.log('');
631
+
632
+ const detected = detectPlatforms(dir);
633
+ const hasKnowzcode = existsSync(join(dir, 'knowzcode'));
634
+
635
+ console.log(` KnowzCode framework: ${hasKnowzcode ? `${c.green}installed${c.reset}` : `${c.dim}not found${c.reset}`}`);
636
+
637
+ if (hasKnowzcode) {
638
+ const versionFile = join(dir, 'knowzcode', '.knowzcode-version');
639
+ if (existsSync(versionFile)) {
640
+ const ver = readFileSync(versionFile, 'utf8').trim();
641
+ console.log(` Installed version: ${c.cyan}${ver}${c.reset}`);
642
+ }
643
+ }
644
+
645
+ console.log('');
646
+ console.log(` ${c.bold}Platforms:${c.reset}`);
647
+
648
+ for (const [id, platform] of Object.entries(PLATFORMS)) {
649
+ const found = detected.includes(id);
650
+ const indicator = found ? `${c.green}detected${c.reset}` : `${c.dim}not detected${c.reset}`;
651
+ console.log(` ${platform.name.padEnd(18)} ${indicator}`);
652
+ }
653
+
654
+ console.log('');
655
+ if (detected.length === 0) {
656
+ console.log(` No platforms detected. Run ${c.cyan}npx knowzcode install${c.reset} to set up.`);
657
+ } else {
658
+ console.log(` ${detected.length} platform(s) detected.`);
659
+ }
660
+ console.log('');
661
+ }
662
+
663
+ // INSTALL
664
+ async function cmdInstall(opts) {
665
+ const dir = opts.target;
666
+ const kcDir = join(dir, 'knowzcode');
667
+
668
+ console.log('');
669
+ console.log(`${c.bold}KnowzCode Install${c.reset}`);
670
+ console.log(`${c.dim}Target: ${dir}${c.reset}`);
671
+ console.log('');
672
+
673
+ // Check for existing installation
674
+ if (existsSync(kcDir) && !opts.force) {
675
+ log.warn('KnowzCode already installed at ' + kcDir);
676
+ log.warn('Use --force to overwrite.');
677
+ process.exit(1);
678
+ }
679
+
680
+ if (!existsSync(dir)) {
681
+ log.err('Target directory does not exist: ' + dir);
682
+ process.exit(1);
683
+ }
684
+
685
+ // 1. Copy knowzcode/ template directory
686
+ log.info('Installing core framework...');
687
+ const srcKc = join(PKG_ROOT, 'knowzcode');
688
+ ensureDir(kcDir);
689
+ ensureDir(join(kcDir, 'specs'));
690
+ ensureDir(join(kcDir, 'workgroups'));
691
+ ensureDir(join(kcDir, 'prompts'));
692
+
693
+ // Create workgroups/README.md (workgroups/ is gitignored and excluded from npm)
694
+ writeFileSync(join(kcDir, 'workgroups', 'README.md'), '# WorkGroups\n\nSession-specific WorkGroup files are stored here.\nThis directory is gitignored — contents are local to each checkout.\n');
695
+
696
+ // Copy .md files (skip tracker and log — generate fresh)
697
+ for (const entry of readdirSync(srcKc)) {
698
+ const srcPath = join(srcKc, entry);
699
+ const stat = statSync(srcPath);
700
+ if (stat.isFile() && entry.endsWith('.md') && entry !== 'knowzcode_tracker.md' && entry !== 'knowzcode_log.md') {
701
+ writeFileSync(join(kcDir, entry), readFileSync(srcPath));
702
+ } else if (stat.isFile() && !entry.endsWith('.md')) {
703
+ // Copy non-md files, handling gitignore.template → .gitignore rename
704
+ if (entry === 'gitignore.template') {
705
+ writeFileSync(join(kcDir, '.gitignore'), readFileSync(srcPath));
706
+ } else {
707
+ writeFileSync(join(kcDir, entry), readFileSync(srcPath));
708
+ }
709
+ }
710
+ }
711
+
712
+ // Copy prompts/
713
+ if (existsSync(join(srcKc, 'prompts'))) {
714
+ copyDirContents(join(srcKc, 'prompts'), join(kcDir, 'prompts'));
715
+ }
716
+
717
+ // Copy specs readme
718
+ if (existsSync(join(srcKc, 'specs', 'Readme.md'))) {
719
+ writeFileSync(join(kcDir, 'specs', 'Readme.md'), readFileSync(join(srcKc, 'specs', 'Readme.md')));
720
+ }
721
+
722
+ // Copy enterprise/ if exists
723
+ if (existsSync(join(srcKc, 'enterprise'))) {
724
+ copyDirContents(join(srcKc, 'enterprise'), join(kcDir, 'enterprise'));
725
+ }
726
+
727
+ // Initialize fresh tracker and log
728
+ initTracker(join(kcDir, 'knowzcode_tracker.md'));
729
+ initLog(join(kcDir, 'knowzcode_log.md'));
730
+
731
+ // Write version marker
732
+ writeFileSync(join(kcDir, '.knowzcode-version'), VERSION + '\n');
733
+
734
+ log.ok('Core framework installed');
735
+
736
+ // 2. Platform detection + selection
737
+ const detected = detectPlatforms(dir);
738
+ let selectedPlatforms;
739
+
740
+ if (opts.platforms.length > 0) {
741
+ if (opts.platforms.includes('all')) {
742
+ selectedPlatforms = Object.keys(PLATFORMS);
743
+ } else {
744
+ selectedPlatforms = opts.platforms.filter((p) => p in PLATFORMS);
745
+ }
746
+ } else if (opts.force) {
747
+ // Non-interactive mode with --force: install for detected platforms only
748
+ selectedPlatforms = detected;
749
+ } else {
750
+ selectedPlatforms = await promptPlatforms(detected);
751
+ }
752
+
753
+ // 3. Generate adapters
754
+ const templates = parseAdapterTemplates();
755
+ const adapterFiles = [];
756
+
757
+ for (const platformId of selectedPlatforms) {
758
+ const platform = PLATFORMS[platformId];
759
+
760
+ if (platformId === 'claude') {
761
+ // Claude Code: copy agents, commands, skills
762
+ const claudeDir = opts.global ? join(process.env.HOME || process.env.USERPROFILE || '~', '.claude') : join(dir, '.claude');
763
+
764
+ log.info(`Installing Claude Code components to ${claudeDir}/`);
765
+
766
+ // Remove stale files before copying on --force
767
+ if (opts.force) {
768
+ removeStaleFiles(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
769
+ removeStaleFiles(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
770
+ removeStaleFiles(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
771
+ }
772
+
773
+ copyDirContents(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
774
+ copyDirContents(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
775
+ copyDirContents(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
776
+
777
+ // Pre-register marketplace in settings.json
778
+ setMarketplaceConfig(claudeDir);
779
+
780
+ adapterFiles.push(claudeDir + '/commands/', claudeDir + '/agents/', claudeDir + '/skills/');
781
+ } else {
782
+ // Other platforms: extract template and write adapter + additional files
783
+ const templateSet = templates.get(platformId);
784
+ if (!templateSet) {
785
+ log.warn(`No adapter template found for ${platform.name} — skipping`);
786
+ continue;
787
+ }
788
+
789
+ // Write primary adapter file
790
+ const adapterFile = platform.adapterPath(dir);
791
+ ensureDir(dirname(adapterFile));
792
+ writeFileSync(adapterFile, injectVersion(templateSet.primary));
793
+ adapterFiles.push(adapterFile);
794
+ log.ok(`${platform.name} adapter: ${adapterFile}`);
795
+
796
+ // Write additional files (prompts, TOMLs, skills, subagents)
797
+ for (const [relativePath, { content }] of templateSet.files) {
798
+ let filePath;
799
+ if (platformId === 'codex' && opts.global && relativePath.startsWith('.agents/skills/')) {
800
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
801
+ filePath = join(homeDir, relativePath);
802
+ } else if (platformId === 'gemini' && opts.global && relativePath.startsWith('.gemini/skills/')) {
803
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
804
+ filePath = join(homeDir, relativePath);
805
+ } else {
806
+ filePath = join(dir, relativePath);
807
+ }
808
+ ensureDir(dirname(filePath));
809
+ writeFileSync(filePath, injectVersion(content));
810
+ adapterFiles.push(filePath);
811
+ }
812
+ if (templateSet.files.size > 0) {
813
+ log.ok(` + ${templateSet.files.size} additional file(s)`);
814
+ }
815
+
816
+ // Clean up legacy .codex/skills/kc/ if present (migrated to .agents/skills/)
817
+ if (platformId === 'codex') {
818
+ const legacySkillDir = join(dir, '.codex', 'skills', 'kc');
819
+ if (existsSync(legacySkillDir)) {
820
+ log.info('Removing legacy .codex/skills/kc/ (migrated to .agents/skills/)');
821
+ rmSync(legacySkillDir, { recursive: true, force: true });
822
+ }
823
+ if (opts.global) {
824
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
825
+ const legacyGlobal = join(homeDir, '.codex', 'skills', 'kc');
826
+ if (existsSync(legacyGlobal)) {
827
+ log.info('Removing legacy global ~/.codex/skills/kc/');
828
+ rmSync(legacyGlobal, { recursive: true, force: true });
829
+ }
830
+ }
831
+ }
832
+ }
833
+ }
834
+
835
+ // 3.5. Gemini MCP config offer (when Gemini is selected and not --force)
836
+ if (selectedPlatforms.includes('gemini') && !opts.global && !opts.force) {
837
+ console.log('');
838
+ console.log(`${c.bold}Gemini MCP Configuration${c.reset}`);
839
+ console.log(`MCP enables vector search, vault access, and AI-powered Q&A.`);
840
+ const wantMcp = await promptConfirm('Configure MCP for Gemini CLI? (requires API key)');
841
+ if (wantMcp) {
842
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
843
+ const apiKey = await rl.question('Enter your KnowzCode API key: ');
844
+ rl.close();
845
+ if (apiKey.trim()) {
846
+ const settingsPath = join(dir, '.gemini', 'settings.json');
847
+ writeGeminiMcpConfig(settingsPath, apiKey.trim(), dir);
848
+ log.ok('Gemini MCP configured in .gemini/settings.json');
849
+ } else {
850
+ log.warn('No API key provided — skipping MCP config. Use /kc:connect-mcp later.');
851
+ }
852
+ }
853
+ }
854
+
855
+ // 4. Agent Teams enablement
856
+ const agentTeamsClaudeDir = opts.global
857
+ ? join(process.env.HOME || process.env.USERPROFILE || '~', '.claude')
858
+ : join(dir, '.claude');
859
+ let agentTeamsEnabled = false;
860
+ if (opts.agentTeams) {
861
+ enableAgentTeams(agentTeamsClaudeDir, opts.global);
862
+ agentTeamsEnabled = true;
863
+ } else if (selectedPlatforms.includes('claude') && !opts.force) {
864
+ // Interactive prompt for Claude Code users
865
+ console.log('');
866
+ console.log(`${c.bold}Agent Teams${c.reset} enables multi-agent coordination where specialized`);
867
+ console.log(`teammates handle each workflow phase. ${c.dim}(experimental)${c.reset}`);
868
+ const wantTeams = await promptConfirm('Enable Agent Teams? (recommended for Claude Code)');
869
+ if (wantTeams) {
870
+ enableAgentTeams(agentTeamsClaudeDir, opts.global);
871
+ agentTeamsEnabled = true;
872
+ }
873
+ }
874
+
875
+ // 5. Summary
876
+ console.log('');
877
+ console.log(`${c.green}${c.bold}Installation complete!${c.reset}`);
878
+ console.log('');
879
+ console.log(' Framework: ' + kcDir + '/');
880
+ if (adapterFiles.length > 0) {
881
+ console.log(' Adapters:');
882
+ for (const f of adapterFiles) {
883
+ console.log(' ' + f);
884
+ }
885
+ }
886
+ if (agentTeamsEnabled) {
887
+ console.log(' Agent Teams: enabled');
888
+ }
889
+ console.log('');
890
+ console.log(`${c.bold}Next steps:${c.reset}`);
891
+ console.log(' 1. Edit knowzcode/knowzcode_project.md — set project name, stack, standards');
892
+ console.log(' 2. Edit knowzcode/environment_context.md — configure build/test commands');
893
+ if (selectedPlatforms.includes('claude')) {
894
+ console.log(' 3. Install the KnowzCode plugin (recommended):');
895
+ console.log(' /plugin install kc@knowzcode');
896
+ console.log(' 4. Start building:');
897
+ console.log(' /kc:work "Your first feature"');
898
+ console.log('');
899
+ console.log(' Note: Commands also work without plugin as /work, /plan, /fix, etc.');
900
+ } else {
901
+ console.log(' 3. Start building: use knowzcode/prompts/[LOOP_1A]__Propose_Change_Set.md');
902
+ }
903
+ console.log('');
904
+ }
905
+
906
+ // UNINSTALL
907
+ async function cmdUninstall(opts) {
908
+ const dir = opts.target;
909
+ const kcDir = join(dir, 'knowzcode');
910
+
911
+ console.log('');
912
+ console.log(`${c.bold}KnowzCode Uninstall${c.reset}`);
913
+ console.log(`${c.dim}Target: ${dir}${c.reset}`);
914
+ console.log('');
915
+
916
+ // Scan for installed components
917
+ const components = [];
918
+
919
+ if (existsSync(kcDir)) {
920
+ components.push({ label: 'Core framework', path: kcDir });
921
+ }
922
+
923
+ // Claude Code components
924
+ const claudeDir = join(dir, '.claude');
925
+ for (const sub of ['commands', 'agents', 'skills']) {
926
+ const p = join(claudeDir, sub);
927
+ if (existsSync(p)) {
928
+ components.push({ label: `Claude Code ${sub}`, path: p });
929
+ }
930
+ }
931
+
932
+ // Platform adapter files
933
+ const adapterChecks = {
934
+ codex: join(dir, 'AGENTS.md'),
935
+ gemini: join(dir, 'GEMINI.md'),
936
+ cursor: join(dir, '.cursor', 'rules', 'knowzcode.mdc'),
937
+ copilot: join(dir, '.github', 'copilot-instructions.md'),
938
+ windsurf: join(dir, '.windsurf', 'rules', 'knowzcode.md'),
939
+ };
940
+
941
+ for (const [id, path] of Object.entries(adapterChecks)) {
942
+ if (existsSync(path)) {
943
+ components.push({ label: `${PLATFORMS[id].name} adapter`, path });
944
+ }
945
+ }
946
+
947
+ // Additional platform-specific files/directories
948
+ const copilotPromptsDir = join(dir, '.github', 'prompts');
949
+ if (existsSync(copilotPromptsDir)) {
950
+ for (const f of readdirSync(copilotPromptsDir)) {
951
+ if (f.startsWith('kc-') && f.endsWith('.prompt.md')) {
952
+ components.push({ label: `Copilot prompt: ${f}`, path: join(copilotPromptsDir, f) });
953
+ }
954
+ }
955
+ }
956
+ const vscodeMcp = join(dir, '.vscode', 'mcp.json');
957
+ if (existsSync(vscodeMcp)) {
958
+ components.push({ label: 'VS Code MCP config', path: vscodeMcp });
959
+ }
960
+ const geminiCmdDir = join(dir, '.gemini', 'commands', 'kc');
961
+ if (existsSync(geminiCmdDir)) {
962
+ components.push({ label: 'Gemini commands (kc/)', path: geminiCmdDir });
963
+ }
964
+ // Gemini skills: .gemini/skills/kc-*
965
+ const geminiSkillDir = join(dir, '.gemini', 'skills');
966
+ if (existsSync(geminiSkillDir)) {
967
+ for (const entry of readdirSync(geminiSkillDir)) {
968
+ if (entry.startsWith('kc-')) {
969
+ components.push({ label: `Gemini skill (${entry}/)`, path: join(geminiSkillDir, entry) });
970
+ }
971
+ }
972
+ }
973
+ const globalGeminiSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.gemini', 'skills');
974
+ if (existsSync(globalGeminiSkillDir)) {
975
+ for (const entry of readdirSync(globalGeminiSkillDir)) {
976
+ if (entry.startsWith('kc-')) {
977
+ components.push({ label: `Gemini skill — global (~/.gemini/skills/${entry}/)`, path: join(globalGeminiSkillDir, entry) });
978
+ }
979
+ }
980
+ }
981
+ // Gemini subagents: .gemini/agents/kc-*.md
982
+ const geminiAgentDir = join(dir, '.gemini', 'agents');
983
+ if (existsSync(geminiAgentDir)) {
984
+ for (const entry of readdirSync(geminiAgentDir)) {
985
+ if (entry.startsWith('kc-') && entry.endsWith('.md')) {
986
+ components.push({ label: `Gemini subagent (${entry})`, path: join(geminiAgentDir, entry) });
987
+ }
988
+ }
989
+ }
990
+ // New path: .agents/skills/kc-*
991
+ const agentsSkillDir = join(dir, '.agents', 'skills');
992
+ if (existsSync(agentsSkillDir)) {
993
+ for (const entry of readdirSync(agentsSkillDir)) {
994
+ if (entry.startsWith('kc-')) {
995
+ components.push({ label: `Codex skill (${entry}/)`, path: join(agentsSkillDir, entry) });
996
+ }
997
+ }
998
+ }
999
+ const globalAgentsSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.agents', 'skills');
1000
+ if (existsSync(globalAgentsSkillDir)) {
1001
+ for (const entry of readdirSync(globalAgentsSkillDir)) {
1002
+ if (entry.startsWith('kc-')) {
1003
+ components.push({ label: `Codex skill — global (~/.agents/skills/${entry}/)`, path: join(globalAgentsSkillDir, entry) });
1004
+ }
1005
+ }
1006
+ }
1007
+ // Legacy path: .codex/skills/kc (remove on uninstall)
1008
+ const legacyCodexSkillDir = join(dir, '.codex', 'skills', 'kc');
1009
+ if (existsSync(legacyCodexSkillDir)) {
1010
+ components.push({ label: 'Codex skills — legacy (.codex/skills/kc/)', path: legacyCodexSkillDir });
1011
+ }
1012
+ const legacyGlobalCodexSkillDir = join(process.env.HOME || process.env.USERPROFILE || '~', '.codex', 'skills', 'kc');
1013
+ if (existsSync(legacyGlobalCodexSkillDir)) {
1014
+ components.push({ label: 'Codex skills — legacy global (~/.codex/skills/kc/)', path: legacyGlobalCodexSkillDir });
1015
+ }
1016
+
1017
+ if (components.length === 0) {
1018
+ log.info('No KnowzCode installation found.');
1019
+ return;
1020
+ }
1021
+
1022
+ console.log(' Components found:');
1023
+ for (const comp of components) {
1024
+ console.log(` ${comp.label}: ${comp.path}`);
1025
+ }
1026
+ console.log('');
1027
+
1028
+ // Ask about preserving user data
1029
+ let preserveUserData = false;
1030
+ if (existsSync(kcDir) && !opts.force) {
1031
+ preserveUserData = await promptConfirm('Preserve user data (specs/, architecture, tracker, log)?');
1032
+ }
1033
+
1034
+ if (!opts.force) {
1035
+ const confirmed = await promptConfirm('Remove all listed components?');
1036
+ if (!confirmed) {
1037
+ log.info('Uninstall cancelled.');
1038
+ return;
1039
+ }
1040
+ }
1041
+
1042
+ const removed = [];
1043
+
1044
+ // Remove components
1045
+ for (const comp of components) {
1046
+ if (comp.path === kcDir && preserveUserData) {
1047
+ // Selective removal — keep user data
1048
+ const preserve = ['specs', 'knowzcode_architecture.md', 'knowzcode_tracker.md', 'knowzcode_log.md', 'knowzcode_project.md'];
1049
+
1050
+ for (const entry of readdirSync(kcDir)) {
1051
+ if (preserve.includes(entry)) continue;
1052
+ const entryPath = join(kcDir, entry);
1053
+ rmSync(entryPath, { recursive: true, force: true });
1054
+ }
1055
+ removed.push(comp.label + ' (user data preserved)');
1056
+ } else {
1057
+ rmSync(comp.path, { recursive: true, force: true });
1058
+ removed.push(comp.label);
1059
+ }
1060
+ }
1061
+
1062
+ // Clean up marketplace config from settings.json
1063
+ removeMarketplaceConfig(claudeDir);
1064
+
1065
+ // Clean up Gemini MCP config (remove only knowz entry, preserve other settings)
1066
+ const geminiSettingsProject = join(dir, '.gemini', 'settings.json');
1067
+ if (removeGeminiMcpConfig(geminiSettingsProject)) {
1068
+ removed.push('Gemini MCP config (.gemini/settings.json)');
1069
+ }
1070
+ const homeDir2 = process.env.HOME || process.env.USERPROFILE || '~';
1071
+ const geminiSettingsUser = join(homeDir2, '.gemini', 'settings.json');
1072
+ if (removeGeminiMcpConfig(geminiSettingsUser)) {
1073
+ removed.push('Gemini MCP config (~/.gemini/settings.json)');
1074
+ }
1075
+
1076
+ console.log('');
1077
+ log.ok('Uninstall complete');
1078
+ console.log(' Removed:');
1079
+ for (const r of removed) {
1080
+ console.log(` ${r}`);
1081
+ }
1082
+ console.log('');
1083
+ }
1084
+
1085
+ // UPGRADE
1086
+ async function cmdUpgrade(opts) {
1087
+ const dir = opts.target;
1088
+ const kcDir = join(dir, 'knowzcode');
1089
+
1090
+ console.log('');
1091
+ console.log(`${c.bold}KnowzCode Upgrade${c.reset}`);
1092
+ console.log(`${c.dim}Target: ${dir}${c.reset}`);
1093
+ console.log('');
1094
+
1095
+ if (!existsSync(kcDir)) {
1096
+ log.err('No KnowzCode installation found. Run `npx knowzcode install` first.');
1097
+ process.exit(1);
1098
+ }
1099
+
1100
+ // Read current version
1101
+ const versionFile = join(kcDir, '.knowzcode-version');
1102
+ const currentVersion = existsSync(versionFile) ? readFileSync(versionFile, 'utf8').trim() : 'unknown';
1103
+
1104
+ if (currentVersion === VERSION && !opts.force) {
1105
+ log.info(`Already at version ${VERSION}. Use --force to reinstall.`);
1106
+ return;
1107
+ }
1108
+
1109
+ log.info(`Upgrading: ${currentVersion} → ${VERSION}`);
1110
+
1111
+ // Files to preserve (never overwrite)
1112
+ const preserveFiles = new Set([
1113
+ 'knowzcode_tracker.md',
1114
+ 'knowzcode_log.md',
1115
+ 'knowzcode_architecture.md',
1116
+ 'knowzcode_project.md',
1117
+ 'environment_context.md',
1118
+ 'user_preferences.md',
1119
+ ]);
1120
+ const preserveDirs = new Set(['specs', 'workgroups']);
1121
+
1122
+ // Files to replace (always update)
1123
+ const srcKc = join(PKG_ROOT, 'knowzcode');
1124
+
1125
+ // Update .md files
1126
+ for (const entry of readdirSync(srcKc)) {
1127
+ const srcPath = join(srcKc, entry);
1128
+ const dstPath = join(kcDir, entry);
1129
+ const stat = statSync(srcPath);
1130
+
1131
+ if (stat.isFile()) {
1132
+ if (preserveFiles.has(entry)) {
1133
+ if (opts.verbose) log.info(`Preserved: ${entry}`);
1134
+ continue;
1135
+ }
1136
+ // Handle gitignore.template → .gitignore rename
1137
+ if (entry === 'gitignore.template') {
1138
+ writeFileSync(join(kcDir, '.gitignore'), readFileSync(srcPath));
1139
+ if (opts.verbose) log.info('Updated: .gitignore (from gitignore.template)');
1140
+ } else {
1141
+ writeFileSync(dstPath, readFileSync(srcPath));
1142
+ if (opts.verbose) log.info(`Updated: ${entry}`);
1143
+ }
1144
+ }
1145
+ }
1146
+
1147
+ // Update prompts/ (always replace)
1148
+ if (existsSync(join(srcKc, 'prompts'))) {
1149
+ const promptsDst = join(kcDir, 'prompts');
1150
+ // Remove old prompts, copy new ones
1151
+ if (existsSync(promptsDst)) rmSync(promptsDst, { recursive: true, force: true });
1152
+ copyDirContents(join(srcKc, 'prompts'), promptsDst);
1153
+ if (opts.verbose) log.info('Updated: prompts/');
1154
+ }
1155
+
1156
+ // Update enterprise/ (always replace)
1157
+ if (existsSync(join(srcKc, 'enterprise'))) {
1158
+ const entDst = join(kcDir, 'enterprise');
1159
+ if (existsSync(entDst)) rmSync(entDst, { recursive: true, force: true });
1160
+ copyDirContents(join(srcKc, 'enterprise'), entDst);
1161
+ if (opts.verbose) log.info('Updated: enterprise/');
1162
+ }
1163
+
1164
+ // Update Claude Code components if present
1165
+ const claudeDir = join(dir, '.claude');
1166
+ if (existsSync(join(claudeDir, 'commands')) || existsSync(join(claudeDir, 'agents'))) {
1167
+ log.info('Updating Claude Code components...');
1168
+ // Remove stale files before copying
1169
+ removeStaleFiles(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
1170
+ removeStaleFiles(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
1171
+ removeStaleFiles(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
1172
+ copyDirContents(join(PKG_ROOT, 'commands'), join(claudeDir, 'commands'));
1173
+ copyDirContents(join(PKG_ROOT, 'agents'), join(claudeDir, 'agents'));
1174
+ copyDirContents(join(PKG_ROOT, 'skills'), join(claudeDir, 'skills'));
1175
+ // Ensure marketplace config is up to date
1176
+ setMarketplaceConfig(claudeDir);
1177
+ }
1178
+
1179
+ // Regenerate adapters for detected platforms
1180
+ const detected = detectPlatforms(dir);
1181
+ const templates = parseAdapterTemplates();
1182
+ const regenerated = [];
1183
+
1184
+ for (const platformId of detected) {
1185
+ if (platformId === 'claude') continue; // Already handled above
1186
+ const platform = PLATFORMS[platformId];
1187
+ if (!platform.adapterPath) continue;
1188
+
1189
+ const adapterFile = platform.adapterPath(dir);
1190
+ if (!existsSync(adapterFile)) continue; // Only update existing adapters
1191
+
1192
+ const templateSet = templates.get(platformId);
1193
+ if (!templateSet) continue;
1194
+
1195
+ // Update primary adapter file
1196
+ writeFileSync(adapterFile, injectVersion(templateSet.primary));
1197
+ regenerated.push(platform.name);
1198
+
1199
+ // Regenerate additional files
1200
+ const currentPaths = new Set();
1201
+ for (const [relativePath, { content }] of templateSet.files) {
1202
+ const filePath = join(dir, relativePath);
1203
+ ensureDir(dirname(filePath));
1204
+ writeFileSync(filePath, injectVersion(content));
1205
+ currentPaths.add(relativePath);
1206
+ }
1207
+
1208
+ // Stale file cleanup for platform-owned directories
1209
+ if (platformId === 'copilot') {
1210
+ const promptsDir = join(dir, '.github', 'prompts');
1211
+ if (existsSync(promptsDir)) {
1212
+ for (const f of readdirSync(promptsDir)) {
1213
+ if (f.startsWith('kc-') && f.endsWith('.prompt.md') && !currentPaths.has(`.github/prompts/${f}`)) {
1214
+ log.info(`Removing stale prompt: ${f}`);
1215
+ rmSync(join(promptsDir, f), { force: true });
1216
+ }
1217
+ }
1218
+ }
1219
+ } else if (platformId === 'gemini') {
1220
+ const tomlDir = join(dir, '.gemini', 'commands', 'kc');
1221
+ if (existsSync(tomlDir)) {
1222
+ for (const f of readdirSync(tomlDir)) {
1223
+ if (f.endsWith('.toml') && !currentPaths.has(`.gemini/commands/kc/${f}`)) {
1224
+ log.info(`Removing stale command: ${f}`);
1225
+ rmSync(join(tomlDir, f), { force: true });
1226
+ }
1227
+ }
1228
+ }
1229
+ // Stale skill cleanup: .gemini/skills/kc-*
1230
+ const geminiSkillDir = join(dir, '.gemini', 'skills');
1231
+ if (existsSync(geminiSkillDir)) {
1232
+ for (const entry of readdirSync(geminiSkillDir)) {
1233
+ if (entry.startsWith('kc-') && !currentPaths.has(`.gemini/skills/${entry}/SKILL.md`)) {
1234
+ log.info(`Removing stale Gemini skill: ${entry}/`);
1235
+ rmSync(join(geminiSkillDir, entry), { recursive: true, force: true });
1236
+ }
1237
+ }
1238
+ }
1239
+ // Stale subagent cleanup: .gemini/agents/kc-*.md
1240
+ const geminiAgentDir = join(dir, '.gemini', 'agents');
1241
+ if (existsSync(geminiAgentDir)) {
1242
+ for (const entry of readdirSync(geminiAgentDir)) {
1243
+ if (entry.startsWith('kc-') && entry.endsWith('.md') && !currentPaths.has(`.gemini/agents/${entry}`)) {
1244
+ log.info(`Removing stale Gemini subagent: ${entry}`);
1245
+ rmSync(join(geminiAgentDir, entry), { force: true });
1246
+ }
1247
+ }
1248
+ }
1249
+ } else if (platformId === 'codex') {
1250
+ const skillDir = join(dir, '.agents', 'skills');
1251
+ if (existsSync(skillDir)) {
1252
+ for (const entry of readdirSync(skillDir)) {
1253
+ if (entry.startsWith('kc-') && !currentPaths.has(`.agents/skills/${entry}/SKILL.md`)) {
1254
+ log.info(`Removing stale skill: ${entry}/`);
1255
+ rmSync(join(skillDir, entry), { recursive: true, force: true });
1256
+ }
1257
+ }
1258
+ }
1259
+ // Migration: remove legacy .codex/skills/kc/ if present
1260
+ const legacySkillDir = join(dir, '.codex', 'skills', 'kc');
1261
+ if (existsSync(legacySkillDir)) {
1262
+ log.info('Removing legacy .codex/skills/kc/ (migrated to .agents/skills/)');
1263
+ rmSync(legacySkillDir, { recursive: true, force: true });
1264
+ }
1265
+ }
1266
+ }
1267
+
1268
+ // Check for global codex skills
1269
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '~';
1270
+ const globalAgentsSkillDir = join(homeDir, '.agents', 'skills');
1271
+ if (existsSync(globalAgentsSkillDir)) {
1272
+ const codexTemplateSet = templates.get('codex');
1273
+ if (codexTemplateSet) {
1274
+ const currentPaths = new Set([...codexTemplateSet.files.keys()]);
1275
+ // Stale cleanup
1276
+ for (const entry of readdirSync(globalAgentsSkillDir)) {
1277
+ if (entry.startsWith('kc-') && !currentPaths.has(`.agents/skills/${entry}/SKILL.md`)) {
1278
+ log.info(`Removing stale global skill: ${entry}/`);
1279
+ rmSync(join(globalAgentsSkillDir, entry), { recursive: true, force: true });
1280
+ }
1281
+ }
1282
+ // Regenerate global skills
1283
+ for (const [relativePath, { content }] of codexTemplateSet.files) {
1284
+ if (relativePath.startsWith('.agents/skills/')) {
1285
+ const filePath = join(homeDir, relativePath);
1286
+ ensureDir(dirname(filePath));
1287
+ writeFileSync(filePath, injectVersion(content));
1288
+ }
1289
+ }
1290
+ log.info('Updated global Codex skills');
1291
+ }
1292
+ }
1293
+ // Migration: remove legacy global .codex/skills/kc/ if present
1294
+ const legacyGlobalSkillDir = join(homeDir, '.codex', 'skills', 'kc');
1295
+ if (existsSync(legacyGlobalSkillDir)) {
1296
+ log.info('Removing legacy global ~/.codex/skills/kc/ (migrated to ~/.agents/skills/)');
1297
+ rmSync(legacyGlobalSkillDir, { recursive: true, force: true });
1298
+ }
1299
+
1300
+ // Check for global Gemini skills
1301
+ const globalGeminiSkillDir = join(homeDir, '.gemini', 'skills');
1302
+ if (existsSync(globalGeminiSkillDir)) {
1303
+ const geminiTemplateSet = templates.get('gemini');
1304
+ if (geminiTemplateSet) {
1305
+ const currentPaths = new Set([...geminiTemplateSet.files.keys()]);
1306
+ // Stale cleanup
1307
+ for (const entry of readdirSync(globalGeminiSkillDir)) {
1308
+ if (entry.startsWith('kc-') && !currentPaths.has(`.gemini/skills/${entry}/SKILL.md`)) {
1309
+ log.info(`Removing stale global Gemini skill: ${entry}/`);
1310
+ rmSync(join(globalGeminiSkillDir, entry), { recursive: true, force: true });
1311
+ }
1312
+ }
1313
+ // Regenerate global skills
1314
+ for (const [relativePath, { content }] of geminiTemplateSet.files) {
1315
+ if (relativePath.startsWith('.gemini/skills/')) {
1316
+ const filePath = join(homeDir, relativePath);
1317
+ ensureDir(dirname(filePath));
1318
+ writeFileSync(filePath, injectVersion(content));
1319
+ }
1320
+ }
1321
+ log.info('Updated global Gemini skills');
1322
+ }
1323
+ }
1324
+
1325
+ // Preserve Gemini MCP config during upgrade (don't overwrite user's API key)
1326
+ const geminiSettingsPath = join(dir, '.gemini', 'settings.json');
1327
+ const geminiMcpPreserved = hasGeminiMcpConfig(geminiSettingsPath);
1328
+ if (geminiMcpPreserved && opts.verbose) {
1329
+ log.info('Preserved: Gemini MCP config (.gemini/settings.json)');
1330
+ }
1331
+
1332
+ // Write new version
1333
+ writeFileSync(versionFile, VERSION + '\n');
1334
+
1335
+ console.log('');
1336
+ log.ok(`Upgraded to ${VERSION}`);
1337
+ console.log('');
1338
+ console.log(` ${c.bold}Preserved:${c.reset} specs/, tracker, log, architecture, project config${geminiMcpPreserved ? ', Gemini MCP config' : ''}`);
1339
+ console.log(` ${c.bold}Updated:${c.reset} loop, prompts, adapters, enterprise templates`);
1340
+ if (regenerated.length > 0) {
1341
+ console.log(` ${c.bold}Adapters:${c.reset} ${regenerated.join(', ')}`);
1342
+ }
1343
+ console.log('');
1344
+ }
1345
+
1346
+ // HELP
1347
+ function cmdHelp() {
1348
+ console.log(`
1349
+ ${c.bold}KnowzCode CLI${c.reset} v${VERSION}
1350
+ Platform-agnostic AI development methodology
1351
+
1352
+ ${c.bold}Usage:${c.reset}
1353
+ npx knowzcode Interactive mode
1354
+ npx knowzcode install [options] Install to current/target directory
1355
+ npx knowzcode uninstall [options] Remove KnowzCode
1356
+ npx knowzcode upgrade [options] Upgrade preserving user data
1357
+ npx knowzcode detect Show detected platforms (dry run)
1358
+
1359
+ ${c.bold}Options:${c.reset}
1360
+ --target <path> Target directory (default: current directory)
1361
+ --platforms <list> Comma-separated: claude,codex,gemini,cursor,copilot,windsurf,all
1362
+ --force Skip confirmation prompts
1363
+ --global Install Claude Code to ~/.claude/, Codex skills to ~/.agents/skills/, Gemini skills to ~/.gemini/skills/
1364
+ --agent-teams Enable Agent Teams in .claude/settings.local.json
1365
+ --verbose Show detailed output
1366
+ -h, --help Show this help
1367
+ -v, --version Show version
1368
+
1369
+ ${c.bold}Examples:${c.reset}
1370
+ npx knowzcode install --platforms claude,cursor
1371
+ npx knowzcode install --platforms all --force
1372
+ npx knowzcode upgrade --target ./my-project
1373
+ npx knowzcode uninstall --force
1374
+ npx knowzcode detect
1375
+ `);
1376
+ }
1377
+
1378
+ // INTERACTIVE
1379
+ async function cmdInteractive(opts) {
1380
+ console.log('');
1381
+ console.log(`${c.bold} ╔═══════════════════════════════════════╗${c.reset}`);
1382
+ console.log(`${c.bold} ║ KnowzCode v${VERSION.padEnd(14)} ║${c.reset}`);
1383
+ console.log(`${c.bold} ║ AI Development Methodology Installer ║${c.reset}`);
1384
+ console.log(`${c.bold} ╚═══════════════════════════════════════╝${c.reset}`);
1385
+ console.log('');
1386
+
1387
+ const dir = opts.target;
1388
+ const kcDir = join(dir, 'knowzcode');
1389
+ const detected = detectPlatforms(dir);
1390
+
1391
+ if (detected.length > 0) {
1392
+ log.info('Detected platforms: ' + detected.map((d) => PLATFORMS[d].name).join(', '));
1393
+ }
1394
+
1395
+ if (existsSync(kcDir)) {
1396
+ const versionFile = join(kcDir, '.knowzcode-version');
1397
+ const currentVersion = existsSync(versionFile) ? readFileSync(versionFile, 'utf8').trim() : 'unknown';
1398
+
1399
+ if (currentVersion !== VERSION) {
1400
+ log.info(`Installed version: ${currentVersion}, available: ${VERSION}`);
1401
+ const doUpgrade = await promptConfirm('Upgrade to latest version?');
1402
+ if (doUpgrade) {
1403
+ return cmdUpgrade(opts);
1404
+ }
1405
+ } else {
1406
+ log.info(`KnowzCode ${currentVersion} already installed.`);
1407
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1408
+ console.log('');
1409
+ console.log(' [1] Reinstall (overwrite)');
1410
+ console.log(' [2] Uninstall');
1411
+ console.log(' [3] Detect platforms');
1412
+ console.log(' [4] Exit');
1413
+ console.log('');
1414
+ const answer = await rl.question('Select action: ');
1415
+ rl.close();
1416
+
1417
+ const choice = answer.trim();
1418
+ if (choice === '1') return cmdInstall({ ...opts, force: true });
1419
+ if (choice === '2') return cmdUninstall(opts);
1420
+ if (choice === '3') return cmdDetect(opts);
1421
+ return;
1422
+ }
1423
+ } else {
1424
+ log.info('No existing installation found. Starting install...');
1425
+ console.log('');
1426
+ return cmdInstall(opts);
1427
+ }
1428
+ }
1429
+
1430
+ // ─── Main ────────────────────────────────────────────────────────────────────
1431
+
1432
+ async function main() {
1433
+ const opts = parseArgs(process.argv);
1434
+
1435
+ switch (opts.command) {
1436
+ case 'install':
1437
+ return cmdInstall(opts);
1438
+ case 'uninstall':
1439
+ return cmdUninstall(opts);
1440
+ case 'upgrade':
1441
+ return cmdUpgrade(opts);
1442
+ case 'detect':
1443
+ return cmdDetect(opts);
1444
+ case 'help':
1445
+ return cmdHelp();
1446
+ case 'version':
1447
+ console.log(VERSION);
1448
+ return;
1449
+ default:
1450
+ return cmdInteractive(opts);
1451
+ }
1452
+ }
1453
+
1454
+ main().catch((err) => {
1455
+ log.err(err.message);
1456
+ process.exit(1);
1457
+ });