@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,301 @@
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.agentSkillsSetupStage = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const canon_resolver_1 = require("../lib/canon-resolver");
10
+ const orchestrator_1 = require("../lib/orchestrator");
11
+ const providers_1 = require("../lib/providers");
12
+ /**
13
+ * Agent prompt files from collab-architecture/prompts/agents/.
14
+ * Each entry maps a skill name to its canon path.
15
+ */
16
+ const AGENT_PROMPTS = [
17
+ {
18
+ name: 'phase-1-survey',
19
+ description: 'GOV-R-001 Phase 1 — Explore codebase, map files, check duplication, propose design.',
20
+ canonPath: 'prompts/agents/phase-1-survey.md',
21
+ },
22
+ {
23
+ name: 'phase-2-change-plan',
24
+ description: 'GOV-R-001 Phase 2 — Produce a detailed implementation plan for the change.',
25
+ canonPath: 'prompts/agents/phase-2-change-plan.md',
26
+ },
27
+ {
28
+ name: 'phase-3-implementation',
29
+ description: 'GOV-R-001 Phase 3 — Execute the implementation following the approved change plan.',
30
+ canonPath: 'prompts/agents/phase-3-implementation.md',
31
+ },
32
+ {
33
+ name: 'phase-4-repo-hygiene',
34
+ description: 'GOV-R-001 Phase 4 — Verify repo hygiene: tests, lint, docs, dead code.',
35
+ canonPath: 'prompts/agents/phase-4-repo-hygiene.md',
36
+ },
37
+ {
38
+ name: 'phase-5-canon-sync',
39
+ description: 'GOV-R-001 Phase 5 — Sync changes back to canonical architecture.',
40
+ canonPath: 'prompts/agents/phase-5-canon-sync.md',
41
+ },
42
+ {
43
+ name: 'architecture-reviewer',
44
+ description: 'Thematic agent — Reviews code against canonical rules and patterns.',
45
+ canonPath: 'prompts/agents/architecture-reviewer.md',
46
+ },
47
+ {
48
+ name: 'drift-detector',
49
+ description: 'Thematic agent — Detects drift between code and canonical architecture.',
50
+ canonPath: 'prompts/agents/drift-detector.md',
51
+ },
52
+ {
53
+ name: 'pattern-extractor',
54
+ description: 'Thematic agent — Extracts reusable patterns from implementation.',
55
+ canonPath: 'prompts/agents/pattern-extractor.md',
56
+ },
57
+ ];
58
+ // ────────────────────────────────────────────────────────────────
59
+ // Mode-aware architecture access preambles
60
+ // ────────────────────────────────────────────────────────────────
61
+ function buildArchitectureAccessBlock(mode, repoConfig, workspaceConfig) {
62
+ const lines = [];
63
+ if (mode === 'indexed') {
64
+ lines.push('## Architecture Access (MCP)', '', 'This project uses an MCP server for architecture retrieval.', '', '**MCP Tools (collab-architecture server):**', '- `context.scopes.list.v2` — List available scopes and collections', '- `context.vector.search.v2` — Semantic search across architecture docs', '- `context.graph.degree.search.v2` — Graph traversal for related concepts', '', '**Also consult local files:**', '- `docs/ai/` — Quick reference helpers (start here for fast context)', '- `docs/architecture/repo/` — Project-specific canons and decisions', '- `.agents/skills/` — Governance phase guidance');
65
+ }
66
+ else {
67
+ lines.push('## Architecture Access', '', 'Read architecture context from local files:');
68
+ if (repoConfig && workspaceConfig) {
69
+ lines.push('- `../../docs/architecture/uxmaltech/` — Institutional canon (collab-architecture)');
70
+ }
71
+ else {
72
+ lines.push('- `docs/architecture/uxmaltech/` — Institutional canon (collab-architecture)');
73
+ }
74
+ lines.push('- `docs/architecture/repo/` — Project-specific canons and decisions', '- `docs/ai/` — Quick reference helpers (start here for fast context)');
75
+ }
76
+ // Append workspace context when running inside a multi-repo workspace
77
+ if (repoConfig && workspaceConfig) {
78
+ const otherRepos = workspaceConfig.repos.filter((r) => r !== repoConfig.name);
79
+ lines.push('', '## Workspace Context', '', `This repo (\`${repoConfig.name}\`) is part of a multi-repo workspace.`);
80
+ if (otherRepos.length > 0) {
81
+ lines.push(`Other repos: ${otherRepos.map((r) => '`' + r + '`').join(', ')}`);
82
+ lines.push(`Access sibling repos via: ${otherRepos.map((r) => '`../../' + r + '/`').join(', ')}`);
83
+ }
84
+ if (repoConfig) {
85
+ lines.push('Shared canonical architecture: `../../docs/architecture/uxmaltech/`');
86
+ }
87
+ }
88
+ return lines.join('\n');
89
+ }
90
+ // ────────────────────────────────────────────────────────────────
91
+ // Generator functions
92
+ // ────────────────────────────────────────────────────────────────
93
+ /**
94
+ * Generates Agent Skills Spec SKILL.md files for Claude, Codex, and Gemini.
95
+ * Format: .agents/skills/<name>/SKILL.md with YAML frontmatter.
96
+ */
97
+ function generateAgentSkillsSpec(ctx, mode, repoConfig, workspaceConfig) {
98
+ const skillsBaseDir = node_path_1.default.join((0, orchestrator_1.getRepoBaseDir)(ctx), '.agents', 'skills');
99
+ const preamble = buildArchitectureAccessBlock(mode, repoConfig, workspaceConfig);
100
+ let written = 0;
101
+ for (const agent of AGENT_PROMPTS) {
102
+ const content = (0, canon_resolver_1.resolveCanonFile)(agent.canonPath);
103
+ if (!content) {
104
+ ctx.logger.debug(`Skipping skill ${agent.name}: prompt not found at ${agent.canonPath}`);
105
+ continue;
106
+ }
107
+ const skillDir = node_path_1.default.join(skillsBaseDir, agent.name);
108
+ const skillFile = node_path_1.default.join(skillDir, 'SKILL.md');
109
+ // Don't overwrite existing skill files
110
+ if (node_fs_1.default.existsSync(skillFile)) {
111
+ ctx.logger.debug(`Skill already exists, skipping: ${agent.name}`);
112
+ continue;
113
+ }
114
+ const skillContent = [
115
+ '---',
116
+ `name: ${agent.name}`,
117
+ `description: "${agent.description}"`,
118
+ '---',
119
+ '',
120
+ preamble,
121
+ '',
122
+ content,
123
+ ].join('\n');
124
+ ctx.executor.ensureDirectory(skillDir);
125
+ ctx.executor.writeFile(skillFile, skillContent, {
126
+ description: `write agent skill ${agent.name}`,
127
+ });
128
+ written++;
129
+ }
130
+ return written;
131
+ }
132
+ /**
133
+ * Generates GitHub Copilot instruction files.
134
+ * - .github/copilot-instructions.md — global instructions (mode-aware)
135
+ * - .github/instructions/<name>.instructions.md — per-agent instructions
136
+ */
137
+ function generateCopilotInstructions(ctx, mode, repoConfig, workspaceConfig) {
138
+ const githubDir = node_path_1.default.join((0, orchestrator_1.getRepoBaseDir)(ctx), '.github');
139
+ const instructionsDir = node_path_1.default.join(githubDir, 'instructions');
140
+ const preamble = buildArchitectureAccessBlock(mode, repoConfig, workspaceConfig);
141
+ let written = 0;
142
+ // Global instructions file
143
+ const globalFile = node_path_1.default.join(githubDir, 'copilot-instructions.md');
144
+ if (!node_fs_1.default.existsSync(globalFile)) {
145
+ const globalContent = [
146
+ '# Copilot Instructions',
147
+ '',
148
+ 'This project follows the Collab architectural governance process (GOV-R-001).',
149
+ 'Use the per-agent instruction files in `.github/instructions/` for phase-specific guidance.',
150
+ '',
151
+ '## Governance Phases',
152
+ '',
153
+ '1. Survey — Map files, check duplication, propose design',
154
+ '2. Change Plan — Detailed implementation plan',
155
+ '3. Implementation — Execute the plan',
156
+ '4. Repo Hygiene — Tests, lint, docs, dead code',
157
+ '5. Canon Sync — Update canonical architecture',
158
+ '',
159
+ preamble,
160
+ '',
161
+ ].join('\n');
162
+ ctx.executor.ensureDirectory(githubDir);
163
+ ctx.executor.writeFile(globalFile, globalContent, {
164
+ description: 'write Copilot global instructions',
165
+ });
166
+ written++;
167
+ }
168
+ // Per-agent instruction files
169
+ for (const agent of AGENT_PROMPTS) {
170
+ const content = (0, canon_resolver_1.resolveCanonFile)(agent.canonPath);
171
+ if (!content) {
172
+ continue;
173
+ }
174
+ const instrFile = node_path_1.default.join(instructionsDir, `${agent.name}.instructions.md`);
175
+ if (node_fs_1.default.existsSync(instrFile)) {
176
+ ctx.logger.debug(`Copilot instruction already exists, skipping: ${agent.name}`);
177
+ continue;
178
+ }
179
+ const instrContent = [
180
+ '---',
181
+ 'applyTo: "**"',
182
+ '---',
183
+ '',
184
+ preamble,
185
+ '',
186
+ content,
187
+ ].join('\n');
188
+ ctx.executor.ensureDirectory(instructionsDir);
189
+ ctx.executor.writeFile(instrFile, instrContent, {
190
+ description: `write Copilot instruction ${agent.name}`,
191
+ });
192
+ written++;
193
+ }
194
+ return written;
195
+ }
196
+ /**
197
+ * Generates a CLAUDE.md file at repo root with mode-aware architecture context.
198
+ */
199
+ function generateClaudeMd(ctx, mode, repoConfig, workspaceConfig) {
200
+ const claudeFile = node_path_1.default.join((0, orchestrator_1.getRepoBaseDir)(ctx), 'CLAUDE.md');
201
+ if (node_fs_1.default.existsSync(claudeFile)) {
202
+ ctx.logger.debug('CLAUDE.md already exists, skipping.');
203
+ return false;
204
+ }
205
+ const accessBlock = buildArchitectureAccessBlock(mode, repoConfig, workspaceConfig);
206
+ const mcpConfigNote = mode === 'indexed'
207
+ ? [
208
+ '',
209
+ '## MCP Configuration',
210
+ '',
211
+ 'MCP client config: `.collab/claude-mcp-config.json`',
212
+ 'Merge this into your Claude Code MCP settings to enable architecture retrieval.',
213
+ '',
214
+ ].join('\n')
215
+ : '';
216
+ const content = [
217
+ '# Claude Code — Architecture Context',
218
+ '',
219
+ 'This project follows the Collab architectural governance process (GOV-R-001).',
220
+ '',
221
+ accessBlock,
222
+ mcpConfigNote,
223
+ '## Agent Skills',
224
+ '',
225
+ 'Agent skills are defined in `.agents/skills/` following the Agent Skills Spec.',
226
+ 'Each skill maps to a GOV-R-001 governance phase or thematic agent.',
227
+ '',
228
+ '## Governance Workflow',
229
+ '',
230
+ 'All changes must follow the five-phase governance process:',
231
+ '',
232
+ '1. **Survey** — Map files, check duplication, propose design',
233
+ '2. **Change Plan** — Detailed implementation plan with acceptance criteria',
234
+ '3. **Implementation** — Execute following the approved plan',
235
+ '4. **Repo Hygiene** — Verify tests, lint, docs, dead code removal',
236
+ '5. **Canon Sync** — Update canonical architecture documentation',
237
+ '',
238
+ '## Rules',
239
+ '',
240
+ '- Read `docs/architecture/uxmaltech/governance/` for full governance rules',
241
+ '- Check `docs/architecture/repo/` for project-specific decisions and conventions',
242
+ '- Use `docs/ai/00_brief.md` for a quick project overview',
243
+ '',
244
+ ].join('\n');
245
+ ctx.executor.writeFile(claudeFile, content, {
246
+ description: 'write CLAUDE.md architecture context',
247
+ });
248
+ return true;
249
+ }
250
+ // ────────────────────────────────────────────────────────────────
251
+ // Stage export
252
+ // ────────────────────────────────────────────────────────────────
253
+ exports.agentSkillsSetupStage = {
254
+ id: 'agent-skills-setup',
255
+ title: 'Generate agent skill files',
256
+ recovery: [
257
+ 'Ensure canons are available (run collab update-canons).',
258
+ 'Run collab init --resume to retry agent skills setup.',
259
+ ],
260
+ run: (ctx) => {
261
+ const enabledProviders = (0, providers_1.getEnabledProviders)(ctx.config);
262
+ if (enabledProviders.length === 0) {
263
+ ctx.logger.info('No providers configured; skipping agent skills setup.');
264
+ return;
265
+ }
266
+ const mode = ctx.config.mode;
267
+ const repoConfig = ctx.options?._repoConfig;
268
+ const workspaceConfig = ctx.config.workspace;
269
+ const hasSkillsProvider = enabledProviders.some((p) => p !== 'copilot');
270
+ const hasCopilot = enabledProviders.includes('copilot');
271
+ const hasClaude = enabledProviders.includes('claude');
272
+ let totalWritten = 0;
273
+ // Agent Skills Spec for Claude, Codex, and Gemini
274
+ if (hasSkillsProvider) {
275
+ const count = generateAgentSkillsSpec(ctx, mode, repoConfig, workspaceConfig);
276
+ totalWritten += count;
277
+ if (count > 0) {
278
+ ctx.logger.info(`Agent Skills Spec: ${count} SKILL.md file(s) written to .agents/skills/.`);
279
+ }
280
+ }
281
+ // Copilot instructions
282
+ if (hasCopilot) {
283
+ const count = generateCopilotInstructions(ctx, mode, repoConfig, workspaceConfig);
284
+ totalWritten += count;
285
+ if (count > 0) {
286
+ ctx.logger.info(`Copilot instructions: ${count} file(s) written to .github/.`);
287
+ }
288
+ }
289
+ // CLAUDE.md for Claude provider
290
+ if (hasClaude) {
291
+ const wrote = generateClaudeMd(ctx, mode, repoConfig, workspaceConfig);
292
+ if (wrote) {
293
+ totalWritten++;
294
+ ctx.logger.info('CLAUDE.md written at repo root.');
295
+ }
296
+ }
297
+ if (totalWritten === 0) {
298
+ ctx.logger.info('All agent skill files already exist; nothing to write.');
299
+ }
300
+ },
301
+ };
@@ -0,0 +1,325 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assistantSetupStage = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const cli_detection_1 = require("../lib/cli-detection");
6
+ const credentials_1 = require("../lib/credentials");
7
+ const errors_1 = require("../lib/errors");
8
+ const model_listing_1 = require("../lib/model-listing");
9
+ const model_registry_1 = require("../lib/model-registry");
10
+ const prompt_1 = require("../lib/prompt");
11
+ const providers_1 = require("../lib/providers");
12
+ const config_1 = require("../lib/config");
13
+ /**
14
+ * Resolves the effective API key for a provider from env var or stored credentials.
15
+ */
16
+ function resolveEffectiveKey(provider, ctx) {
17
+ const envKey = process.env[providers_1.PROVIDER_DEFAULTS[provider].envVar];
18
+ if (envKey) {
19
+ return envKey;
20
+ }
21
+ return (0, credentials_1.loadApiKey)(ctx.config, provider);
22
+ }
23
+ async function configureApiKey(provider, ctx) {
24
+ const defaults = providers_1.PROVIDER_DEFAULTS[provider];
25
+ const isNonInteractive = Boolean(ctx.options?.yes);
26
+ const envVar = defaults.envVar;
27
+ if (process.env[envVar]) {
28
+ ctx.logger.info(` \u2713 ${envVar} detected in environment`);
29
+ return { method: 'api-key', envVar };
30
+ }
31
+ // Env var is not set — prompt for API key or let user set it later
32
+ if (isNonInteractive) {
33
+ ctx.logger.warn(`${envVar} is not set in current environment. Set it before running collab commands, ` +
34
+ `or run collab init interactively to enter it directly.`);
35
+ return { method: 'api-key', envVar };
36
+ }
37
+ ctx.logger.info(` ${envVar} is not set in current environment.`);
38
+ const apiKey = await (0, prompt_1.promptText)(`Enter API key for ${defaults.label} (or leave empty to use ${envVar} env var later)`);
39
+ if (apiKey) {
40
+ // Save the key securely to .collab/credentials.json
41
+ if (!ctx.executor.dryRun) {
42
+ (0, credentials_1.saveApiKey)(ctx.config, provider, apiKey);
43
+ ctx.logger.info(` \u2713 API key saved to .collab/credentials.json`);
44
+ }
45
+ else {
46
+ ctx.logger.info(` [dry-run] Would save API key to .collab/credentials.json`);
47
+ }
48
+ }
49
+ else {
50
+ ctx.logger.warn(`No API key provided. Set ${envVar} before running collab commands.`);
51
+ }
52
+ return { method: 'api-key', envVar };
53
+ }
54
+ /**
55
+ * Queries the provider API for available models, prints a summary,
56
+ * and persists results to the model registry.
57
+ * Returns the model list, or null if the query fails or is skipped.
58
+ */
59
+ async function fetchAndShowModels(provider, apiKey, ctx) {
60
+ try {
61
+ const models = await (0, model_listing_1.listModels)(provider, apiKey);
62
+ if (models.length === 0) {
63
+ ctx.logger.warn(` API key accepted but no generative models found.`);
64
+ return null;
65
+ }
66
+ ctx.logger.info(` \u2713 API key validated \u2014 ${models.length} model(s) available:`);
67
+ for (const m of models) {
68
+ const label = m.name ? `${m.id} (${m.name})` : m.id;
69
+ ctx.logger.info(` ${label}`);
70
+ }
71
+ // Persist to model registry for future features
72
+ if (!ctx.executor.dryRun) {
73
+ (0, model_registry_1.saveProviderModels)(ctx.config, provider, models);
74
+ }
75
+ return models;
76
+ }
77
+ catch (err) {
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ ctx.logger.warn(` Could not query models: ${message}`);
80
+ ctx.logger.info(` Falling back to default model list.`);
81
+ return null;
82
+ }
83
+ }
84
+ /**
85
+ * Selects a model — from the live API list when available, otherwise from hardcoded defaults.
86
+ */
87
+ async function selectModel(provider, availableModels, isNonInteractive, cliConfiguredModel) {
88
+ const defaults = providers_1.PROVIDER_DEFAULTS[provider];
89
+ if (isNonInteractive) {
90
+ // Prefer CLI-configured model, then first hardcoded default
91
+ return cliConfiguredModel ?? defaults.models[0];
92
+ }
93
+ if (availableModels && availableModels.length > 0) {
94
+ const defaultSet = new Set(defaults.models);
95
+ const apiIds = new Set(availableModels.map((m) => m.id));
96
+ const byId = new Map(availableModels.map((m) => [m.id, m]));
97
+ const choices = [];
98
+ // If CLI has a configured model not in defaults, add it first
99
+ if (cliConfiguredModel && !defaultSet.has(cliConfiguredModel) && !apiIds.has(cliConfiguredModel)) {
100
+ choices.push({ value: cliConfiguredModel, label: `${cliConfiguredModel} (CLI configured)` });
101
+ }
102
+ // Defaults that are available in the API
103
+ for (const d of defaults.models) {
104
+ if (apiIds.has(d)) {
105
+ const m = byId.get(d);
106
+ choices.push({ value: m.id, label: m.name ? `${m.id} \u2014 ${m.name}` : m.id });
107
+ }
108
+ }
109
+ // CLI-configured model from the API list (move to front if found)
110
+ if (cliConfiguredModel && apiIds.has(cliConfiguredModel) && !defaultSet.has(cliConfiguredModel)) {
111
+ const m = byId.get(cliConfiguredModel);
112
+ const label = m.name ? `${m.id} \u2014 ${m.name} (CLI configured)` : `${m.id} (CLI configured)`;
113
+ // Insert at beginning
114
+ choices.unshift({ value: m.id, label });
115
+ }
116
+ // Remaining models from the API
117
+ const others = availableModels.filter((m) => !defaultSet.has(m.id) && m.id !== cliConfiguredModel);
118
+ for (const m of others) {
119
+ choices.push({ value: m.id, label: m.name ? `${m.id} \u2014 ${m.name}` : m.id });
120
+ }
121
+ if (choices.length > 0) {
122
+ const defaultChoice = cliConfiguredModel
123
+ ? choices.find((c) => c.value === cliConfiguredModel)?.value ?? choices[0].value
124
+ : choices[0].value;
125
+ return (0, prompt_1.promptChoice)(`Default model for ${defaults.label}:`, choices, defaultChoice);
126
+ }
127
+ return availableModels[0].id;
128
+ }
129
+ // Hardcoded fallback — include CLI-configured model if not already in defaults
130
+ const fallbackChoices = [...defaults.models];
131
+ if (cliConfiguredModel && !fallbackChoices.includes(cliConfiguredModel)) {
132
+ fallbackChoices.unshift(cliConfiguredModel);
133
+ }
134
+ const defaultValue = cliConfiguredModel ?? fallbackChoices[0];
135
+ return (0, prompt_1.promptChoice)(`Default model for ${defaults.label}:`, fallbackChoices.map((m) => ({
136
+ value: m,
137
+ label: m === cliConfiguredModel ? `${m} (CLI configured)` : m,
138
+ })), defaultValue);
139
+ }
140
+ /**
141
+ * Configures the Copilot (GitHub) provider.
142
+ * Copilot doesn't use API keys or models — it works via `gh` CLI and GitHub issues.
143
+ * Validates that `gh` is installed and authenticated.
144
+ */
145
+ function configureCopilotProvider(ctx) {
146
+ ctx.logger.info(`\nConfiguring ${providers_1.PROVIDER_DEFAULTS.copilot.label}...`);
147
+ const cli = (0, cli_detection_1.detectProviderCli)('copilot');
148
+ if (!cli.available) {
149
+ // gh not installed or not authenticated
150
+ const ghExists = (() => {
151
+ try {
152
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
153
+ (0, node_child_process_1.execSync)(`${whichCmd} gh`, {
154
+ encoding: 'utf8',
155
+ timeout: 3_000,
156
+ stdio: ['ignore', 'pipe', 'ignore'],
157
+ });
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ })();
164
+ if (!ghExists) {
165
+ ctx.logger.warn(' gh CLI not found. Install it from https://cli.github.com/');
166
+ return { enabled: false, auth: { method: 'cli' } };
167
+ }
168
+ // gh exists but not authenticated
169
+ ctx.logger.warn(' gh CLI found but not authenticated. Run: gh auth login');
170
+ return { enabled: false, auth: { method: 'cli' } };
171
+ }
172
+ const ver = cli.version ? ` (${cli.version})` : '';
173
+ ctx.logger.info(` \u2713 gh CLI detected${ver}`);
174
+ ctx.logger.info(` \u2713 GitHub authentication verified`);
175
+ return {
176
+ enabled: true,
177
+ auth: { method: 'cli' },
178
+ cli: cli,
179
+ };
180
+ }
181
+ async function configureProvider(provider, ctx) {
182
+ const defaults = providers_1.PROVIDER_DEFAULTS[provider];
183
+ const isNonInteractive = Boolean(ctx.options?.yes);
184
+ ctx.logger.info(`\nConfiguring ${defaults.label}...`);
185
+ // Detect official CLI
186
+ const cli = (0, cli_detection_1.detectProviderCli)(provider);
187
+ if (cli.available) {
188
+ const ver = cli.version ? ` (${cli.version})` : '';
189
+ ctx.logger.info(` \u2713 ${cli.command} CLI detected${ver}`);
190
+ }
191
+ // Determine auth method
192
+ let authMethod;
193
+ const hasEnvKey = Boolean(process.env[defaults.envVar]);
194
+ if (cli.available && !hasEnvKey) {
195
+ // CLI is available and no env var — offer CLI vs API key choice
196
+ if (isNonInteractive) {
197
+ // Non-interactive: default to CLI when detected and no env var
198
+ authMethod = 'cli';
199
+ ctx.logger.info(` Using ${cli.command} CLI (no ${defaults.envVar} set).`);
200
+ }
201
+ else {
202
+ authMethod = await (0, prompt_1.promptChoice)(`Authentication for ${defaults.label}:`, [
203
+ { value: 'cli', label: `Use ${cli.command} CLI directly (no API key needed)` },
204
+ { value: 'api-key', label: `Enter API key (${defaults.envVar})` },
205
+ ], 'cli');
206
+ }
207
+ }
208
+ else if (cli.available && hasEnvKey) {
209
+ // Both CLI and env var available — let user pick
210
+ if (isNonInteractive) {
211
+ authMethod = 'api-key';
212
+ }
213
+ else {
214
+ authMethod = await (0, prompt_1.promptChoice)(`Authentication for ${defaults.label}:`, [
215
+ { value: 'cli', label: `Use ${cli.command} CLI directly (no API key needed)` },
216
+ { value: 'api-key', label: `API key via ${defaults.envVar} (detected)` },
217
+ ], 'api-key');
218
+ }
219
+ }
220
+ else {
221
+ // No CLI — API key is the only option
222
+ authMethod = 'api-key';
223
+ }
224
+ // Configure based on chosen method
225
+ let auth;
226
+ let availableModels = null;
227
+ if (authMethod === 'cli') {
228
+ auth = { method: 'cli' };
229
+ ctx.logger.info(` \u2713 Will use ${cli.command} CLI for ${defaults.label}.`);
230
+ if (cli.configuredModel) {
231
+ ctx.logger.info(` \u2713 CLI configured model: ${cli.configuredModel}`);
232
+ }
233
+ }
234
+ else {
235
+ auth = await configureApiKey(provider, ctx);
236
+ // Fetch and show models when we have an API key
237
+ const effectiveKey = resolveEffectiveKey(provider, ctx);
238
+ if (effectiveKey && !ctx.executor.dryRun) {
239
+ availableModels = await fetchAndShowModels(provider, effectiveKey, ctx);
240
+ }
241
+ }
242
+ // Model selection — prefer CLI-configured model when using CLI auth
243
+ const cliModel = authMethod === 'cli' ? cli.configuredModel : undefined;
244
+ const model = await selectModel(provider, availableModels, isNonInteractive, cliModel);
245
+ return {
246
+ enabled: true,
247
+ auth,
248
+ model,
249
+ cli: cli.available ? cli : undefined,
250
+ };
251
+ }
252
+ exports.assistantSetupStage = {
253
+ id: 'assistant-setup',
254
+ title: 'Configure AI assistant providers',
255
+ recovery: [
256
+ 'Re-run collab init --resume to reconfigure providers.',
257
+ 'Ensure required API keys are set (env var or collab init interactive).',
258
+ ],
259
+ run: async (ctx) => {
260
+ const isNonInteractive = Boolean(ctx.options?.yes);
261
+ const providersFlag = ctx.options?.providers;
262
+ // Determine which providers to configure
263
+ let selectedProviders;
264
+ if (providersFlag) {
265
+ // Explicit --providers flag
266
+ selectedProviders = (0, providers_1.parseProviderList)(providersFlag);
267
+ }
268
+ else if (isNonInteractive) {
269
+ // Auto-detect from environment variables
270
+ selectedProviders = await (0, providers_1.autoDetectProviders)();
271
+ if (selectedProviders.length === 0) {
272
+ ctx.logger.warn('No AI provider environment variables detected. ' +
273
+ `Set one of: ${providers_1.PROVIDER_KEYS.map((k) => providers_1.PROVIDER_DEFAULTS[k].envVar).join(', ')} ` +
274
+ 'or use --providers to specify explicitly.');
275
+ ctx.logger.info('Skipping assistant-setup stage (no providers configured).');
276
+ return;
277
+ }
278
+ ctx.logger.info(`Auto-detected providers: ${selectedProviders.map((k) => providers_1.PROVIDER_DEFAULTS[k].label).join(', ')}`);
279
+ }
280
+ else {
281
+ // Interactive multi-select
282
+ selectedProviders = await (0, prompt_1.promptMultiSelect)('Select AI providers to configure:', providers_1.PROVIDER_KEYS.map((key) => ({
283
+ value: key,
284
+ label: providers_1.PROVIDER_DEFAULTS[key].label,
285
+ description: providers_1.PROVIDER_DEFAULTS[key].description,
286
+ })));
287
+ }
288
+ if (selectedProviders.length === 0) {
289
+ if (providersFlag) {
290
+ // Explicit --providers flag with empty/invalid list: error
291
+ throw new errors_1.CliError('At least one AI provider must be selected. ' +
292
+ `Available providers: ${providers_1.PROVIDER_KEYS.join(', ')}`);
293
+ }
294
+ // Interactive with no selection: skip gracefully
295
+ ctx.logger.info('No providers selected. You can configure providers later with collab init --resume.');
296
+ return;
297
+ }
298
+ // Configure each selected provider
299
+ const providerConfigs = {};
300
+ for (const provider of selectedProviders) {
301
+ providerConfigs[provider] =
302
+ provider === 'copilot'
303
+ ? configureCopilotProvider(ctx)
304
+ : await configureProvider(provider, ctx);
305
+ }
306
+ // Mark unselected providers as disabled
307
+ for (const key of providers_1.PROVIDER_KEYS) {
308
+ if (!selectedProviders.includes(key)) {
309
+ providerConfigs[key] = { enabled: false, auth: { method: 'api-key' } };
310
+ }
311
+ }
312
+ // Persist to config
313
+ const assistants = { providers: providerConfigs };
314
+ ctx.config.assistants = assistants;
315
+ // Write updated config
316
+ ctx.executor.writeFile(ctx.config.configFile, `${(0, config_1.serializeUserConfig)(ctx.config)}\n`, { description: 'write assistant provider configuration' });
317
+ // Summary
318
+ const enabledNames = selectedProviders.map((k) => {
319
+ const cfg = providerConfigs[k];
320
+ const authTag = cfg.auth.method === 'cli' ? `${cfg.cli?.command ?? 'cli'} CLI` : `API key`;
321
+ return `${providers_1.PROVIDER_DEFAULTS[k].label} (${authTag}, model: ${cfg.model})`;
322
+ });
323
+ ctx.logger.result(`Configured providers: ${enabledNames.join(', ')}`);
324
+ },
325
+ };