@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.
- package/README.md +227 -0
- package/bin/collab +10 -0
- package/dist/cli.js +34 -0
- package/dist/commands/canon/index.js +16 -0
- package/dist/commands/canon/rebuild.js +95 -0
- package/dist/commands/compose/generate.js +63 -0
- package/dist/commands/compose/index.js +18 -0
- package/dist/commands/compose/validate.js +53 -0
- package/dist/commands/doctor.js +153 -0
- package/dist/commands/index.js +27 -0
- package/dist/commands/infra/down.js +23 -0
- package/dist/commands/infra/index.js +20 -0
- package/dist/commands/infra/shared.js +59 -0
- package/dist/commands/infra/status.js +64 -0
- package/dist/commands/infra/up.js +29 -0
- package/dist/commands/init.js +830 -0
- package/dist/commands/mcp/index.js +20 -0
- package/dist/commands/mcp/shared.js +57 -0
- package/dist/commands/mcp/start.js +45 -0
- package/dist/commands/mcp/status.js +62 -0
- package/dist/commands/mcp/stop.js +23 -0
- package/dist/commands/seed.js +55 -0
- package/dist/commands/uninstall.js +36 -0
- package/dist/commands/up.js +78 -0
- package/dist/commands/update-canons.js +48 -0
- package/dist/commands/upgrade.js +54 -0
- package/dist/index.js +14 -0
- package/dist/lib/ai-client.js +317 -0
- package/dist/lib/ansi.js +58 -0
- package/dist/lib/canon-index-generator.js +64 -0
- package/dist/lib/canon-index-targets.js +68 -0
- package/dist/lib/canon-resolver.js +262 -0
- package/dist/lib/canon-scaffold.js +57 -0
- package/dist/lib/cli-detection.js +149 -0
- package/dist/lib/command-context.js +23 -0
- package/dist/lib/compose-defaults.js +47 -0
- package/dist/lib/compose-env.js +24 -0
- package/dist/lib/compose-paths.js +36 -0
- package/dist/lib/compose-renderer.js +134 -0
- package/dist/lib/compose-validator.js +56 -0
- package/dist/lib/config.js +195 -0
- package/dist/lib/credentials.js +63 -0
- package/dist/lib/docker-checks.js +73 -0
- package/dist/lib/docker-compose.js +15 -0
- package/dist/lib/docker-status.js +151 -0
- package/dist/lib/domain-gen.js +376 -0
- package/dist/lib/ecosystem.js +150 -0
- package/dist/lib/env-file.js +77 -0
- package/dist/lib/errors.js +30 -0
- package/dist/lib/executor.js +85 -0
- package/dist/lib/github-auth.js +204 -0
- package/dist/lib/hash.js +7 -0
- package/dist/lib/health-checker.js +140 -0
- package/dist/lib/logger.js +87 -0
- package/dist/lib/mcp-client.js +88 -0
- package/dist/lib/mode.js +36 -0
- package/dist/lib/model-listing.js +102 -0
- package/dist/lib/model-registry.js +55 -0
- package/dist/lib/npm-operations.js +69 -0
- package/dist/lib/orchestrator.js +170 -0
- package/dist/lib/parsers.js +42 -0
- package/dist/lib/port-resolver.js +57 -0
- package/dist/lib/preconditions.js +35 -0
- package/dist/lib/preflight.js +88 -0
- package/dist/lib/process.js +6 -0
- package/dist/lib/prompt.js +125 -0
- package/dist/lib/providers.js +117 -0
- package/dist/lib/repo-analysis-helpers.js +379 -0
- package/dist/lib/repo-scanner.js +195 -0
- package/dist/lib/service-health.js +79 -0
- package/dist/lib/shell.js +49 -0
- package/dist/lib/state.js +38 -0
- package/dist/lib/update-checker.js +130 -0
- package/dist/lib/version.js +27 -0
- package/dist/stages/agent-skills-setup.js +301 -0
- package/dist/stages/assistant-setup.js +325 -0
- package/dist/stages/canon-ingest.js +249 -0
- package/dist/stages/canon-rebuild-graph.js +33 -0
- package/dist/stages/canon-rebuild-indexes.js +40 -0
- package/dist/stages/canon-rebuild-snapshot.js +75 -0
- package/dist/stages/canon-rebuild-validate.js +57 -0
- package/dist/stages/canon-rebuild-vectors.js +30 -0
- package/dist/stages/canon-scaffold.js +15 -0
- package/dist/stages/canon-sync.js +49 -0
- package/dist/stages/ci-setup.js +56 -0
- package/dist/stages/domain-gen.js +363 -0
- package/dist/stages/graph-seed.js +26 -0
- package/dist/stages/repo-analysis-fileonly.js +111 -0
- package/dist/stages/repo-analysis.js +112 -0
- package/dist/stages/repo-scaffold.js +110 -0
- package/dist/templates/canon/contracts-readme.js +39 -0
- package/dist/templates/canon/domain-readme.js +40 -0
- package/dist/templates/canon/evolution/changelog.js +53 -0
- package/dist/templates/canon/governance/confidence-levels.js +38 -0
- package/dist/templates/canon/governance/implementation-process.js +34 -0
- package/dist/templates/canon/governance/review-process.js +29 -0
- package/dist/templates/canon/governance/schema-versioning.js +25 -0
- package/dist/templates/canon/governance/what-enters-the-canon.js +44 -0
- package/dist/templates/canon/index.js +28 -0
- package/dist/templates/canon/knowledge-readme.js +129 -0
- package/dist/templates/canon/system-prompt.js +101 -0
- package/dist/templates/ci/architecture-merge.js +29 -0
- package/dist/templates/ci/architecture-pr.js +26 -0
- package/dist/templates/ci/index.js +7 -0
- package/dist/templates/consolidated.js +114 -0
- package/dist/templates/infra.js +90 -0
- package/dist/templates/mcp.js +32 -0
- package/install.sh +455 -0
- 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
|
+
};
|