@uxmaltech/collab-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +227 -0
  2. package/bin/collab +10 -0
  3. package/dist/cli.js +34 -0
  4. package/dist/commands/canon/index.js +16 -0
  5. package/dist/commands/canon/rebuild.js +95 -0
  6. package/dist/commands/compose/generate.js +63 -0
  7. package/dist/commands/compose/index.js +18 -0
  8. package/dist/commands/compose/validate.js +53 -0
  9. package/dist/commands/doctor.js +153 -0
  10. package/dist/commands/index.js +27 -0
  11. package/dist/commands/infra/down.js +23 -0
  12. package/dist/commands/infra/index.js +20 -0
  13. package/dist/commands/infra/shared.js +59 -0
  14. package/dist/commands/infra/status.js +64 -0
  15. package/dist/commands/infra/up.js +29 -0
  16. package/dist/commands/init.js +830 -0
  17. package/dist/commands/mcp/index.js +20 -0
  18. package/dist/commands/mcp/shared.js +57 -0
  19. package/dist/commands/mcp/start.js +45 -0
  20. package/dist/commands/mcp/status.js +62 -0
  21. package/dist/commands/mcp/stop.js +23 -0
  22. package/dist/commands/seed.js +55 -0
  23. package/dist/commands/uninstall.js +36 -0
  24. package/dist/commands/up.js +78 -0
  25. package/dist/commands/update-canons.js +48 -0
  26. package/dist/commands/upgrade.js +54 -0
  27. package/dist/index.js +14 -0
  28. package/dist/lib/ai-client.js +317 -0
  29. package/dist/lib/ansi.js +58 -0
  30. package/dist/lib/canon-index-generator.js +64 -0
  31. package/dist/lib/canon-index-targets.js +68 -0
  32. package/dist/lib/canon-resolver.js +262 -0
  33. package/dist/lib/canon-scaffold.js +57 -0
  34. package/dist/lib/cli-detection.js +149 -0
  35. package/dist/lib/command-context.js +23 -0
  36. package/dist/lib/compose-defaults.js +47 -0
  37. package/dist/lib/compose-env.js +24 -0
  38. package/dist/lib/compose-paths.js +36 -0
  39. package/dist/lib/compose-renderer.js +134 -0
  40. package/dist/lib/compose-validator.js +56 -0
  41. package/dist/lib/config.js +195 -0
  42. package/dist/lib/credentials.js +63 -0
  43. package/dist/lib/docker-checks.js +73 -0
  44. package/dist/lib/docker-compose.js +15 -0
  45. package/dist/lib/docker-status.js +151 -0
  46. package/dist/lib/domain-gen.js +376 -0
  47. package/dist/lib/ecosystem.js +150 -0
  48. package/dist/lib/env-file.js +77 -0
  49. package/dist/lib/errors.js +30 -0
  50. package/dist/lib/executor.js +85 -0
  51. package/dist/lib/github-auth.js +204 -0
  52. package/dist/lib/hash.js +7 -0
  53. package/dist/lib/health-checker.js +140 -0
  54. package/dist/lib/logger.js +87 -0
  55. package/dist/lib/mcp-client.js +88 -0
  56. package/dist/lib/mode.js +36 -0
  57. package/dist/lib/model-listing.js +102 -0
  58. package/dist/lib/model-registry.js +55 -0
  59. package/dist/lib/npm-operations.js +69 -0
  60. package/dist/lib/orchestrator.js +170 -0
  61. package/dist/lib/parsers.js +42 -0
  62. package/dist/lib/port-resolver.js +57 -0
  63. package/dist/lib/preconditions.js +35 -0
  64. package/dist/lib/preflight.js +88 -0
  65. package/dist/lib/process.js +6 -0
  66. package/dist/lib/prompt.js +125 -0
  67. package/dist/lib/providers.js +117 -0
  68. package/dist/lib/repo-analysis-helpers.js +379 -0
  69. package/dist/lib/repo-scanner.js +195 -0
  70. package/dist/lib/service-health.js +79 -0
  71. package/dist/lib/shell.js +49 -0
  72. package/dist/lib/state.js +38 -0
  73. package/dist/lib/update-checker.js +130 -0
  74. package/dist/lib/version.js +27 -0
  75. package/dist/stages/agent-skills-setup.js +301 -0
  76. package/dist/stages/assistant-setup.js +325 -0
  77. package/dist/stages/canon-ingest.js +249 -0
  78. package/dist/stages/canon-rebuild-graph.js +33 -0
  79. package/dist/stages/canon-rebuild-indexes.js +40 -0
  80. package/dist/stages/canon-rebuild-snapshot.js +75 -0
  81. package/dist/stages/canon-rebuild-validate.js +57 -0
  82. package/dist/stages/canon-rebuild-vectors.js +30 -0
  83. package/dist/stages/canon-scaffold.js +15 -0
  84. package/dist/stages/canon-sync.js +49 -0
  85. package/dist/stages/ci-setup.js +56 -0
  86. package/dist/stages/domain-gen.js +363 -0
  87. package/dist/stages/graph-seed.js +26 -0
  88. package/dist/stages/repo-analysis-fileonly.js +111 -0
  89. package/dist/stages/repo-analysis.js +112 -0
  90. package/dist/stages/repo-scaffold.js +110 -0
  91. package/dist/templates/canon/contracts-readme.js +39 -0
  92. package/dist/templates/canon/domain-readme.js +40 -0
  93. package/dist/templates/canon/evolution/changelog.js +53 -0
  94. package/dist/templates/canon/governance/confidence-levels.js +38 -0
  95. package/dist/templates/canon/governance/implementation-process.js +34 -0
  96. package/dist/templates/canon/governance/review-process.js +29 -0
  97. package/dist/templates/canon/governance/schema-versioning.js +25 -0
  98. package/dist/templates/canon/governance/what-enters-the-canon.js +44 -0
  99. package/dist/templates/canon/index.js +28 -0
  100. package/dist/templates/canon/knowledge-readme.js +129 -0
  101. package/dist/templates/canon/system-prompt.js +101 -0
  102. package/dist/templates/ci/architecture-merge.js +29 -0
  103. package/dist/templates/ci/architecture-pr.js +26 -0
  104. package/dist/templates/ci/index.js +7 -0
  105. package/dist/templates/consolidated.js +114 -0
  106. package/dist/templates/infra.js +90 -0
  107. package/dist/templates/mcp.js +32 -0
  108. package/install.sh +455 -0
  109. package/package.json +48 -0
@@ -0,0 +1,830 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerInitCommand = registerInitCommand;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const command_context_1 = require("../lib/command-context");
10
+ const compose_validator_1 = require("../lib/compose-validator");
11
+ const config_1 = require("../lib/config");
12
+ const ecosystem_1 = require("../lib/ecosystem");
13
+ const compose_renderer_1 = require("../lib/compose-renderer");
14
+ const errors_1 = require("../lib/errors");
15
+ const mode_1 = require("../lib/mode");
16
+ const parsers_1 = require("../lib/parsers");
17
+ const orchestrator_1 = require("../lib/orchestrator");
18
+ const github_auth_1 = require("../lib/github-auth");
19
+ const prompt_1 = require("../lib/prompt");
20
+ const preflight_1 = require("../lib/preflight");
21
+ const preconditions_1 = require("../lib/preconditions");
22
+ const shared_1 = require("./infra/shared");
23
+ const shared_2 = require("./mcp/shared");
24
+ const assistant_setup_1 = require("../stages/assistant-setup");
25
+ const canon_sync_1 = require("../stages/canon-sync");
26
+ const canon_ingest_1 = require("../stages/canon-ingest");
27
+ const graph_seed_1 = require("../stages/graph-seed");
28
+ const repo_scaffold_1 = require("../stages/repo-scaffold");
29
+ const repo_analysis_1 = require("../stages/repo-analysis");
30
+ const repo_analysis_fileonly_1 = require("../stages/repo-analysis-fileonly");
31
+ const agent_skills_setup_1 = require("../stages/agent-skills-setup");
32
+ const ci_setup_1 = require("../stages/ci-setup");
33
+ const domain_gen_1 = require("../stages/domain-gen");
34
+ const canon_resolver_1 = require("../lib/canon-resolver");
35
+ const providers_1 = require("../lib/providers");
36
+ const service_health_1 = require("../lib/service-health");
37
+ function parseComposeMode(value, fallback = 'consolidated') {
38
+ if (!value) {
39
+ return fallback;
40
+ }
41
+ if (value === 'consolidated' || value === 'split') {
42
+ return value;
43
+ }
44
+ throw new errors_1.CliError(`Invalid compose mode '${value}'. Use 'consolidated' or 'split'.`);
45
+ }
46
+ // Use parseNumber from lib/parsers instead of local toNumber
47
+ function inferComposeMode(config) {
48
+ const infraPath = node_path_1.default.resolve(config.workspaceDir, config.compose.infraFile);
49
+ const mcpPath = node_path_1.default.resolve(config.workspaceDir, config.compose.mcpFile);
50
+ if (node_fs_1.default.existsSync(infraPath) && node_fs_1.default.existsSync(mcpPath)) {
51
+ return 'split';
52
+ }
53
+ return 'consolidated';
54
+ }
55
+ async function resolveWizardSelection(options, config) {
56
+ const defaults = {
57
+ mode: (0, mode_1.parseMode)(options.mode, config.mode),
58
+ composeMode: parseComposeMode(options.composeMode, inferComposeMode(config)),
59
+ };
60
+ if (options.yes) {
61
+ if (!options.mode) {
62
+ process.stderr.write('Info: Non-interactive mode defaults to file-only. Use --mode indexed for graph/vector features.\n');
63
+ }
64
+ return {
65
+ ...defaults,
66
+ mode: options.mode ? (0, mode_1.parseMode)(options.mode) : 'file-only',
67
+ composeMode: options.composeMode ? parseComposeMode(options.composeMode) : 'consolidated',
68
+ };
69
+ }
70
+ const mode = options.mode
71
+ ? (0, mode_1.parseMode)(options.mode)
72
+ : await (0, prompt_1.promptChoice)('Select setup mode:', [
73
+ { value: 'file-only', label: 'file-only (skip infra + MCP startup)' },
74
+ { value: 'indexed', label: 'indexed (start infra + MCP and enable retrieval)' },
75
+ ], defaults.mode);
76
+ // Skip compose-mode prompt when mode is file-only — no Docker/MCP
77
+ // infrastructure is used, so compose configuration is irrelevant.
78
+ const composeMode = mode === 'file-only'
79
+ ? parseComposeMode(options.composeMode, 'consolidated')
80
+ : options.composeMode
81
+ ? parseComposeMode(options.composeMode)
82
+ : await (0, prompt_1.promptChoice)('Select compose generation mode:', [
83
+ { value: 'consolidated', label: 'consolidated (single docker-compose.yml)' },
84
+ { value: 'split', label: 'split (infra + mcp compose files)' },
85
+ ], defaults.composeMode);
86
+ return {
87
+ mode,
88
+ composeMode,
89
+ };
90
+ }
91
+ function renderMcpSnippet(provider, config) {
92
+ const workspace = config.workspaceDir;
93
+ const mcpUrl = 'http://127.0.0.1:7337/mcp';
94
+ switch (provider) {
95
+ case 'codex':
96
+ return {
97
+ filename: 'codex-mcp-config.toml',
98
+ content: [
99
+ '# Generated by collab-cli',
100
+ '[mcp_servers.collab_architecture]',
101
+ 'transport = "http"',
102
+ `url = "${mcpUrl}"`,
103
+ '',
104
+ '# If MCP_API_KEYS is configured, set a bearer token header in your client.',
105
+ '# headers = { Authorization = "Bearer <token>" }',
106
+ '',
107
+ `# Workspace: ${workspace}`,
108
+ '',
109
+ ].join('\n'),
110
+ };
111
+ case 'claude':
112
+ return {
113
+ filename: 'claude-mcp-config.json',
114
+ content: JSON.stringify({
115
+ _comment: 'Generated by collab-cli — merge into your Claude Code MCP settings',
116
+ mcpServers: {
117
+ 'collab-architecture': {
118
+ type: 'url',
119
+ url: mcpUrl,
120
+ },
121
+ },
122
+ }, null, 2) + '\n',
123
+ };
124
+ case 'gemini':
125
+ return {
126
+ filename: 'gemini-mcp-config.json',
127
+ content: JSON.stringify({
128
+ _comment: 'Generated by collab-cli — merge into your Gemini MCP settings',
129
+ mcpServers: {
130
+ 'collab-architecture': {
131
+ type: 'url',
132
+ url: mcpUrl,
133
+ },
134
+ },
135
+ }, null, 2) + '\n',
136
+ };
137
+ case 'copilot':
138
+ // Copilot doesn't use MCP config snippets
139
+ return null;
140
+ }
141
+ }
142
+ // ────────────────────────────────────────────────────────────────
143
+ // GitHub auth & business canon helpers
144
+ // ────────────────────────────────────────────────────────────────
145
+ function buildGitHubAuthStage(effectiveConfig, logger, options) {
146
+ return {
147
+ id: 'github-auth',
148
+ title: 'Authorize GitHub access',
149
+ recovery: [
150
+ 'Set COLLAB_GITHUB_CLIENT_ID env var if OAuth App is not configured.',
151
+ 'Use --github-token <token> to provide a token directly.',
152
+ 'Run collab init --resume after fixing GitHub access.',
153
+ ],
154
+ run: async () => {
155
+ // Skip if no business canon is configured — GitHub auth is only needed for private repos
156
+ if (!effectiveConfig.canons?.business) {
157
+ logger.info('No business canon configured; skipping GitHub authorization.');
158
+ return;
159
+ }
160
+ // Check for pre-existing valid token
161
+ const existing = (0, github_auth_1.loadGitHubAuth)(effectiveConfig.collabDir);
162
+ if (existing) {
163
+ const valid = await (0, github_auth_1.isGitHubAuthValid)(existing);
164
+ if (valid) {
165
+ logger.info('GitHub authorization already configured and valid.');
166
+ return;
167
+ }
168
+ logger.info('Existing GitHub token is invalid or expired. Re-authorizing...');
169
+ }
170
+ // --github-token flag: store directly
171
+ if (options.githubToken) {
172
+ (0, github_auth_1.storeGitHubToken)(effectiveConfig.collabDir, options.githubToken);
173
+ logger.info('GitHub token stored from --github-token flag.');
174
+ return;
175
+ }
176
+ // --yes without token: fail
177
+ if (options.yes) {
178
+ throw new errors_1.CliError('GitHub authorization required. Use --github-token <token> in non-interactive mode.');
179
+ }
180
+ // Interactive: run Device Flow
181
+ await (0, github_auth_1.runGitHubDeviceFlow)(effectiveConfig.collabDir, (msg) => logger.info(msg));
182
+ },
183
+ };
184
+ }
185
+ function parseBusinessCanonOption(value) {
186
+ if (!value || value === 'none' || value === 'skip') {
187
+ return undefined;
188
+ }
189
+ if (!value.includes('/')) {
190
+ throw new errors_1.CliError(`Invalid business canon format "${value}". Use "owner/repo" or "none" to skip.`);
191
+ }
192
+ return {
193
+ business: {
194
+ repo: value,
195
+ branch: 'main',
196
+ localDir: 'business',
197
+ },
198
+ };
199
+ }
200
+ async function resolveBusinessCanon(options, logger) {
201
+ // CLI flag takes priority
202
+ if (options.businessCanon) {
203
+ return parseBusinessCanonOption(options.businessCanon);
204
+ }
205
+ // --yes without --business-canon: mandatory error
206
+ if (options.yes) {
207
+ throw new errors_1.CliError('--business-canon is required with --yes. Use --business-canon owner/repo or --business-canon none.');
208
+ }
209
+ // Interactive prompt
210
+ const repo = await (0, prompt_1.promptText)('Business architecture canon repo (owner/repo, empty to skip):');
211
+ if (!repo) {
212
+ logger.info('No business canon configured.');
213
+ return undefined;
214
+ }
215
+ if (!repo.includes('/')) {
216
+ throw new errors_1.CliError(`Invalid format "${repo}". Use "owner/repo".`);
217
+ }
218
+ const branch = await (0, prompt_1.promptText)('Business canon branch:', 'main');
219
+ return {
220
+ business: {
221
+ repo,
222
+ branch: branch || 'main',
223
+ localDir: 'business',
224
+ },
225
+ };
226
+ }
227
+ // ────────────────────────────────────────────────────────────────
228
+ // Shared inline stages used by both pipelines
229
+ // ────────────────────────────────────────────────────────────────
230
+ function buildPreflightStage(executor, logger, mode) {
231
+ return {
232
+ id: 'preflight',
233
+ title: 'Run preflight checks',
234
+ recovery: [
235
+ 'Install missing dependencies reported by preflight.',
236
+ 'Run collab init --resume after fixing prerequisites.',
237
+ ],
238
+ run: () => {
239
+ const checks = (0, preflight_1.runPreflightChecks)(executor, { mode });
240
+ (0, preflight_1.assertPreflightChecks)(checks, logger);
241
+ },
242
+ };
243
+ }
244
+ function buildConfigStage(effectiveConfig, executor, logger, configExistedBefore, force) {
245
+ return {
246
+ id: 'environment-setup',
247
+ title: 'Write local collab configuration',
248
+ recovery: [
249
+ 'Verify write permissions for .collab and workspace directory.',
250
+ 'Run collab init --resume once permissions are fixed.',
251
+ ],
252
+ run: () => {
253
+ if (configExistedBefore && !force) {
254
+ logger.info('Existing configuration detected; preserving it. Use --force to overwrite.');
255
+ return;
256
+ }
257
+ executor.ensureDirectory(effectiveConfig.collabDir);
258
+ executor.writeFile(effectiveConfig.configFile, `${(0, config_1.serializeUserConfig)(effectiveConfig)}\n`, { description: 'write collab config' });
259
+ },
260
+ };
261
+ }
262
+ function parseRepos(value) {
263
+ if (!value)
264
+ return null;
265
+ return value.split(',').map((r) => r.trim()).filter(Boolean);
266
+ }
267
+ async function resolveWorkspace(workspaceDir, options, logger) {
268
+ const name = (0, config_1.deriveWorkspaceName)(workspaceDir);
269
+ // Explicit --repos flag takes priority
270
+ const explicit = parseRepos(options.repos);
271
+ if (explicit && explicit.length > 0) {
272
+ const type = explicit.length >= 2 ? 'multi-repo' : 'mono-repo';
273
+ logger.info(`Workspace mode: ${explicit.length} repo(s) specified: ${explicit.join(', ')}`);
274
+ return { name, type, repos: explicit };
275
+ }
276
+ // Auto-detect workspace layout
277
+ const layout = (0, config_1.detectWorkspaceLayout)(workspaceDir);
278
+ if (layout) {
279
+ if (options.yes) {
280
+ logger.info(`Workspace auto-detected (${layout.type}): ${layout.repos.length} repo(s) found: ${layout.repos.join(', ')}`);
281
+ return { name, type: layout.type, repos: layout.repos };
282
+ }
283
+ // Interactive: for multi-repo let user confirm/select repos
284
+ if (layout.type === 'multi-repo') {
285
+ const selected = await (0, prompt_1.promptMultiSelect)('This directory contains multiple git repositories. Select repos to include:', layout.repos.map((r) => ({ value: r, label: r })), layout.repos);
286
+ if (selected.length === 0)
287
+ return null;
288
+ return { name, type: 'multi-repo', repos: selected };
289
+ }
290
+ // mono-repo auto-detected
291
+ logger.info(`Mono-repo workspace detected: ${layout.repos.join(', ')}`);
292
+ return { name, type: 'mono-repo', repos: layout.repos };
293
+ }
294
+ // No repos found
295
+ if (options.yes) {
296
+ // Non-interactive with no repos → treat cwd as mono-repo
297
+ logger.info('No repos discovered; initializing as mono-repo workspace.');
298
+ return { name, type: 'mono-repo', repos: ['.'] };
299
+ }
300
+ return null;
301
+ }
302
+ // ────────────────────────────────────────────────────────────────
303
+ // Workspace pipeline builders
304
+ // ────────────────────────────────────────────────────────────────
305
+ function buildWorkspaceStages(effectiveConfig, executor, logger, configExistedBefore, options) {
306
+ return [
307
+ buildPreflightStage(executor, logger),
308
+ buildConfigStage(effectiveConfig, executor, logger, configExistedBefore, options.force),
309
+ buildGitHubAuthStage(effectiveConfig, logger, options),
310
+ assistant_setup_1.assistantSetupStage,
311
+ canon_sync_1.canonSyncStage,
312
+ ];
313
+ }
314
+ function buildPerRepoStages(mode) {
315
+ const analysisStage = mode === 'indexed' ? repo_analysis_1.repoAnalysisStage : repo_analysis_fileonly_1.repoAnalysisFileOnlyStage;
316
+ return [
317
+ repo_scaffold_1.repoScaffoldStage,
318
+ analysisStage,
319
+ ci_setup_1.ciSetupStage,
320
+ agent_skills_setup_1.agentSkillsSetupStage,
321
+ ];
322
+ }
323
+ function buildInfraStages(effectiveConfig, executor, logger, options, composeMode) {
324
+ const health = {
325
+ timeoutMs: (0, parsers_1.parseNumber)(options.timeoutMs, 5_000),
326
+ retries: (0, parsers_1.parseNumber)(options.retries, 15),
327
+ retryDelayMs: (0, parsers_1.parseNumber)(options.retryDelayMs, 2_000),
328
+ };
329
+ return [
330
+ {
331
+ id: 'compose-generation',
332
+ title: 'Generate and validate compose files',
333
+ recovery: [
334
+ 'Run collab compose validate to inspect configuration errors.',
335
+ 'Run collab init --resume after fixing compose inputs.',
336
+ ],
337
+ run: () => {
338
+ const generation = (0, compose_renderer_1.generateComposeFiles)({
339
+ config: effectiveConfig,
340
+ mode: composeMode,
341
+ outputDirectory: options.outputDir,
342
+ logger,
343
+ executor,
344
+ });
345
+ for (const warning of generation.driftWarnings) {
346
+ logger.warn(warning);
347
+ }
348
+ (0, compose_validator_1.assertComposeFilesValid)(generation.files.map((file) => file.filePath), effectiveConfig.workspaceDir, executor);
349
+ },
350
+ },
351
+ {
352
+ id: 'infra-start',
353
+ title: 'Start infrastructure services',
354
+ recovery: [
355
+ 'Run collab infra status to inspect Qdrant and Nebula.',
356
+ 'Run collab init --resume after infra services are healthy.',
357
+ ],
358
+ run: async () => {
359
+ // Check if infra is already running (e.g. from a workspace-level init)
360
+ const env = (0, service_health_1.loadRuntimeEnv)(effectiveConfig);
361
+ const probe = await (0, service_health_1.waitForInfraHealth)(env, { ...health, retries: 1 });
362
+ if (probe.ok) {
363
+ logger.info('Infrastructure already running — skipping docker compose up.');
364
+ (0, service_health_1.logServiceHealth)(logger, 'infra health', probe);
365
+ return;
366
+ }
367
+ const selection = (0, shared_1.resolveInfraComposeFile)(effectiveConfig, options.outputDir, undefined);
368
+ await (0, shared_1.runInfraCompose)(logger, executor, effectiveConfig, selection, 'up', { health });
369
+ },
370
+ },
371
+ {
372
+ id: 'mcp-start',
373
+ title: 'Start MCP service',
374
+ recovery: [
375
+ 'Run collab mcp status to inspect MCP runtime.',
376
+ 'Run collab init --resume after MCP health endpoint responds.',
377
+ ],
378
+ run: async () => {
379
+ // Check if MCP is already running (e.g. from a workspace-level init)
380
+ const env = (0, service_health_1.loadRuntimeEnv)(effectiveConfig);
381
+ const probe = await (0, service_health_1.waitForMcpHealth)(env, { ...health, retries: 1 });
382
+ if (probe.ok) {
383
+ logger.info('MCP service already running — skipping docker compose up.');
384
+ (0, service_health_1.logServiceHealth)(logger, 'mcp health', probe);
385
+ return;
386
+ }
387
+ const selection = (0, shared_2.resolveMcpComposeFile)(effectiveConfig, options.outputDir, undefined);
388
+ await (0, shared_2.runMcpCompose)(logger, executor, effectiveConfig, selection, 'up', { health });
389
+ },
390
+ },
391
+ {
392
+ id: 'mcp-client-config',
393
+ title: 'Generate MCP client config snippets',
394
+ recovery: [
395
+ 'Verify permissions in .collab directory.',
396
+ 'Run collab init --resume to regenerate MCP config snippets.',
397
+ ],
398
+ run: () => {
399
+ if (options.skipMcpSnippets) {
400
+ logger.info('Skipping MCP snippet generation by user choice.');
401
+ return;
402
+ }
403
+ const enabled = (0, providers_1.getEnabledProviders)(effectiveConfig);
404
+ if (enabled.length === 0) {
405
+ logger.info('No providers configured; skipping MCP snippet generation.');
406
+ return;
407
+ }
408
+ for (const provider of enabled) {
409
+ const snippet = renderMcpSnippet(provider, effectiveConfig);
410
+ if (!snippet)
411
+ continue;
412
+ const target = node_path_1.default.join(effectiveConfig.collabDir, snippet.filename);
413
+ executor.writeFile(target, snippet.content, {
414
+ description: `write ${providers_1.PROVIDER_DEFAULTS[provider].label} MCP config snippet`,
415
+ });
416
+ }
417
+ logger.info(`Generated MCP snippets for: ${enabled.map((k) => providers_1.PROVIDER_DEFAULTS[k].label).join(', ')}`);
418
+ },
419
+ },
420
+ graph_seed_1.graphSeedStage,
421
+ canon_ingest_1.canonIngestStage,
422
+ ];
423
+ }
424
+ // ────────────────────────────────────────────────────────────────
425
+ // File-only pipeline (8 stages)
426
+ // ────────────────────────────────────────────────────────────────
427
+ function buildFileOnlyPipeline(effectiveConfig, executor, logger, configExistedBefore, options) {
428
+ return [
429
+ buildPreflightStage(executor, logger), // 1
430
+ buildConfigStage(effectiveConfig, executor, logger, configExistedBefore, options.force), // 2
431
+ buildGitHubAuthStage(effectiveConfig, logger, options), // 3
432
+ assistant_setup_1.assistantSetupStage, // 4
433
+ canon_sync_1.canonSyncStage, // 5
434
+ repo_scaffold_1.repoScaffoldStage, // 6
435
+ repo_analysis_fileonly_1.repoAnalysisFileOnlyStage, // 7
436
+ ci_setup_1.ciSetupStage, // 8
437
+ agent_skills_setup_1.agentSkillsSetupStage, // 9
438
+ ];
439
+ }
440
+ // ────────────────────────────────────────────────────────────────
441
+ // Indexed pipeline (15 stages)
442
+ // ────────────────────────────────────────────────────────────────
443
+ function buildIndexedPipeline(effectiveConfig, executor, logger, configExistedBefore, options, composeMode) {
444
+ return [
445
+ // Phase A — Local setup (shared with file-only)
446
+ buildPreflightStage(executor, logger, 'indexed'), // 1
447
+ buildConfigStage(effectiveConfig, executor, logger, configExistedBefore, options.force), // 2
448
+ buildGitHubAuthStage(effectiveConfig, logger, options), // 3
449
+ assistant_setup_1.assistantSetupStage, // 4
450
+ canon_sync_1.canonSyncStage, // 5
451
+ repo_scaffold_1.repoScaffoldStage, // 6
452
+ repo_analysis_1.repoAnalysisStage, // 7
453
+ ci_setup_1.ciSetupStage, // 8
454
+ agent_skills_setup_1.agentSkillsSetupStage, // 9
455
+ // Phase B — Infrastructure + Phase C — Ingestion (9-14)
456
+ ...buildInfraStages(effectiveConfig, executor, logger, options, composeMode),
457
+ ];
458
+ }
459
+ // ────────────────────────────────────────────────────────────────
460
+ // Standalone infra phase (collab init infra)
461
+ // ────────────────────────────────────────────────────────────────
462
+ async function runInfraOnly(context, options) {
463
+ // Build an indexed config — infra always implies indexed mode.
464
+ // If a config already exists we honour it; otherwise bootstrap one.
465
+ const effectiveConfig = {
466
+ ...(0, config_1.defaultCollabConfig)(context.config.workspaceDir),
467
+ ...context.config,
468
+ mode: 'indexed',
469
+ };
470
+ // Persist config so subsequent commands (infra status, mcp status) work.
471
+ const configExists = node_fs_1.default.existsSync(effectiveConfig.configFile);
472
+ if (!configExists) {
473
+ context.executor.ensureDirectory(effectiveConfig.collabDir);
474
+ context.executor.writeFile(effectiveConfig.configFile, `${(0, config_1.serializeUserConfig)(effectiveConfig)}\n`, { description: 'write collab config (infra bootstrap)' });
475
+ }
476
+ const composeMode = parseComposeMode(options.composeMode, inferComposeMode(effectiveConfig));
477
+ context.logger.phaseHeader('Infrastructure', 'Docker + MCP services');
478
+ const infraStages = buildInfraStages(effectiveConfig, context.executor, context.logger, options, composeMode);
479
+ await (0, orchestrator_1.runOrchestration)({
480
+ workflowId: 'init:infra',
481
+ config: effectiveConfig,
482
+ executor: context.executor,
483
+ logger: context.logger,
484
+ resume: options.resume,
485
+ mode: 'indexed (infra)',
486
+ stageOptions: { outputDir: options.outputDir },
487
+ }, infraStages);
488
+ // Summary
489
+ context.logger.phaseHeader('Infrastructure Ready');
490
+ context.logger.summaryFooter([
491
+ { label: 'Phase', value: 'infra only' },
492
+ { label: 'Compose mode', value: composeMode },
493
+ { label: 'Dry-run', value: context.executor.dryRun ? 'yes' : 'no' },
494
+ { label: 'Config', value: effectiveConfig.configFile },
495
+ ]);
496
+ }
497
+ // ────────────────────────────────────────────────────────────────
498
+ // Repo domain generation (collab init --repo=<package>)
499
+ // ────────────────────────────────────────────────────────────────
500
+ /**
501
+ * Resolves and validates the path to a repository package.
502
+ *
503
+ * Resolution order:
504
+ * 1. Absolute path → use directly
505
+ * 2. Relative path from workspace dir → resolve
506
+ * 3. Name within workspace → join with workspaceDir
507
+ *
508
+ * @throws {CliError} When the path is not found or is not a directory.
509
+ */
510
+ function resolveRepoPath(repoValue, config) {
511
+ const isDirectory = (p) => {
512
+ try {
513
+ return node_fs_1.default.statSync(p).isDirectory();
514
+ }
515
+ catch {
516
+ return false;
517
+ }
518
+ };
519
+ // 1. Absolute path
520
+ if (node_path_1.default.isAbsolute(repoValue)) {
521
+ if (!node_fs_1.default.existsSync(repoValue)) {
522
+ throw new errors_1.CliError(`Repository path not found: ${repoValue}`);
523
+ }
524
+ if (!isDirectory(repoValue)) {
525
+ throw new errors_1.CliError(`Repository path is not a directory: ${repoValue}`);
526
+ }
527
+ return repoValue;
528
+ }
529
+ // 2. Relative path from workspace dir (respects --cwd)
530
+ const fromCwd = node_path_1.default.resolve(config.workspaceDir, repoValue);
531
+ if (isDirectory(fromCwd)) {
532
+ return fromCwd;
533
+ }
534
+ // 3. Name within workspace
535
+ const fromWorkspace = node_path_1.default.join(config.workspaceDir, repoValue);
536
+ if (isDirectory(fromWorkspace)) {
537
+ return fromWorkspace;
538
+ }
539
+ throw new errors_1.CliError(`Repository "${repoValue}" not found.\n` +
540
+ `Searched:\n` +
541
+ ` - ${fromCwd}\n` +
542
+ ` - ${fromWorkspace}\n` +
543
+ `Provide an absolute path, a relative path from cwd, or a repo name within your workspace.`);
544
+ }
545
+ async function runRepoDomainGeneration(context, options) {
546
+ const repoValue = options.repo;
547
+ // Build a minimal config — reuse existing if available
548
+ const effectiveConfig = {
549
+ ...(0, config_1.defaultCollabConfig)(context.config.workspaceDir),
550
+ ...context.config,
551
+ };
552
+ // Resolve business canon if passed via flag (but don't require it for file-only)
553
+ const canons = options.businessCanon ? parseBusinessCanonOption(options.businessCanon) : undefined;
554
+ if (canons) {
555
+ effectiveConfig.canons = canons;
556
+ }
557
+ // Store GitHub token if provided (required for indexed push/sync)
558
+ if (options.githubToken) {
559
+ if (context.executor.dryRun) {
560
+ context.logger.info('[dry-run] Would store GitHub token from --github-token flag.');
561
+ }
562
+ else {
563
+ (0, github_auth_1.storeGitHubToken)(effectiveConfig.collabDir, options.githubToken);
564
+ context.logger.info('GitHub token stored from --github-token flag.');
565
+ }
566
+ }
567
+ // Resolve mode
568
+ let mode;
569
+ if (options.mode) {
570
+ mode = (0, mode_1.parseMode)(options.mode);
571
+ }
572
+ else if (options.yes) {
573
+ mode = 'file-only';
574
+ }
575
+ else {
576
+ mode = await (0, prompt_1.promptChoice)('Select domain generation mode:', [
577
+ { value: 'file-only', label: 'file-only (write domain files to local repo only)' },
578
+ { value: 'indexed', label: 'indexed (write to business canon + ingest into MCP)' },
579
+ ], 'file-only');
580
+ }
581
+ // Validate prerequisites
582
+ if (mode === 'indexed' && !(0, canon_resolver_1.isBusinessCanonConfigured)(effectiveConfig)) {
583
+ throw new errors_1.CliError('Business canon is required for indexed mode. ' +
584
+ 'Use --business-canon owner/repo to configure it, or use --mode file-only.');
585
+ }
586
+ // Resolve repo path
587
+ const repoPath = resolveRepoPath(repoValue, effectiveConfig);
588
+ const repoName = node_path_1.default.basename(repoPath);
589
+ context.logger.phaseHeader('Domain Generation', `${repoName} (${mode})`);
590
+ // Build pipeline
591
+ const stages = mode === 'file-only'
592
+ ? (0, domain_gen_1.buildFileOnlyDomainPipeline)()
593
+ : (0, domain_gen_1.buildIndexedDomainPipeline)();
594
+ // Execute
595
+ await (0, orchestrator_1.runOrchestration)({
596
+ workflowId: 'init:repo-domain',
597
+ config: effectiveConfig,
598
+ executor: context.executor,
599
+ logger: context.logger,
600
+ resume: options.resume,
601
+ mode: `${mode} (repo domain)`,
602
+ stageOptions: {
603
+ _repoPath: repoPath,
604
+ yes: options.yes,
605
+ providers: options.providers,
606
+ },
607
+ }, stages);
608
+ // Summary
609
+ context.logger.phaseHeader('Domain Generation Complete');
610
+ context.logger.summaryFooter([
611
+ { label: 'Mode', value: mode },
612
+ { label: 'Repository', value: repoName },
613
+ { label: 'Dry-run', value: context.executor.dryRun ? 'yes' : 'no' },
614
+ ]);
615
+ }
616
+ // ────────────────────────────────────────────────────────────────
617
+ // Command registration
618
+ // ────────────────────────────────────────────────────────────────
619
+ function registerInitCommand(program) {
620
+ program
621
+ .command('init')
622
+ .description('Run onboarding wizard and orchestrate setup stages')
623
+ .argument('[phase]', 'Optional phase to run in isolation (e.g. "infra")')
624
+ .option('-f, --force', 'Overwrite existing .collab/config.json with new wizard selection')
625
+ .option('--yes', 'Accept wizard defaults and run non-interactively')
626
+ .option('--resume', 'Resume from the last incomplete wizard stage')
627
+ .option('--mode <mode>', 'Wizard mode: file-only|indexed')
628
+ .option('--compose-mode <mode>', 'Compose mode: consolidated|split')
629
+ .option('--output-dir <directory>', 'Directory used to write compose outputs')
630
+ .option('--repos <list>', 'Comma-separated repo directories for workspace mode')
631
+ .option('--repo <package>', 'Generate domain definition from package analysis')
632
+ .option('--skip-mcp-snippets', 'Skip MCP client config snippet generation')
633
+ .option('--skip-analysis', 'Skip AI-powered repository analysis stage')
634
+ .option('--skip-ci', 'Skip CI workflow generation')
635
+ .option('--providers <list>', 'Comma-separated AI provider list (codex,claude,gemini,copilot)')
636
+ .option('--business-canon <owner/repo>', 'Business canon repo (owner/repo or "none" to skip)')
637
+ .option('--github-token <token>', 'GitHub token for non-interactive mode')
638
+ .option('--timeout-ms <ms>', 'Per-check timeout in milliseconds', '5000')
639
+ .option('--retries <count>', 'Health check retries', '15')
640
+ .option('--retry-delay-ms <ms>', 'Delay between retries in milliseconds', '2000')
641
+ .addHelpText('after', `
642
+ Examples:
643
+ collab init
644
+ collab init --yes
645
+ collab init --yes --mode file-only
646
+ collab init --yes --mode indexed
647
+ collab init --repos api,web,shared --yes
648
+ collab init --repo collab-chat-ai-pkg --mode file-only
649
+ collab init --repo collab-chat-ai-pkg --mode indexed
650
+ collab init --resume
651
+ collab init infra
652
+ collab init infra --resume
653
+ `)
654
+ .action(async (phase, options, command) => {
655
+ const context = (0, command_context_1.createCommandContext)(command);
656
+ (0, preconditions_1.ensureWritableDirectory)(context.config.workspaceDir);
657
+ // ── Phase shortcut: collab init infra ───────────────────
658
+ if (phase === 'infra') {
659
+ await runInfraOnly(context, options);
660
+ return;
661
+ }
662
+ if (phase) {
663
+ throw new errors_1.CliError(`Unknown init phase "${phase}". Available phases: infra`);
664
+ }
665
+ // ── Repo domain generation: collab init --repo <pkg> ───
666
+ if (options.repo) {
667
+ await runRepoDomainGeneration(context, options);
668
+ // Ecosystem compatibility checks (same as full wizard)
669
+ const compatibility = await (0, ecosystem_1.checkEcosystemCompatibility)(context.config, {
670
+ dryRun: context.executor.dryRun,
671
+ });
672
+ for (const check of compatibility) {
673
+ const prefix = check.ok ? '[PASS]' : '[WARN]';
674
+ context.logger.result(`${prefix} ${check.id}: ${check.detail}`);
675
+ if (!check.ok && check.fix)
676
+ context.logger.result(` fix: ${check.fix}`);
677
+ }
678
+ return;
679
+ }
680
+ // ── Full wizard flow ────────────────────────────────────
681
+ const configExistedBefore = node_fs_1.default.existsSync(context.config.configFile);
682
+ if (options.force) {
683
+ context.logger.warn('Force mode enabled: configuration will be overwritten with wizard selections.');
684
+ }
685
+ // ── Step 1: Configuration wizard ────────────────────────
686
+ context.logger.phaseHeader('collab init', 'Configuration');
687
+ const selections = await resolveWizardSelection(options, context.config);
688
+ const preserveExisting = configExistedBefore && !options.force;
689
+ const effectiveConfig = {
690
+ ...(0, config_1.defaultCollabConfig)(context.config.workspaceDir),
691
+ ...context.config,
692
+ mode: preserveExisting ? context.config.mode : selections.mode,
693
+ };
694
+ // ── Step 2: Business canon configuration ──────────────────
695
+ const canons = await resolveBusinessCanon(options, context.logger);
696
+ if (canons) {
697
+ effectiveConfig.canons = canons;
698
+ }
699
+ const stageOptions = {
700
+ yes: options.yes,
701
+ providers: options.providers,
702
+ outputDir: options.outputDir,
703
+ skipAnalysis: options.skipAnalysis,
704
+ skipCi: options.skipCi,
705
+ };
706
+ // ── Workspace detection ───────────────────────────────────
707
+ // Prefer persisted workspace config when it exists (unless
708
+ // --force or explicit --repos override is provided).
709
+ const ws = !options.force && !options.repos && context.config.workspace
710
+ ? context.config.workspace
711
+ : await resolveWorkspace(context.config.workspaceDir, options, context.logger);
712
+ if (ws) {
713
+ // ── WORKSPACE MODE ────────────────────────────────────
714
+ effectiveConfig.workspace = { name: ws.name, type: ws.type, repos: ws.repos };
715
+ effectiveConfig.compose = {
716
+ ...effectiveConfig.compose,
717
+ projectName: `collab-${ws.name}`,
718
+ };
719
+ const repoConfigs = (0, config_1.resolveRepoConfigs)(effectiveConfig);
720
+ // Phase W — workspace-level stages
721
+ context.logger.phaseHeader('Workspace Setup', `${ws.repos.length} repositories (${ws.type})`);
722
+ const workspaceStages = buildWorkspaceStages(effectiveConfig, context.executor, context.logger, configExistedBefore, options);
723
+ await (0, orchestrator_1.runOrchestration)({
724
+ workflowId: 'init',
725
+ config: effectiveConfig,
726
+ executor: context.executor,
727
+ logger: context.logger,
728
+ resume: options.resume,
729
+ mode: `${selections.mode} (workspace)`,
730
+ stageOptions,
731
+ }, workspaceStages);
732
+ // Phase R — per-repo stages
733
+ context.logger.phaseHeader('Repository Analysis', `${selections.mode} mode`);
734
+ const perRepoStages = buildPerRepoStages(selections.mode);
735
+ for (const [i, rc] of repoConfigs.entries()) {
736
+ context.logger.repoHeader(rc.name, i + 1, repoConfigs.length);
737
+ await (0, orchestrator_1.runPerRepoOrchestration)({
738
+ workflowId: 'init',
739
+ config: effectiveConfig,
740
+ executor: context.executor,
741
+ logger: context.logger,
742
+ resume: options.resume,
743
+ stageOptions,
744
+ }, rc, perRepoStages);
745
+ }
746
+ // Phase I — infra stages (indexed only)
747
+ if (selections.mode === 'indexed') {
748
+ context.logger.phaseHeader('Infrastructure', 'Docker + MCP services');
749
+ const infraStages = buildInfraStages(effectiveConfig, context.executor, context.logger, options, selections.composeMode);
750
+ await (0, orchestrator_1.runOrchestration)({
751
+ workflowId: 'init:infra',
752
+ config: effectiveConfig,
753
+ executor: context.executor,
754
+ logger: context.logger,
755
+ resume: options.resume,
756
+ mode: 'indexed (infra)',
757
+ stageOptions,
758
+ }, infraStages);
759
+ }
760
+ }
761
+ else {
762
+ // ── SINGLE-REPO MODE (unchanged) ──────────────────────
763
+ context.logger.phaseHeader('Project Setup', selections.mode);
764
+ const stages = selections.mode === 'file-only'
765
+ ? buildFileOnlyPipeline(effectiveConfig, context.executor, context.logger, configExistedBefore, options)
766
+ : buildIndexedPipeline(effectiveConfig, context.executor, context.logger, configExistedBefore, options, selections.composeMode);
767
+ await (0, orchestrator_1.runOrchestration)({
768
+ workflowId: 'init',
769
+ config: effectiveConfig,
770
+ executor: context.executor,
771
+ logger: context.logger,
772
+ resume: options.resume,
773
+ mode: selections.mode,
774
+ stageOptions,
775
+ }, stages);
776
+ }
777
+ // ── Summary ───────────────────────────────────────────
778
+ context.logger.phaseHeader('Setup Complete');
779
+ const enabledProviders = (0, providers_1.getEnabledProviders)(effectiveConfig);
780
+ const providerLabel = enabledProviders.length > 0
781
+ ? enabledProviders.map((k) => providers_1.PROVIDER_DEFAULTS[k].label).join(', ')
782
+ : '(none configured)';
783
+ const summaryEntries = [
784
+ { label: 'Mode', value: selections.mode },
785
+ { label: 'Dry-run', value: context.executor.dryRun ? 'yes' : 'no' },
786
+ { label: 'Config', value: effectiveConfig.configFile },
787
+ { label: 'Providers', value: providerLabel },
788
+ ];
789
+ if (ws) {
790
+ summaryEntries.splice(1, 0, { label: 'Workspace', value: `${ws.name} (${ws.type})` }, { label: 'Repos', value: ws.repos.join(', ') });
791
+ }
792
+ if (selections.mode === 'indexed') {
793
+ summaryEntries.splice(ws ? 3 : 1, 0, { label: 'Compose mode', value: selections.composeMode });
794
+ }
795
+ context.logger.summaryFooter(summaryEntries);
796
+ // Ecosystem compatibility checks
797
+ const compatibility = await (0, ecosystem_1.checkEcosystemCompatibility)(effectiveConfig, {
798
+ dryRun: context.executor.dryRun,
799
+ });
800
+ for (const check of compatibility) {
801
+ const prefix = check.ok ? '[PASS]' : '[WARN]';
802
+ context.logger.result(`${prefix} ${check.id}: ${check.detail}`);
803
+ if (!check.ok && check.fix) {
804
+ context.logger.result(` fix: ${check.fix}`);
805
+ }
806
+ }
807
+ // ── Next steps guidance ──────────────────────────────
808
+ context.logger.info('');
809
+ context.logger.info('Next steps:');
810
+ if (selections.mode === 'file-only') {
811
+ context.logger.info(' - Upgrade to indexed mode for graph/vector features:');
812
+ context.logger.info(' collab init --mode indexed');
813
+ }
814
+ if (selections.mode === 'indexed') {
815
+ context.logger.info(' - Populate graph and vector stores:');
816
+ context.logger.info(' collab canon rebuild --confirm');
817
+ }
818
+ if (ws) {
819
+ context.logger.info(' - Initialize domain repos:');
820
+ context.logger.info(' collab init --repo=<package-name>');
821
+ }
822
+ context.logger.info(' - Verify full setup health:');
823
+ context.logger.info(' collab doctor');
824
+ context.logger.info(' - Finalize and archive when done:');
825
+ context.logger.info(' collab end');
826
+ if (!options.force && configExistedBefore) {
827
+ context.logger.debug('Existing configuration was reused.');
828
+ }
829
+ });
830
+ }