@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,249 @@
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.canonIngestStage = void 0;
7
+ exports.ingestCanonFiles = ingestCanonFiles;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const config_1 = require("../lib/config");
12
+ const canon_resolver_1 = require("../lib/canon-resolver");
13
+ const mcp_client_1 = require("../lib/mcp-client");
14
+ const service_health_1 = require("../lib/service-health");
15
+ const MAX_DOCS_PER_REQUEST = 100;
16
+ /**
17
+ * Recursively collects all `.md` files under a directory.
18
+ */
19
+ function collectMarkdownFiles(dir) {
20
+ if (!node_fs_1.default.existsSync(dir)) {
21
+ return [];
22
+ }
23
+ const results = [];
24
+ for (const entry of node_fs_1.default.readdirSync(dir, { withFileTypes: true })) {
25
+ const full = node_path_1.default.join(dir, entry.name);
26
+ if (entry.isDirectory()) {
27
+ results.push(...collectMarkdownFiles(full));
28
+ }
29
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
30
+ results.push(full);
31
+ }
32
+ }
33
+ return results;
34
+ }
35
+ /**
36
+ * Reads files and produces IngestDocument objects with paths relative to a base.
37
+ */
38
+ function filesToDocuments(files, baseDir) {
39
+ return files.map((f) => ({
40
+ path: node_path_1.default.relative(baseDir, f),
41
+ content: node_fs_1.default.readFileSync(f, 'utf8'),
42
+ }));
43
+ }
44
+ function chunkDocuments(documents, batchSize) {
45
+ const chunks = [];
46
+ for (let index = 0; index < documents.length; index += batchSize) {
47
+ chunks.push(documents.slice(index, index + batchSize));
48
+ }
49
+ return chunks;
50
+ }
51
+ /**
52
+ * Normalizes a repo identifier/url into an "owner/repo" slug when possible.
53
+ */
54
+ function normalizeGitHubRepoSlug(repo) {
55
+ return repo
56
+ .trim()
57
+ .replace(/\/+$/, '')
58
+ .replace(/\.git$/, '')
59
+ .replace(/^https?:\/\/github\.com\//i, '')
60
+ .replace(/^git@github\.com:/i, '')
61
+ .replace(/^ssh:\/\/git@github\.com\//i, '');
62
+ }
63
+ /**
64
+ * Derives a scope name from a business canon repo slug.
65
+ * e.g. "uxmaltech/my-app-architecture.git/" → "my-app-architecture"
66
+ */
67
+ function businessScopeFromRepo(repo) {
68
+ const normalized = normalizeGitHubRepoSlug(repo);
69
+ const scope = normalized.split('/').filter(Boolean).pop();
70
+ return scope ?? normalized;
71
+ }
72
+ function parseRepoIdentityFromRemote(remoteUrl) {
73
+ if (!/github\.com[:/]/i.test(remoteUrl)) {
74
+ return undefined;
75
+ }
76
+ const normalized = normalizeGitHubRepoSlug(remoteUrl);
77
+ const [organization, repoName] = normalized.split('/').filter(Boolean);
78
+ if (!organization || !repoName) {
79
+ return undefined;
80
+ }
81
+ return {
82
+ organization,
83
+ repo: `${organization}/${repoName}`,
84
+ scope: repoName,
85
+ };
86
+ }
87
+ function sanitizeScope(value, fallback) {
88
+ const normalized = value
89
+ .trim()
90
+ .replace(/\/+$/, '')
91
+ .replace(/\.git$/, '');
92
+ const scope = normalized.split('/').filter(Boolean).pop();
93
+ return scope && scope.length > 0 ? scope : fallback;
94
+ }
95
+ function resolveRepoIdentity(repoDir, fallbackScope) {
96
+ try {
97
+ const remoteUrl = (0, node_child_process_1.execFileSync)('git', ['-C', repoDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
98
+ const fromRemote = parseRepoIdentityFromRemote(remoteUrl);
99
+ if (fromRemote) {
100
+ return fromRemote;
101
+ }
102
+ }
103
+ catch {
104
+ // Fallback below when repo has no git origin or git is unavailable.
105
+ }
106
+ const scope = sanitizeScope(fallbackScope, 'repo');
107
+ return {
108
+ organization: 'local',
109
+ repo: `local/${scope}`,
110
+ scope,
111
+ };
112
+ }
113
+ async function ingestInBatches(baseUrl, payload, apiKey, timeoutMs) {
114
+ const chunks = chunkDocuments(payload.documents, MAX_DOCS_PER_REQUEST);
115
+ let totalFiles = 0;
116
+ let totalPoints = 0;
117
+ let totalNodes = 0;
118
+ let totalEdges = 0;
119
+ let collection = '';
120
+ let space = '';
121
+ for (const documents of chunks) {
122
+ const result = await (0, mcp_client_1.ingestDocuments)(baseUrl, { ...payload, documents }, apiKey, timeoutMs);
123
+ totalFiles += result.vector.ingested_files;
124
+ totalPoints += result.vector.total_points;
125
+ totalNodes += result.graph.nodes_created;
126
+ totalEdges += result.graph.edges_created;
127
+ collection = collection || result.vector.collection;
128
+ space = space || result.graph.space;
129
+ }
130
+ return {
131
+ vector: {
132
+ ingested_files: totalFiles,
133
+ total_points: totalPoints,
134
+ collection: collection || payload.scope,
135
+ },
136
+ graph: {
137
+ nodes_created: totalNodes,
138
+ edges_created: totalEdges,
139
+ space: space || payload.scope,
140
+ },
141
+ };
142
+ }
143
+ async function ingestCanonFiles(ctx) {
144
+ const baseUrl = (0, mcp_client_1.getMcpBaseUrl)(ctx.config);
145
+ const env = (0, service_health_1.loadRuntimeEnv)(ctx.config);
146
+ const apiKey = (0, mcp_client_1.resolveMcpApiKey)(env);
147
+ const timeoutMs = (0, mcp_client_1.resolveMcpHttpTimeoutMs)(env);
148
+ // --- Framework canon (uxmaltech) ---
149
+ const uxmaltechFiles = collectMarkdownFiles(ctx.config.uxmaltechDir);
150
+ if (uxmaltechFiles.length > 0) {
151
+ ctx.logger.info(`Ingesting ${uxmaltechFiles.length} framework architecture file(s) via HTTP.`);
152
+ const payload = {
153
+ context: 'technical',
154
+ scope: 'uxmaltech',
155
+ organization: 'uxmaltech',
156
+ repo: 'uxmaltech/collab-architecture',
157
+ documents: filesToDocuments(uxmaltechFiles, ctx.config.uxmaltechDir),
158
+ };
159
+ const result = await ingestInBatches(baseUrl, payload, apiKey, timeoutMs);
160
+ ctx.logger.info(`Framework canon ingested: ${result.vector.ingested_files} files, ` +
161
+ `${result.vector.total_points} vectors, ${result.graph.nodes_created} graph nodes.`);
162
+ }
163
+ else {
164
+ ctx.logger.info('No framework architecture files found to ingest.');
165
+ }
166
+ // --- Business canon ---
167
+ if ((0, canon_resolver_1.isBusinessCanonConfigured)(ctx.config)) {
168
+ const localDir = ctx.config.canons?.business?.localDir ?? 'business';
169
+ const businessDir = node_path_1.default.join(ctx.config.architectureDir, localDir);
170
+ const businessFiles = collectMarkdownFiles(businessDir);
171
+ if (businessFiles.length > 0) {
172
+ const configuredBusinessRepo = ctx.config.canons.business.repo;
173
+ const normalizedBusinessRepo = normalizeGitHubRepoSlug(configuredBusinessRepo);
174
+ const businessScope = businessScopeFromRepo(normalizedBusinessRepo);
175
+ const businessOrg = normalizedBusinessRepo.split('/')[0] ?? 'uxmaltech';
176
+ const businessRepo = normalizedBusinessRepo.includes('/')
177
+ ? normalizedBusinessRepo
178
+ : `${businessOrg}/${normalizedBusinessRepo}`;
179
+ ctx.logger.info(`Ingesting ${businessFiles.length} business architecture file(s) via HTTP.`);
180
+ const payload = {
181
+ context: 'technical',
182
+ scope: businessScope,
183
+ organization: businessOrg,
184
+ repo: businessRepo,
185
+ documents: filesToDocuments(businessFiles, businessDir),
186
+ };
187
+ const result = await ingestInBatches(baseUrl, payload, apiKey, timeoutMs);
188
+ ctx.logger.info(`Business canon ingested: ${result.vector.ingested_files} files, ` +
189
+ `${result.vector.total_points} vectors, ${result.graph.nodes_created} graph nodes.`);
190
+ }
191
+ else {
192
+ ctx.logger.info('No business architecture files found to ingest.');
193
+ }
194
+ }
195
+ // --- Repo architecture docs ---
196
+ if ((0, config_1.isWorkspaceMode)(ctx.config)) {
197
+ for (const rc of (0, config_1.resolveRepoConfigs)(ctx.config)) {
198
+ const repoFiles = collectMarkdownFiles(rc.architectureRepoDir);
199
+ if (repoFiles.length === 0) {
200
+ continue;
201
+ }
202
+ const repoIdentity = resolveRepoIdentity(rc.repoDir, rc.name);
203
+ ctx.logger.info(`Ingesting ${repoFiles.length} file(s) from repo ${repoIdentity.repo} via HTTP.`);
204
+ const payload = {
205
+ context: 'technical',
206
+ scope: repoIdentity.scope,
207
+ organization: repoIdentity.organization,
208
+ repo: repoIdentity.repo,
209
+ documents: filesToDocuments(repoFiles, rc.architectureRepoDir),
210
+ };
211
+ const result = await ingestInBatches(baseUrl, payload, apiKey, timeoutMs);
212
+ ctx.logger.info(`Repo ${repoIdentity.repo} ingested: ${result.vector.ingested_files} files, ` +
213
+ `${result.vector.total_points} vectors.`);
214
+ }
215
+ return;
216
+ }
217
+ // Non-workspace mode: ingest local repo architecture docs (`docs/architecture/repo`).
218
+ const repoFiles = collectMarkdownFiles(ctx.config.repoDir);
219
+ if (repoFiles.length > 0) {
220
+ const fallbackScope = node_path_1.default.basename(ctx.config.workspaceDir);
221
+ const repoIdentity = resolveRepoIdentity(ctx.config.workspaceDir, fallbackScope);
222
+ ctx.logger.info(`Ingesting ${repoFiles.length} file(s) from local repo architecture via HTTP.`);
223
+ const payload = {
224
+ context: 'technical',
225
+ scope: repoIdentity.scope,
226
+ organization: repoIdentity.organization,
227
+ repo: repoIdentity.repo,
228
+ documents: filesToDocuments(repoFiles, ctx.config.repoDir),
229
+ };
230
+ const result = await ingestInBatches(baseUrl, payload, apiKey, timeoutMs);
231
+ ctx.logger.info(`Repo architecture ingested: ${result.vector.ingested_files} files, ` +
232
+ `${result.vector.total_points} vectors.`);
233
+ }
234
+ }
235
+ exports.canonIngestStage = {
236
+ id: 'canon-ingest',
237
+ title: 'Ingest canonical architecture files into MCP',
238
+ recovery: [
239
+ 'Ensure MCP service is running and accessible.',
240
+ 'Run collab init --resume to retry canon ingestion.',
241
+ ],
242
+ run: async (ctx) => {
243
+ if (ctx.executor.dryRun) {
244
+ ctx.logger.info('[dry-run] Would ingest architecture files into MCP via HTTP.');
245
+ return;
246
+ }
247
+ await ingestCanonFiles(ctx);
248
+ },
249
+ };
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonRebuildGraphStage = void 0;
4
+ const mcp_client_1 = require("../lib/mcp-client");
5
+ const service_health_1 = require("../lib/service-health");
6
+ const errors_1 = require("../lib/errors");
7
+ exports.canonRebuildGraphStage = {
8
+ id: 'canon-rebuild-graph',
9
+ title: 'Rebuild NebulaGraph seeds via MCP',
10
+ recovery: [
11
+ 'Ensure infrastructure is running: collab infra up',
12
+ 'Ensure MCP server is running: collab mcp start',
13
+ 'Run collab canon rebuild --confirm --graph to retry.',
14
+ ],
15
+ run: async (ctx) => {
16
+ if (ctx.executor.dryRun) {
17
+ ctx.logger.info('[dry-run] Would re-seed NebulaGraph via MCP HTTP endpoint.');
18
+ return;
19
+ }
20
+ const env = (0, service_health_1.loadRuntimeEnv)(ctx.config);
21
+ // Pre-check: is MCP reachable?
22
+ const health = await (0, service_health_1.waitForMcpHealth)(env, service_health_1.REBUILD_HEALTH_OPTIONS);
23
+ if (!health.ok) {
24
+ throw new errors_1.CliError('MCP server is not reachable. Start it with: collab mcp start');
25
+ }
26
+ const baseUrl = (0, mcp_client_1.getMcpBaseUrl)(ctx.config);
27
+ const apiKey = (0, mcp_client_1.resolveMcpApiKey)(env);
28
+ const timeoutMs = (0, mcp_client_1.resolveMcpHttpTimeoutMs)(env);
29
+ ctx.logger.info('Triggering NebulaGraph re-seed via MCP...');
30
+ const result = await (0, mcp_client_1.triggerGraphSeed)(baseUrl, apiKey, timeoutMs);
31
+ ctx.logger.info(`Graph re-seed complete: ${result.nodes_created} nodes, ${result.edges_created} edges.`);
32
+ },
33
+ };
@@ -0,0 +1,40 @@
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.canonRebuildIndexesStage = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const canon_index_generator_1 = require("../lib/canon-index-generator");
9
+ const canon_index_targets_1 = require("../lib/canon-index-targets");
10
+ exports.canonRebuildIndexesStage = {
11
+ id: 'canon-rebuild-indexes',
12
+ title: 'Regenerate canon index files',
13
+ recovery: [
14
+ 'Verify write permissions for the architecture directory.',
15
+ 'Run collab canon rebuild --confirm --indexes to retry.',
16
+ ],
17
+ run: (ctx) => {
18
+ const archDir = ctx.config.architectureDir;
19
+ const targets = (0, canon_index_targets_1.getCanonIndexTargets)(archDir);
20
+ if (ctx.executor.dryRun) {
21
+ ctx.logger.info(`[dry-run] Would regenerate ${targets.length} index files under ${archDir}.`);
22
+ for (const t of targets) {
23
+ ctx.logger.info(` ${node_path_1.default.relative(archDir, t.outputFile)}`);
24
+ }
25
+ return;
26
+ }
27
+ let regenerated = 0;
28
+ for (const target of targets) {
29
+ const entries = (0, canon_index_generator_1.scanCanonEntries)(target.scanDir);
30
+ const content = (0, canon_index_generator_1.generateIndexReadme)(target.sectionTitle, target.description, entries);
31
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(target.outputFile));
32
+ ctx.executor.writeFile(target.outputFile, content, {
33
+ description: `rebuild index ${target.sectionTitle}`,
34
+ });
35
+ ctx.logger.info(`Rebuilt ${node_path_1.default.relative(archDir, target.outputFile)} (${entries.length} entries)`);
36
+ regenerated++;
37
+ }
38
+ ctx.logger.info(`Index rebuild complete: ${regenerated} files regenerated.`);
39
+ },
40
+ };
@@ -0,0 +1,75 @@
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.canonRebuildSnapshotStage = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const canon_index_targets_1 = require("../lib/canon-index-targets");
10
+ /**
11
+ * Recursively copies files from sourceDir into snapshotDir using the executor.
12
+ * Reads go through `fs` directly (the Executor API only wraps side-effect
13
+ * operations: commands, writes, mkdirs), writes go through `ctx.executor`.
14
+ * Returns the number of files copied.
15
+ */
16
+ function snapshotDirectory(sourceDir, snapshotDir, ctx) {
17
+ if (!node_fs_1.default.existsSync(sourceDir))
18
+ return 0;
19
+ let count = 0;
20
+ const entries = node_fs_1.default.readdirSync(sourceDir, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ const src = node_path_1.default.join(sourceDir, entry.name);
23
+ const dest = node_path_1.default.join(snapshotDir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ ctx.executor.ensureDirectory(dest);
26
+ count += snapshotDirectory(src, dest, ctx);
27
+ }
28
+ else if (entry.isFile()) {
29
+ const content = node_fs_1.default.readFileSync(src, 'utf8');
30
+ ctx.executor.writeFile(dest, content, { description: `snapshot ${entry.name}` });
31
+ count++;
32
+ }
33
+ }
34
+ return count;
35
+ }
36
+ exports.canonRebuildSnapshotStage = {
37
+ id: 'canon-rebuild-snapshot',
38
+ title: 'Create pre-rebuild snapshot',
39
+ recovery: [
40
+ 'Check .collab/snapshots/ for existing snapshots.',
41
+ ],
42
+ run: (ctx) => {
43
+ if (ctx.executor.dryRun) {
44
+ ctx.logger.info('[dry-run] Would create snapshot of current canon artifacts.');
45
+ return;
46
+ }
47
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
48
+ const snapshotBase = node_path_1.default.join(ctx.config.collabDir, 'snapshots', timestamp);
49
+ ctx.executor.ensureDirectory(snapshotBase);
50
+ let totalFiles = 0;
51
+ // Snapshot index README files (derived from shared canon-index-targets)
52
+ const indexPaths = (0, canon_index_targets_1.getSnapshotIndexPaths)(ctx.config.architectureDir);
53
+ for (const relPath of indexPaths) {
54
+ const src = node_path_1.default.join(ctx.config.architectureDir, relPath);
55
+ if (node_fs_1.default.existsSync(src)) {
56
+ const dest = node_path_1.default.join(snapshotBase, 'indexes', relPath);
57
+ ctx.executor.ensureDirectory(node_path_1.default.dirname(dest));
58
+ ctx.executor.writeFile(dest, node_fs_1.default.readFileSync(src, 'utf8'), {
59
+ description: `snapshot index ${relPath}`,
60
+ });
61
+ totalFiles++;
62
+ }
63
+ }
64
+ // Snapshot graph seed files (indexed mode only)
65
+ if (ctx.config.mode === 'indexed') {
66
+ const graphSeedDir = node_path_1.default.join(ctx.config.uxmaltechDir, 'graph', 'seed');
67
+ if (node_fs_1.default.existsSync(graphSeedDir)) {
68
+ const snapshotGraphDir = node_path_1.default.join(snapshotBase, 'graph-seed');
69
+ ctx.executor.ensureDirectory(snapshotGraphDir);
70
+ totalFiles += snapshotDirectory(graphSeedDir, snapshotGraphDir, ctx);
71
+ }
72
+ }
73
+ ctx.logger.info(`Snapshot created: ${snapshotBase} (${totalFiles} file(s))`);
74
+ },
75
+ };
@@ -0,0 +1,57 @@
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.canonRebuildValidateStage = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const canon_index_targets_1 = require("../lib/canon-index-targets");
10
+ function validateIndexFiles(ctx) {
11
+ const errors = [];
12
+ const archDir = ctx.config.architectureDir;
13
+ const targets = (0, canon_index_targets_1.getCanonIndexTargets)(archDir);
14
+ for (const target of targets) {
15
+ const fullPath = target.outputFile;
16
+ const relPath = node_path_1.default.relative(archDir, fullPath);
17
+ if (!node_fs_1.default.existsSync(fullPath)) {
18
+ errors.push(`Missing index file: ${relPath}`);
19
+ continue;
20
+ }
21
+ const content = node_fs_1.default.readFileSync(fullPath, 'utf8');
22
+ if (content.trim().length === 0) {
23
+ errors.push(`Empty index file: ${relPath}`);
24
+ }
25
+ }
26
+ return errors;
27
+ }
28
+ exports.canonRebuildValidateStage = {
29
+ id: 'canon-rebuild-validate',
30
+ title: 'Validate rebuilt artifacts',
31
+ recovery: [
32
+ 'Review validation errors above.',
33
+ 'Run collab canon rebuild --confirm to retry the full rebuild.',
34
+ ],
35
+ run: (ctx) => {
36
+ if (ctx.executor.dryRun) {
37
+ ctx.logger.info('[dry-run] Would validate rebuilt canon artifacts.');
38
+ return;
39
+ }
40
+ const rebuildIndexes = ctx.options?.rebuildIndexes;
41
+ const errors = [];
42
+ if (rebuildIndexes) {
43
+ errors.push(...validateIndexFiles(ctx));
44
+ }
45
+ if (errors.length > 0) {
46
+ ctx.logger.warn(`Validation found ${errors.length} issue(s):`);
47
+ for (const e of errors) {
48
+ ctx.logger.warn(` - ${e}`);
49
+ }
50
+ // Warn but do not fail — missing directories may be expected
51
+ // in workspaces that haven't scaffolded all categories yet.
52
+ }
53
+ else {
54
+ ctx.logger.info('Post-rebuild validation passed.');
55
+ }
56
+ },
57
+ };
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonRebuildVectorsStage = void 0;
4
+ const service_health_1 = require("../lib/service-health");
5
+ const errors_1 = require("../lib/errors");
6
+ const canon_ingest_1 = require("./canon-ingest");
7
+ exports.canonRebuildVectorsStage = {
8
+ id: 'canon-rebuild-vectors',
9
+ title: 'Rebuild vector embeddings via MCP',
10
+ recovery: [
11
+ 'Ensure infrastructure is running: collab infra up',
12
+ 'Ensure MCP server is running: collab mcp start',
13
+ 'Run collab canon rebuild --confirm --vectors to retry.',
14
+ ],
15
+ run: async (ctx) => {
16
+ if (ctx.executor.dryRun) {
17
+ ctx.logger.info('[dry-run] Would re-ingest all canon files into Qdrant via MCP HTTP.');
18
+ return;
19
+ }
20
+ const env = (0, service_health_1.loadRuntimeEnv)(ctx.config);
21
+ // Pre-check: is MCP reachable?
22
+ const health = await (0, service_health_1.waitForMcpHealth)(env, service_health_1.REBUILD_HEALTH_OPTIONS);
23
+ if (!health.ok) {
24
+ throw new errors_1.CliError('MCP server is not reachable. Start it with: collab mcp start');
25
+ }
26
+ ctx.logger.info('Re-ingesting all canon files into Qdrant vector store...');
27
+ await (0, canon_ingest_1.ingestCanonFiles)(ctx);
28
+ ctx.logger.info('Vector embedding rebuild complete.');
29
+ },
30
+ };
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonScaffoldStage = void 0;
4
+ const canon_scaffold_1 = require("../lib/canon-scaffold");
5
+ exports.canonScaffoldStage = {
6
+ id: 'canon-scaffold',
7
+ title: 'Generate canonical architecture scaffold',
8
+ recovery: [
9
+ 'Verify write permissions for docs/architecture directory.',
10
+ 'Run collab init --resume to retry scaffold generation.',
11
+ ],
12
+ run: (ctx) => {
13
+ (0, canon_scaffold_1.generateCanonScaffold)(ctx);
14
+ },
15
+ };
@@ -0,0 +1,49 @@
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.canonSyncStage = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const canon_resolver_1 = require("../lib/canon-resolver");
9
+ const github_auth_1 = require("../lib/github-auth");
10
+ exports.canonSyncStage = {
11
+ id: 'canon-sync',
12
+ title: 'Sync canonical architecture',
13
+ recovery: [
14
+ 'Verify internet connectivity for git clone/pull.',
15
+ 'Run collab update-canons manually, then collab init --resume.',
16
+ ],
17
+ run: (ctx) => {
18
+ if (ctx.executor.dryRun) {
19
+ ctx.logger.info('[dry-run] Would sync canons and copy to uxmaltech/.');
20
+ return;
21
+ }
22
+ // ── Framework canon ──────────────────────────────────────
23
+ if (!(0, canon_resolver_1.isCanonsAvailable)()) {
24
+ ctx.logger.info('Canons not installed. Downloading collab-architecture...');
25
+ }
26
+ const ok = (0, canon_resolver_1.syncCanons)((msg) => ctx.logger.info(msg));
27
+ if (!ok || !(0, canon_resolver_1.isCanonsAvailable)()) {
28
+ throw new Error('Failed to sync collab-architecture canons. Check git access and connectivity.');
29
+ }
30
+ const targetDir = ctx.config.uxmaltechDir;
31
+ ctx.logger.info(`Copying framework canon to ${targetDir}...`);
32
+ const fileCount = (0, canon_resolver_1.copyCanonContent)(targetDir, (msg) => ctx.logger.debug(msg));
33
+ ctx.logger.info(`Framework canon sync: ${fileCount} file(s) copied to uxmaltech/.`);
34
+ // ── Business canon ───────────────────────────────────────
35
+ if ((0, canon_resolver_1.isBusinessCanonConfigured)(ctx.config)) {
36
+ const auth = (0, github_auth_1.loadGitHubAuth)(ctx.config.collabDir);
37
+ const token = auth?.token;
38
+ const bizOk = (0, canon_resolver_1.syncBusinessCanon)(ctx.config, (msg) => ctx.logger.info(msg), token);
39
+ if (!bizOk) {
40
+ throw new Error('Failed to sync business canon. Check repo access and connectivity.');
41
+ }
42
+ const localDir = ctx.config.canons?.business?.localDir ?? 'business';
43
+ const bizTargetDir = node_path_1.default.join(ctx.config.architectureDir, localDir);
44
+ ctx.logger.info(`Copying business canon to ${bizTargetDir}...`);
45
+ const bizFileCount = (0, canon_resolver_1.copyBusinessCanonContent)(ctx.config, bizTargetDir, (msg) => ctx.logger.debug(msg));
46
+ ctx.logger.info(`Business canon sync: ${bizFileCount} file(s) copied to ${localDir}/.`);
47
+ }
48
+ },
49
+ };
@@ -0,0 +1,56 @@
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.ciSetupStage = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const orchestrator_1 = require("../lib/orchestrator");
10
+ const ci_1 = require("../templates/ci");
11
+ exports.ciSetupStage = {
12
+ id: 'ci-setup',
13
+ title: 'Generate CI workflow files for architecture',
14
+ recovery: [
15
+ 'Verify write permissions for .github/workflows/ directory.',
16
+ 'Run collab init --resume to retry CI setup.',
17
+ ],
18
+ run: (ctx) => {
19
+ if (ctx.options?.skipCi) {
20
+ ctx.logger.info('Skipping CI workflow generation by user choice.');
21
+ return;
22
+ }
23
+ const workflowDir = node_path_1.default.join((0, orchestrator_1.getRepoBaseDir)(ctx), '.github', 'workflows');
24
+ let created = 0;
25
+ let skipped = 0;
26
+ // PR workflow — both modes
27
+ const prFile = node_path_1.default.join(workflowDir, 'architecture-pr.yml');
28
+ if (!ctx.executor.dryRun && node_fs_1.default.existsSync(prFile)) {
29
+ ctx.logger.debug('PR workflow already exists, skipping: architecture-pr.yml');
30
+ skipped++;
31
+ }
32
+ else {
33
+ ctx.executor.ensureDirectory(workflowDir);
34
+ ctx.executor.writeFile(prFile, ci_1.architecturePrTemplate, {
35
+ description: 'write architecture PR validation workflow',
36
+ });
37
+ created++;
38
+ }
39
+ // Merge workflow — indexed mode only
40
+ if (ctx.config.mode === 'indexed') {
41
+ const mergeFile = node_path_1.default.join(workflowDir, 'architecture-merge.yml');
42
+ if (!ctx.executor.dryRun && node_fs_1.default.existsSync(mergeFile)) {
43
+ ctx.logger.debug('Merge workflow already exists, skipping: architecture-merge.yml');
44
+ skipped++;
45
+ }
46
+ else {
47
+ ctx.executor.ensureDirectory(workflowDir);
48
+ ctx.executor.writeFile(mergeFile, ci_1.architectureMergeTemplate, {
49
+ description: 'write architecture merge ingestion workflow',
50
+ });
51
+ created++;
52
+ }
53
+ }
54
+ ctx.logger.info(`CI workflows: ${created} file(s) created, ${skipped} existing file(s) preserved.`);
55
+ },
56
+ };