@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,376 @@
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.renderPrinciplesMd = renderPrinciplesMd;
7
+ exports.renderRulesMd = renderRulesMd;
8
+ exports.renderAntiPatternsMd = renderAntiPatternsMd;
9
+ exports.renderGlossaryMd = renderGlossaryMd;
10
+ exports.renderPatternMd = renderPatternMd;
11
+ exports.writeDomainFiles = writeDomainFiles;
12
+ exports.findNextIds = findNextIds;
13
+ exports.generateDomainGraphSeed = generateDomainGraphSeed;
14
+ exports.appendGraphSeed = appendGraphSeed;
15
+ exports.buildDomainGenPrompt = buildDomainGenPrompt;
16
+ exports.parseDomainGenerationResponse = parseDomainGenerationResponse;
17
+ const node_fs_1 = __importDefault(require("node:fs"));
18
+ const node_path_1 = __importDefault(require("node:path"));
19
+ const repo_analysis_helpers_1 = require("./repo-analysis-helpers");
20
+ // ────────────────────────────────────────────────────────────────
21
+ // Renderers — follow exact collab-architecture/domains/ format
22
+ // ────────────────────────────────────────────────────────────────
23
+ /** Renders principles as markdown following the collab-architecture domain format. */
24
+ function renderPrinciplesMd(r) {
25
+ const lines = [`# ${r.domainName} Principles`, ''];
26
+ for (const p of r.principles) {
27
+ lines.push(`- ${p.id}: ${p.text}`);
28
+ }
29
+ lines.push('');
30
+ lines.push('<!-- AI-GENERATED -->');
31
+ lines.push('');
32
+ return lines.join('\n');
33
+ }
34
+ /** Renders rules as markdown following the collab-architecture domain format. */
35
+ function renderRulesMd(r) {
36
+ const lines = [`# ${r.domainName} Rules`, ''];
37
+ for (const rule of r.rules) {
38
+ lines.push(`- ${rule.id}: ${rule.text}`);
39
+ }
40
+ lines.push('');
41
+ lines.push('<!-- AI-GENERATED -->');
42
+ lines.push('');
43
+ return lines.join('\n');
44
+ }
45
+ /** Renders anti-patterns as markdown with violated rule references. */
46
+ function renderAntiPatternsMd(r) {
47
+ const lines = [`# ${r.domainName} Anti-Patterns`, ''];
48
+ for (const ap of r.antiPatterns) {
49
+ lines.push(`- ${ap.id}: ${ap.description}`);
50
+ lines.push('');
51
+ lines.push(' **Rules Violated:**');
52
+ for (const rule of ap.rulesViolated) {
53
+ lines.push(` - ${rule}`);
54
+ }
55
+ lines.push('');
56
+ }
57
+ lines.push('<!-- AI-GENERATED -->');
58
+ lines.push('');
59
+ return lines.join('\n');
60
+ }
61
+ /** Renders the domain glossary as a markdown term list. */
62
+ function renderGlossaryMd(r) {
63
+ const lines = [`# ${r.domainName} Glossary`, ''];
64
+ for (const g of r.glossary) {
65
+ lines.push(`- ${g.term}: ${g.definition}`);
66
+ }
67
+ lines.push('');
68
+ lines.push('<!-- AI-GENERATED -->');
69
+ lines.push('');
70
+ return lines.join('\n');
71
+ }
72
+ /** Renders a single pattern as a full markdown document with context, problem, and solution. */
73
+ function renderPatternMd(p) {
74
+ const lines = [
75
+ `# Pattern: ${p.name}`,
76
+ '',
77
+ `Pattern ID: ${p.id}`,
78
+ 'Status: Active',
79
+ 'Confidence: provisional',
80
+ '',
81
+ 'Context:',
82
+ p.context,
83
+ '',
84
+ 'Problem:',
85
+ p.problem,
86
+ '',
87
+ 'Solution:',
88
+ p.solution,
89
+ '',
90
+ 'Rules Enforced:',
91
+ ...p.rulesEnforced.map((r) => `- ${r}`),
92
+ '',
93
+ 'Consequences:',
94
+ p.consequences,
95
+ '',
96
+ '<!-- AI-GENERATED -->',
97
+ '',
98
+ ];
99
+ return lines.join('\n');
100
+ }
101
+ // ────────────────────────────────────────────────────────────────
102
+ // File writing
103
+ // ────────────────────────────────────────────────────────────────
104
+ /**
105
+ * Sanitizes a string for use as a directory or file name.
106
+ */
107
+ function slugify(name) {
108
+ return name
109
+ .toLowerCase()
110
+ .replace(/[^a-z0-9]+/g, '-')
111
+ .replace(/^-|-$/g, '');
112
+ }
113
+ /**
114
+ * Writes all domain files to a target directory via the executor abstraction.
115
+ * Creates: principles.md, rules.md, anti-patterns.md, glossary.md, patterns/*.md
116
+ *
117
+ * When an executor is provided, all filesystem operations go through it
118
+ * to respect `--dry-run` mode and centralized side-effect control.
119
+ *
120
+ * @returns The number of files written.
121
+ */
122
+ function writeDomainFiles(targetDir, result, executor) {
123
+ let count = 0;
124
+ const writeFile = (filePath, content, desc) => {
125
+ if (executor) {
126
+ executor.writeFile(filePath, content, { description: desc });
127
+ }
128
+ else {
129
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
130
+ node_fs_1.default.writeFileSync(filePath, content, 'utf8');
131
+ }
132
+ };
133
+ const ensureDir = (dirPath) => {
134
+ if (executor) {
135
+ executor.ensureDirectory(dirPath);
136
+ }
137
+ else {
138
+ node_fs_1.default.mkdirSync(dirPath, { recursive: true });
139
+ }
140
+ };
141
+ ensureDir(targetDir);
142
+ writeFile(node_path_1.default.join(targetDir, 'principles.md'), renderPrinciplesMd(result), 'write principles.md');
143
+ count++;
144
+ writeFile(node_path_1.default.join(targetDir, 'rules.md'), renderRulesMd(result), 'write rules.md');
145
+ count++;
146
+ writeFile(node_path_1.default.join(targetDir, 'anti-patterns.md'), renderAntiPatternsMd(result), 'write anti-patterns.md');
147
+ count++;
148
+ writeFile(node_path_1.default.join(targetDir, 'glossary.md'), renderGlossaryMd(result), 'write glossary.md');
149
+ count++;
150
+ if (result.patterns.length > 0) {
151
+ const patternsDir = node_path_1.default.join(targetDir, 'patterns');
152
+ ensureDir(patternsDir);
153
+ for (const pattern of result.patterns) {
154
+ const filename = `${slugify(pattern.name)}.md`;
155
+ writeFile(node_path_1.default.join(patternsDir, filename), renderPatternMd(pattern), `write pattern ${filename}`);
156
+ count++;
157
+ }
158
+ }
159
+ return count;
160
+ }
161
+ /**
162
+ * Parses an existing data.ngql file to find the next available IDs
163
+ * for domains, patterns, and technologies.
164
+ */
165
+ function findNextIds(dataPath) {
166
+ const ids = { domain: 1, pattern: 1, technology: 1 };
167
+ if (!node_fs_1.default.existsSync(dataPath)) {
168
+ return ids;
169
+ }
170
+ const content = node_fs_1.default.readFileSync(dataPath, 'utf8');
171
+ // Find highest DOM-NNN
172
+ const domMatches = content.matchAll(/DOM-(\d+)/g);
173
+ for (const m of domMatches) {
174
+ const num = parseInt(m[1], 10);
175
+ if (num >= ids.domain)
176
+ ids.domain = num + 1;
177
+ }
178
+ // Find highest PAT-NNN
179
+ const patMatches = content.matchAll(/PAT-(\d+)/g);
180
+ for (const m of patMatches) {
181
+ const num = parseInt(m[1], 10);
182
+ if (num >= ids.pattern)
183
+ ids.pattern = num + 1;
184
+ }
185
+ // Find highest TECH-NNN
186
+ const techMatches = content.matchAll(/TECH-(\d+)/g);
187
+ for (const m of techMatches) {
188
+ const num = parseInt(m[1], 10);
189
+ if (num >= ids.technology)
190
+ ids.technology = num + 1;
191
+ }
192
+ return ids;
193
+ }
194
+ /**
195
+ * Generates nGQL INSERT statements for a domain, its technologies,
196
+ * patterns, and relationships.
197
+ */
198
+ function generateDomainGraphSeed(result, nextIds) {
199
+ const lines = [];
200
+ const pad = (n) => String(n).padStart(3, '0');
201
+ const domId = `DOM-${pad(nextIds.domain)}`;
202
+ // Domain vertex
203
+ lines.push(`INSERT VERTEX Node(name, type, status, summary) VALUES` +
204
+ ` "${domId}":("${escape(result.domainName)}", "domain", "active", "${escape(result.summary)}");`);
205
+ // Technology vertices and edges
206
+ let techCounter = nextIds.technology;
207
+ for (const tech of result.technologies) {
208
+ const techId = `TECH-${pad(techCounter)}`;
209
+ lines.push(`INSERT VERTEX Node(name, type, status, summary) VALUES` +
210
+ ` "${techId}":("${escape(tech.name)}", "technology", "active", "${escape(tech.summary)}");`);
211
+ lines.push(`INSERT EDGE Relationship(type) VALUES` +
212
+ ` "${domId}"->"${techId}":("USES_TECHNOLOGY");`);
213
+ techCounter++;
214
+ }
215
+ // Pattern vertices and edges
216
+ let patCounter = nextIds.pattern;
217
+ for (const pat of result.patterns) {
218
+ const patId = `PAT-${pad(patCounter)}`;
219
+ lines.push(`INSERT VERTEX Node(name, type, status, summary) VALUES` +
220
+ ` "${patId}":("${escape(pat.name)}", "pattern", "active", "${escape(pat.context)}");`);
221
+ lines.push(`INSERT EDGE Relationship(type) VALUES` +
222
+ ` "${domId}"->"${patId}":("IMPLEMENTS");`);
223
+ patCounter++;
224
+ }
225
+ lines.push('');
226
+ return lines.join('\n');
227
+ }
228
+ function escape(str) {
229
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ');
230
+ }
231
+ /**
232
+ * Appends nGQL statements to a data.ngql file, creating parent dirs if needed.
233
+ *
234
+ * When an executor is provided, the write goes through the executor abstraction
235
+ * to respect `--dry-run` mode and centralized side-effect control.
236
+ */
237
+ function appendGraphSeed(dataPath, nGql, executor) {
238
+ const separator = `\n-- Domain generated by collab-cli (${new Date().toISOString().split('T')[0]})\n`;
239
+ const existing = node_fs_1.default.existsSync(dataPath) ? node_fs_1.default.readFileSync(dataPath, 'utf8') : '';
240
+ if (executor) {
241
+ executor.writeFile(dataPath, existing + separator + nGql, { description: 'append graph seed' });
242
+ }
243
+ else {
244
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(dataPath), { recursive: true });
245
+ node_fs_1.default.appendFileSync(dataPath, separator + nGql, 'utf8');
246
+ }
247
+ }
248
+ // ────────────────────────────────────────────────────────────────
249
+ // AI prompt
250
+ // ────────────────────────────────────────────────────────────────
251
+ const DOMAIN_GEN_SYSTEM_PROMPT = `You are a software architecture analyst. Your task is to analyze a repository and generate canonical domain definitions following the collab-architecture framework format.
252
+
253
+ You MUST respond with a single JSON object — no markdown, no explanations, just JSON.
254
+
255
+ The JSON must follow this exact schema:
256
+
257
+ {
258
+ "domainName": "Human-readable domain name (e.g. 'Chat AI', 'User Management')",
259
+ "domainSlug": "kebab-case slug (e.g. 'chat-ai', 'user-management')",
260
+ "prefix": "SHORT uppercase prefix for IDs (e.g. 'CHAT', 'USR') — max 4 characters",
261
+ "summary": "One-sentence summary of this domain's purpose",
262
+ "principles": [
263
+ { "id": "{PREFIX}-P-001", "text": "Concise principle statement" }
264
+ ],
265
+ "rules": [
266
+ { "id": "{PREFIX}-R-001", "text": "Prescriptive rule using MUST/MUST NOT language" }
267
+ ],
268
+ "antiPatterns": [
269
+ {
270
+ "id": "{PREFIX}-AP-001",
271
+ "description": "Description of the anti-pattern",
272
+ "rulesViolated": ["{PREFIX}-R-001"]
273
+ }
274
+ ],
275
+ "glossary": [
276
+ { "term": "Term", "definition": "Clear one-line definition" }
277
+ ],
278
+ "patterns": [
279
+ {
280
+ "id": "{PREFIX}-PAT-001",
281
+ "name": "Pattern Name",
282
+ "context": "When/where this pattern applies",
283
+ "problem": "What problem it solves",
284
+ "solution": "How to implement it",
285
+ "rulesEnforced": ["{PREFIX}-R-001"],
286
+ "consequences": "What happens when you follow this pattern"
287
+ }
288
+ ],
289
+ "technologies": [
290
+ { "name": "Technology Name", "summary": "Brief description of role in domain" }
291
+ ]
292
+ }
293
+
294
+ Guidelines:
295
+ - Generate 3-7 principles, 3-7 rules, 2-5 anti-patterns, 5-10 glossary terms, 2-5 patterns, and list all key technologies
296
+ - Principles are aspirational statements of what should be true
297
+ - Rules are prescriptive and use MUST/MUST NOT language
298
+ - Anti-patterns reference specific rule IDs they violate
299
+ - Patterns reference specific rule IDs they enforce
300
+ - Technologies should list frameworks, databases, message brokers, etc. used by the domain`;
301
+ /**
302
+ * Builds the AI prompt messages for domain generation.
303
+ */
304
+ function buildDomainGenPrompt(repoCtx) {
305
+ const parts = [
306
+ `# Repository: ${repoCtx.name}`,
307
+ '',
308
+ `**Language:** ${repoCtx.language}`,
309
+ repoCtx.framework ? `**Framework:** ${repoCtx.framework}` : null,
310
+ `**Source files:** ${repoCtx.totalSourceFiles}`,
311
+ '',
312
+ '## Dependencies',
313
+ repoCtx.dependencies.length > 0
314
+ ? repoCtx.dependencies.map((d) => `- ${d}`).join('\n')
315
+ : '_No dependencies detected._',
316
+ '',
317
+ '## Key Files',
318
+ repoCtx.keyFiles.map((f) => `- ${f}`).join('\n'),
319
+ '',
320
+ '## Directory Structure',
321
+ '```',
322
+ repoCtx.structure,
323
+ '```',
324
+ '',
325
+ 'Analyze this repository and generate a complete domain definition as a JSON object.',
326
+ ];
327
+ return {
328
+ system: DOMAIN_GEN_SYSTEM_PROMPT,
329
+ user: parts.filter((p) => p !== null).join('\n'),
330
+ };
331
+ }
332
+ // ────────────────────────────────────────────────────────────────
333
+ // Response parser
334
+ // ────────────────────────────────────────────────────────────────
335
+ /**
336
+ * Parses the AI response into a DomainGenerationResult.
337
+ * Handles JSON wrapped in markdown code fences.
338
+ * Validates required fields and nested element shapes, failing fast on malformed data.
339
+ */
340
+ function parseDomainGenerationResponse(raw) {
341
+ const jsonStr = (0, repo_analysis_helpers_1.extractJson)(raw);
342
+ const parsed = JSON.parse(jsonStr);
343
+ // Validate required fields
344
+ const required = ['domainName', 'domainSlug', 'prefix', 'summary'];
345
+ for (const field of required) {
346
+ if (!parsed[field] || typeof parsed[field] !== 'string') {
347
+ throw new Error(`Missing or invalid required field: ${field}`);
348
+ }
349
+ }
350
+ return {
351
+ domainName: parsed.domainName,
352
+ domainSlug: parsed.domainSlug,
353
+ prefix: parsed.prefix,
354
+ summary: parsed.summary,
355
+ principles: validateArray(parsed.principles, 'principles', ['id', 'text']),
356
+ rules: validateArray(parsed.rules, 'rules', ['id', 'text']),
357
+ antiPatterns: validateArray(parsed.antiPatterns, 'antiPatterns', ['id', 'description']),
358
+ glossary: validateArray(parsed.glossary, 'glossary', ['term', 'definition']),
359
+ patterns: validateArray(parsed.patterns, 'patterns', ['id', 'name']),
360
+ technologies: validateArray(parsed.technologies, 'technologies', ['name', 'summary']),
361
+ };
362
+ }
363
+ /**
364
+ * Validates an array field from the AI response, filtering out malformed entries
365
+ * and ensuring each element has the required string fields.
366
+ */
367
+ function validateArray(value, fieldName, requiredKeys) {
368
+ if (!Array.isArray(value))
369
+ return [];
370
+ return value.filter((item) => {
371
+ if (typeof item !== 'object' || item === null)
372
+ return false;
373
+ const obj = item;
374
+ return requiredKeys.every((key) => typeof obj[key] === 'string' && obj[key] !== '');
375
+ });
376
+ }
@@ -0,0 +1,150 @@
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.checkEcosystemCompatibility = checkEcosystemCompatibility;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const semver_1 = __importDefault(require("semver"));
10
+ const health_checker_1 = require("./health-checker");
11
+ const MANIFEST_PATH = node_path_1.default.resolve(__dirname, '../../ecosystem.manifest.json');
12
+ function readJsonFile(targetFile) {
13
+ try {
14
+ const raw = node_fs_1.default.readFileSync(targetFile, 'utf8');
15
+ return JSON.parse(raw);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ function readManifest() {
22
+ const manifest = readJsonFile(MANIFEST_PATH);
23
+ if (!manifest) {
24
+ return {
25
+ manifestVersion: '1.0.0',
26
+ cliVersionRange: '>=0.1.0',
27
+ collabArchitectureSchemaRange: '^1.0.0',
28
+ collabArchitectureMcpVersionRange: '^0.1.0',
29
+ collabArchitectureMcpContractRange: '^1.0.0',
30
+ };
31
+ }
32
+ return manifest;
33
+ }
34
+ function readCliVersion() {
35
+ const packagePath = node_path_1.default.resolve(__dirname, '../../package.json');
36
+ const pkg = readJsonFile(packagePath);
37
+ return pkg?.version ?? '0.0.0';
38
+ }
39
+ function findSchemaVersionFile(config) {
40
+ const envOverride = process.env.COLLAB_ARCHITECTURE_SCHEMA_PATH;
41
+ if (envOverride && node_fs_1.default.existsSync(envOverride)) {
42
+ return node_path_1.default.resolve(envOverride);
43
+ }
44
+ const candidates = [
45
+ node_path_1.default.resolve(config.workspaceDir, '../collab-architecture/schema/version.json'),
46
+ node_path_1.default.resolve(config.workspaceDir, 'collab-architecture/schema/version.json'),
47
+ ];
48
+ for (const candidate of candidates) {
49
+ if (node_fs_1.default.existsSync(candidate)) {
50
+ return candidate;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ async function checkEcosystemCompatibility(config, options = {}) {
56
+ const manifest = readManifest();
57
+ const checks = [];
58
+ const cliVersion = readCliVersion();
59
+ const cliCompatible = semver_1.default.satisfies(cliVersion, manifest.cliVersionRange, {
60
+ includePrerelease: true,
61
+ });
62
+ checks.push({
63
+ id: 'cli-version',
64
+ ok: cliCompatible,
65
+ detail: `CLI ${cliVersion} vs required ${manifest.cliVersionRange}`,
66
+ fix: `Upgrade collab-cli to satisfy ${manifest.cliVersionRange}.`,
67
+ });
68
+ const schemaFile = findSchemaVersionFile(config);
69
+ if (!schemaFile) {
70
+ checks.push({
71
+ id: 'canon-schema-version',
72
+ ok: false,
73
+ detail: 'schema/version.json not found for collab-architecture',
74
+ fix: 'Clone collab-architecture next to this workspace or set COLLAB_ARCHITECTURE_SCHEMA_PATH.',
75
+ });
76
+ }
77
+ else {
78
+ const schema = readJsonFile(schemaFile);
79
+ const schemaVersion = schema?.schemaVersion ?? '0.0.0';
80
+ const schemaCompatible = semver_1.default.satisfies(schemaVersion, manifest.collabArchitectureSchemaRange, {
81
+ includePrerelease: true,
82
+ });
83
+ checks.push({
84
+ id: 'canon-schema-version',
85
+ ok: schemaCompatible,
86
+ detail: `schema ${schemaVersion} vs required ${manifest.collabArchitectureSchemaRange}`,
87
+ fix: 'Update collab-architecture to a compatible schema version.',
88
+ });
89
+ if (schema?.minCompatibleCLI) {
90
+ const minCliCompatible = semver_1.default.gte(cliVersion, schema.minCompatibleCLI);
91
+ checks.push({
92
+ id: 'canon-min-cli',
93
+ ok: minCliCompatible,
94
+ detail: `CLI ${cliVersion} vs schema minCompatibleCLI ${schema.minCompatibleCLI}`,
95
+ fix: `Upgrade collab-cli to >= ${schema.minCompatibleCLI}.`,
96
+ });
97
+ }
98
+ }
99
+ const mcpHealthUrl = options.mcpHealthUrl ??
100
+ `http://${process.env.MCP_HOST || '127.0.0.1'}:${process.env.MCP_PORT || '7337'}/health`;
101
+ const mcpHealth = await (0, health_checker_1.checkHttpHealth)('mcp-health', mcpHealthUrl, {
102
+ retries: 1,
103
+ timeoutMs: 3_000,
104
+ retryDelayMs: 0,
105
+ dryRun: options.dryRun,
106
+ });
107
+ if (!mcpHealth.ok || mcpHealth.skipped) {
108
+ checks.push({
109
+ id: 'mcp-version',
110
+ ok: Boolean(mcpHealth.skipped),
111
+ detail: mcpHealth.skipped
112
+ ? 'skipped in dry-run mode'
113
+ : `MCP health endpoint unreachable (${mcpHealth.error ?? mcpHealth.detail})`,
114
+ fix: 'Start MCP service and retry doctor or compatibility checks.',
115
+ });
116
+ }
117
+ else {
118
+ try {
119
+ const response = await fetch(mcpHealthUrl);
120
+ const payload = (await response.json());
121
+ const mcpVersion = payload.version ?? '0.0.0';
122
+ const contractVersion = payload.contractVersion ?? '0.0.0';
123
+ checks.push({
124
+ id: 'mcp-version',
125
+ ok: semver_1.default.satisfies(mcpVersion, manifest.collabArchitectureMcpVersionRange, {
126
+ includePrerelease: true,
127
+ }),
128
+ detail: `MCP ${mcpVersion} vs required ${manifest.collabArchitectureMcpVersionRange}`,
129
+ fix: 'Upgrade/downgrade collab-architecture-mcp to a compatible version.',
130
+ });
131
+ checks.push({
132
+ id: 'mcp-contract-version',
133
+ ok: semver_1.default.satisfies(contractVersion, manifest.collabArchitectureMcpContractRange, {
134
+ includePrerelease: true,
135
+ }),
136
+ detail: `MCP contract ${contractVersion} vs required ${manifest.collabArchitectureMcpContractRange}`,
137
+ fix: 'Upgrade collab-architecture-mcp or update compatibility manifest.',
138
+ });
139
+ }
140
+ catch (error) {
141
+ checks.push({
142
+ id: 'mcp-version',
143
+ ok: false,
144
+ detail: error instanceof Error ? error.message : String(error),
145
+ fix: 'Verify MCP /health endpoint returns JSON with version and contractVersion.',
146
+ });
147
+ }
148
+ }
149
+ return checks;
150
+ }
@@ -0,0 +1,77 @@
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.readEnvFile = readEnvFile;
7
+ exports.mergeEnvWithDefaults = mergeEnvWithDefaults;
8
+ exports.renderEnvFile = renderEnvFile;
9
+ exports.writeEnvFile = writeEnvFile;
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ function parseEnvLine(line) {
13
+ const trimmed = line.trim();
14
+ if (trimmed === '' || trimmed.startsWith('#')) {
15
+ return null;
16
+ }
17
+ const index = trimmed.indexOf('=');
18
+ if (index <= 0) {
19
+ return null;
20
+ }
21
+ const key = trimmed.slice(0, index).trim();
22
+ const value = trimmed.slice(index + 1);
23
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
24
+ return null;
25
+ }
26
+ return [key, value];
27
+ }
28
+ function readEnvFile(filePath) {
29
+ if (!node_fs_1.default.existsSync(filePath)) {
30
+ return {};
31
+ }
32
+ const content = node_fs_1.default.readFileSync(filePath, 'utf8');
33
+ const env = {};
34
+ for (const line of content.split(/\r?\n/)) {
35
+ const parsed = parseEnvLine(line);
36
+ if (parsed) {
37
+ env[parsed[0]] = parsed[1];
38
+ }
39
+ }
40
+ return env;
41
+ }
42
+ function mergeEnvWithDefaults(existing, defaults) {
43
+ const merged = { ...defaults };
44
+ for (const [key, value] of Object.entries(existing)) {
45
+ merged[key] = value;
46
+ }
47
+ return merged;
48
+ }
49
+ function renderEnvFile(env, keyOrder) {
50
+ const orderedKeys = new Set();
51
+ const lines = [];
52
+ for (const key of keyOrder) {
53
+ if (key in env) {
54
+ lines.push(`${key}=${env[key]}`);
55
+ orderedKeys.add(key);
56
+ }
57
+ }
58
+ const customKeys = Object.keys(env)
59
+ .filter((key) => !orderedKeys.has(key))
60
+ .sort((a, b) => a.localeCompare(b));
61
+ if (customKeys.length > 0) {
62
+ lines.push('');
63
+ for (const key of customKeys) {
64
+ lines.push(`${key}=${env[key]}`);
65
+ }
66
+ }
67
+ return `# Generated by collab-cli. You can edit values; regeneration preserves your overrides.\n${lines.join('\n')}\n`;
68
+ }
69
+ function writeEnvFile(filePath, env, keyOrder, executor) {
70
+ const rendered = renderEnvFile(env, keyOrder);
71
+ if (executor) {
72
+ executor.writeFile(filePath, rendered, { description: 'write env file' });
73
+ return;
74
+ }
75
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
76
+ node_fs_1.default.writeFileSync(filePath, rendered, 'utf8');
77
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CommandExecutionError = exports.CliError = void 0;
4
+ /**
5
+ * Base error class for all expected CLI failures.
6
+ * The top-level entrypoint catches these and exits with `exitCode`
7
+ * instead of printing an unhandled exception stack trace.
8
+ */
9
+ class CliError extends Error {
10
+ exitCode;
11
+ constructor(message, exitCode = 1) {
12
+ super(message);
13
+ this.name = 'CliError';
14
+ this.exitCode = exitCode;
15
+ }
16
+ }
17
+ exports.CliError = CliError;
18
+ /**
19
+ * Thrown when a subprocess exits with a non-zero status.
20
+ * Carries the full stdout/stderr for diagnostics.
21
+ */
22
+ class CommandExecutionError extends CliError {
23
+ details;
24
+ constructor(message, details) {
25
+ super(message, details.exitCode ?? 1);
26
+ this.name = 'CommandExecutionError';
27
+ this.details = details;
28
+ }
29
+ }
30
+ exports.CommandExecutionError = CommandExecutionError;