@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,317 @@
|
|
|
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.createAiClient = createAiClient;
|
|
7
|
+
exports.createFirstAvailableClient = createFirstAvailableClient;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_https_1 = __importDefault(require("node:https"));
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const node_url_1 = require("node:url");
|
|
14
|
+
const credentials_1 = require("./credentials");
|
|
15
|
+
const providers_1 = require("./providers");
|
|
16
|
+
/**
|
|
17
|
+
* Makes an HTTPS POST request and returns the response body as a string.
|
|
18
|
+
*/
|
|
19
|
+
function httpsPost(url, headers, body, timeoutMs = 120_000) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const parsed = new node_url_1.URL(url);
|
|
22
|
+
const req = node_https_1.default.request({
|
|
23
|
+
hostname: parsed.hostname,
|
|
24
|
+
port: parsed.port || 443,
|
|
25
|
+
path: parsed.pathname + parsed.search,
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
...headers,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
'Content-Length': Buffer.byteLength(body).toString(),
|
|
31
|
+
},
|
|
32
|
+
}, (res) => {
|
|
33
|
+
let data = '';
|
|
34
|
+
res.on('data', (chunk) => {
|
|
35
|
+
data += chunk.toString();
|
|
36
|
+
});
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
resolve({ statusCode: res.statusCode ?? 0, body: data });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.setTimeout(timeoutMs, () => {
|
|
42
|
+
req.destroy(new Error('AI API request timed out'));
|
|
43
|
+
});
|
|
44
|
+
req.on('error', reject);
|
|
45
|
+
req.write(body);
|
|
46
|
+
req.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the API key for a provider.
|
|
51
|
+
*
|
|
52
|
+
* Resolution order:
|
|
53
|
+
* 1. Environment variable (e.g. OPENAI_API_KEY)
|
|
54
|
+
* 2. Stored credential from .collab/credentials.json
|
|
55
|
+
*/
|
|
56
|
+
function resolveApiKey(provider, config) {
|
|
57
|
+
const defaults = providers_1.PROVIDER_DEFAULTS[provider];
|
|
58
|
+
// 1. Check environment variable
|
|
59
|
+
const envKey = process.env[defaults.envVar];
|
|
60
|
+
if (envKey) {
|
|
61
|
+
return envKey;
|
|
62
|
+
}
|
|
63
|
+
// 2. Check stored credentials
|
|
64
|
+
const storedKey = (0, credentials_1.loadApiKey)(config, provider);
|
|
65
|
+
if (storedKey) {
|
|
66
|
+
return storedKey;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// ---------- OpenAI ----------
|
|
71
|
+
function createOpenAiClient(apiKey) {
|
|
72
|
+
return {
|
|
73
|
+
provider: 'codex',
|
|
74
|
+
async complete(messages, options = {}) {
|
|
75
|
+
const model = options.model ?? providers_1.PROVIDER_DEFAULTS.codex.models[0];
|
|
76
|
+
const body = JSON.stringify({
|
|
77
|
+
model,
|
|
78
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
79
|
+
max_completion_tokens: options.maxTokens ?? 4096,
|
|
80
|
+
temperature: options.temperature ?? 0.2,
|
|
81
|
+
});
|
|
82
|
+
const res = await httpsPost('https://api.openai.com/v1/chat/completions', {
|
|
83
|
+
Authorization: `Bearer ${apiKey}`,
|
|
84
|
+
Accept: 'application/json',
|
|
85
|
+
}, body);
|
|
86
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
87
|
+
throw new Error(`OpenAI API error (${res.statusCode}): ${res.body}`);
|
|
88
|
+
}
|
|
89
|
+
const parsed = JSON.parse(res.body);
|
|
90
|
+
return parsed.choices?.[0]?.message?.content ?? '';
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ---------- Anthropic ----------
|
|
95
|
+
function createAnthropicClient(apiKey) {
|
|
96
|
+
return {
|
|
97
|
+
provider: 'claude',
|
|
98
|
+
async complete(messages, options = {}) {
|
|
99
|
+
const model = options.model ?? providers_1.PROVIDER_DEFAULTS.claude.models[0];
|
|
100
|
+
// Anthropic API uses a system parameter, not a system message
|
|
101
|
+
const systemMessage = messages.find((m) => m.role === 'system');
|
|
102
|
+
const userMessages = messages.filter((m) => m.role !== 'system');
|
|
103
|
+
const body = JSON.stringify({
|
|
104
|
+
model,
|
|
105
|
+
max_tokens: options.maxTokens ?? 4096,
|
|
106
|
+
...(systemMessage ? { system: systemMessage.content } : {}),
|
|
107
|
+
messages: userMessages.map((m) => ({ role: m.role, content: m.content })),
|
|
108
|
+
});
|
|
109
|
+
const res = await httpsPost('https://api.anthropic.com/v1/messages', {
|
|
110
|
+
'x-api-key': apiKey,
|
|
111
|
+
'anthropic-version': '2023-06-01',
|
|
112
|
+
Accept: 'application/json',
|
|
113
|
+
}, body);
|
|
114
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
115
|
+
throw new Error(`Anthropic API error (${res.statusCode}): ${res.body}`);
|
|
116
|
+
}
|
|
117
|
+
const parsed = JSON.parse(res.body);
|
|
118
|
+
const textBlock = parsed.content?.find((b) => b.type === 'text');
|
|
119
|
+
return textBlock?.text ?? '';
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ---------- Gemini ----------
|
|
124
|
+
function createGeminiClient(apiKey) {
|
|
125
|
+
return {
|
|
126
|
+
provider: 'gemini',
|
|
127
|
+
async complete(messages, options = {}) {
|
|
128
|
+
const model = options.model ?? providers_1.PROVIDER_DEFAULTS.gemini.models[0];
|
|
129
|
+
// Gemini uses a different message format
|
|
130
|
+
const systemInstruction = messages.find((m) => m.role === 'system');
|
|
131
|
+
const contents = messages
|
|
132
|
+
.filter((m) => m.role !== 'system')
|
|
133
|
+
.map((m) => ({
|
|
134
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
135
|
+
parts: [{ text: m.content }],
|
|
136
|
+
}));
|
|
137
|
+
const body = JSON.stringify({
|
|
138
|
+
...(systemInstruction
|
|
139
|
+
? { system_instruction: { parts: [{ text: systemInstruction.content }] } }
|
|
140
|
+
: {}),
|
|
141
|
+
contents,
|
|
142
|
+
generationConfig: {
|
|
143
|
+
maxOutputTokens: options.maxTokens ?? 4096,
|
|
144
|
+
temperature: options.temperature ?? 0.2,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
148
|
+
const res = await httpsPost(url, { Accept: 'application/json' }, body);
|
|
149
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
150
|
+
throw new Error(`Gemini API error (${res.statusCode}): ${res.body}`);
|
|
151
|
+
}
|
|
152
|
+
const parsed = JSON.parse(res.body);
|
|
153
|
+
return parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ---------- CLI-based clients ----------
|
|
158
|
+
/**
|
|
159
|
+
* Combines system and user messages into a single prompt string
|
|
160
|
+
* for CLI tools that don't support separate message roles.
|
|
161
|
+
*/
|
|
162
|
+
function buildCombinedPrompt(messages) {
|
|
163
|
+
const parts = [];
|
|
164
|
+
for (const msg of messages) {
|
|
165
|
+
if (msg.role === 'system') {
|
|
166
|
+
parts.push(msg.content);
|
|
167
|
+
}
|
|
168
|
+
else if (msg.role === 'user') {
|
|
169
|
+
parts.push('---\n\n' + msg.content);
|
|
170
|
+
}
|
|
171
|
+
else if (msg.role === 'assistant') {
|
|
172
|
+
parts.push('Assistant: ' + msg.content);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.join('\n\n');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Executes codex CLI for completion using `codex exec`.
|
|
179
|
+
*/
|
|
180
|
+
function execCodexCli(prompt, model) {
|
|
181
|
+
const tmpFile = node_path_1.default.join(node_os_1.default.tmpdir(), `collab-analysis-${Date.now()}.txt`);
|
|
182
|
+
try {
|
|
183
|
+
const args = [
|
|
184
|
+
'exec',
|
|
185
|
+
'--ephemeral',
|
|
186
|
+
'--sandbox', 'read-only',
|
|
187
|
+
'--skip-git-repo-check',
|
|
188
|
+
'-o', tmpFile,
|
|
189
|
+
];
|
|
190
|
+
if (model) {
|
|
191
|
+
args.push('-m', model);
|
|
192
|
+
}
|
|
193
|
+
args.push(prompt);
|
|
194
|
+
(0, node_child_process_1.execFileSync)('codex', args, {
|
|
195
|
+
encoding: 'utf8',
|
|
196
|
+
timeout: 300_000,
|
|
197
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
198
|
+
});
|
|
199
|
+
if (node_fs_1.default.existsSync(tmpFile)) {
|
|
200
|
+
return node_fs_1.default.readFileSync(tmpFile, 'utf8').trim();
|
|
201
|
+
}
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
try {
|
|
206
|
+
if (node_fs_1.default.existsSync(tmpFile)) {
|
|
207
|
+
node_fs_1.default.unlinkSync(tmpFile);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Ignore cleanup errors
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Executes claude CLI for completion using `claude -p`.
|
|
217
|
+
*/
|
|
218
|
+
function execClaudeCli(prompt, model) {
|
|
219
|
+
const args = ['-p'];
|
|
220
|
+
if (model) {
|
|
221
|
+
args.push('--model', model);
|
|
222
|
+
}
|
|
223
|
+
args.push(prompt);
|
|
224
|
+
const output = (0, node_child_process_1.execFileSync)('claude', args, {
|
|
225
|
+
encoding: 'utf8',
|
|
226
|
+
timeout: 300_000,
|
|
227
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
228
|
+
});
|
|
229
|
+
return output.trim();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Executes gemini CLI for completion.
|
|
233
|
+
*/
|
|
234
|
+
function execGeminiCli(prompt, model) {
|
|
235
|
+
const args = [];
|
|
236
|
+
if (model) {
|
|
237
|
+
args.push('--model', model);
|
|
238
|
+
}
|
|
239
|
+
args.push(prompt);
|
|
240
|
+
const output = (0, node_child_process_1.execFileSync)('gemini', args, {
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
timeout: 300_000,
|
|
243
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
244
|
+
});
|
|
245
|
+
return output.trim();
|
|
246
|
+
}
|
|
247
|
+
const CLI_EXECUTORS = {
|
|
248
|
+
codex: execCodexCli,
|
|
249
|
+
claude: execClaudeCli,
|
|
250
|
+
gemini: execGeminiCli,
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* Creates an AI client that uses the provider's official CLI for completion.
|
|
254
|
+
* This allows repo analysis to work without API keys.
|
|
255
|
+
*/
|
|
256
|
+
function createCliClient(provider, model) {
|
|
257
|
+
return {
|
|
258
|
+
provider,
|
|
259
|
+
complete(messages, options = {}) {
|
|
260
|
+
const effectiveModel = options.model ?? model;
|
|
261
|
+
const prompt = buildCombinedPrompt(messages);
|
|
262
|
+
const executor = CLI_EXECUTORS[provider];
|
|
263
|
+
if (!executor) {
|
|
264
|
+
throw new Error(`No CLI executor available for provider '${provider}'.`);
|
|
265
|
+
}
|
|
266
|
+
return Promise.resolve(executor(prompt, effectiveModel));
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// ---------- Factory ----------
|
|
271
|
+
const CLIENT_FACTORIES = {
|
|
272
|
+
codex: createOpenAiClient,
|
|
273
|
+
claude: createAnthropicClient,
|
|
274
|
+
gemini: createGeminiClient,
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* Creates an AI client for the given provider, resolving auth automatically.
|
|
278
|
+
*
|
|
279
|
+
* Resolution order:
|
|
280
|
+
* 1. API key (env var or stored credentials) → use HTTP API
|
|
281
|
+
* 2. CLI auth configured with CLI available → use CLI executable
|
|
282
|
+
* 3. null if neither available
|
|
283
|
+
*/
|
|
284
|
+
function createAiClient(provider, config, logger) {
|
|
285
|
+
// Copilot doesn't support AI completion — it works via GitHub issues
|
|
286
|
+
if (provider === 'copilot') {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
// 1. Try API key first
|
|
290
|
+
const factory = CLIENT_FACTORIES[provider];
|
|
291
|
+
const apiKey = resolveApiKey(provider, config);
|
|
292
|
+
if (apiKey && factory) {
|
|
293
|
+
return factory(apiKey);
|
|
294
|
+
}
|
|
295
|
+
// 2. Try CLI auth if configured
|
|
296
|
+
const providerConfig = config.assistants?.providers?.[provider];
|
|
297
|
+
if (providerConfig?.auth?.method === 'cli' && providerConfig?.cli?.available) {
|
|
298
|
+
const model = providerConfig.model ?? providerConfig.cli.configuredModel;
|
|
299
|
+
logger.debug(`Using ${providerConfig.cli.command} CLI for ${providers_1.PROVIDER_DEFAULTS[provider].label} completion.`);
|
|
300
|
+
return createCliClient(provider, model);
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Creates an AI client for the first available provider.
|
|
306
|
+
* Tries providers in order: codex, claude, gemini.
|
|
307
|
+
*/
|
|
308
|
+
function createFirstAvailableClient(providers, config, logger) {
|
|
309
|
+
for (const provider of providers) {
|
|
310
|
+
const client = createAiClient(provider, config, logger);
|
|
311
|
+
if (client) {
|
|
312
|
+
logger.info(`Using ${providers_1.PROVIDER_DEFAULTS[provider].label} for repository analysis.`);
|
|
313
|
+
return client;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}
|
package/dist/lib/ansi.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ANSI escape code utilities for terminal output.
|
|
4
|
+
* Zero external dependencies — uses raw escape sequences.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CLEAR_SCREEN = exports.BULLET = exports.CROSS = exports.CHECK = void 0;
|
|
8
|
+
exports.bold = bold;
|
|
9
|
+
exports.dim = dim;
|
|
10
|
+
exports.green = green;
|
|
11
|
+
exports.red = red;
|
|
12
|
+
exports.yellow = yellow;
|
|
13
|
+
exports.cyan = cyan;
|
|
14
|
+
exports.gray = gray;
|
|
15
|
+
const ESC = '\x1b[';
|
|
16
|
+
const RESET = `${ESC}0m`;
|
|
17
|
+
function supportsColor() {
|
|
18
|
+
if (process.env['NO_COLOR'] !== undefined) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (process.env['FORCE_COLOR'] !== undefined) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return Boolean(process.stdout.isTTY);
|
|
25
|
+
}
|
|
26
|
+
const colorEnabled = supportsColor();
|
|
27
|
+
function wrap(code, text) {
|
|
28
|
+
if (!colorEnabled) {
|
|
29
|
+
return text;
|
|
30
|
+
}
|
|
31
|
+
return `${ESC}${code}m${text}${RESET}`;
|
|
32
|
+
}
|
|
33
|
+
function bold(text) {
|
|
34
|
+
return wrap('1', text);
|
|
35
|
+
}
|
|
36
|
+
function dim(text) {
|
|
37
|
+
return wrap('2', text);
|
|
38
|
+
}
|
|
39
|
+
function green(text) {
|
|
40
|
+
return wrap('32', text);
|
|
41
|
+
}
|
|
42
|
+
function red(text) {
|
|
43
|
+
return wrap('31', text);
|
|
44
|
+
}
|
|
45
|
+
function yellow(text) {
|
|
46
|
+
return wrap('33', text);
|
|
47
|
+
}
|
|
48
|
+
function cyan(text) {
|
|
49
|
+
return wrap('36', text);
|
|
50
|
+
}
|
|
51
|
+
function gray(text) {
|
|
52
|
+
return wrap('90', text);
|
|
53
|
+
}
|
|
54
|
+
exports.CHECK = '\u2713';
|
|
55
|
+
exports.CROSS = '\u2717';
|
|
56
|
+
exports.BULLET = '\u2022';
|
|
57
|
+
/** ANSI: clear entire screen and move cursor to top-left. */
|
|
58
|
+
exports.CLEAR_SCREEN = '\x1b[2J\x1b[H';
|
|
@@ -0,0 +1,64 @@
|
|
|
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.scanCanonEntries = scanCanonEntries;
|
|
7
|
+
exports.generateIndexReadme = generateIndexReadme;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
/**
|
|
11
|
+
* Scans .md files in a directory and extracts canon entry metadata.
|
|
12
|
+
* Parses the heading (# ID Title) and plain-text Status/Confidence fields.
|
|
13
|
+
* Skips README.md files.
|
|
14
|
+
*/
|
|
15
|
+
function scanCanonEntries(dir) {
|
|
16
|
+
if (!node_fs_1.default.existsSync(dir))
|
|
17
|
+
return [];
|
|
18
|
+
const entries = [];
|
|
19
|
+
const files = node_fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md');
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(dir, file), 'utf8');
|
|
22
|
+
// Parse heading: "# AX-001 Authoritative Canon" or "# ADR-006 Collab: ..."
|
|
23
|
+
const heading = content.match(/^#\s+(.+)/m);
|
|
24
|
+
if (!heading)
|
|
25
|
+
continue;
|
|
26
|
+
const headingText = heading[1].trim();
|
|
27
|
+
// Match "ID Title" where ID is like AX-001, ADR-006, CN-002, AP-003, UIC-001, etc.
|
|
28
|
+
const parts = headingText.match(/^([A-Z]+-\d+)\s+(.+)$/);
|
|
29
|
+
const id = parts?.[1] ?? file.replace(/\.md$/, '');
|
|
30
|
+
const title = parts?.[2] ?? headingText;
|
|
31
|
+
// Parse plain-text fields (not bold)
|
|
32
|
+
const confidence = content.match(/^Confidence:\s*(\w+)/mi)?.[1] ?? 'unknown';
|
|
33
|
+
const status = content.match(/^Status:\s*(\w+)/mi)?.[1] ?? 'active';
|
|
34
|
+
entries.push({ id, title, confidence, status, fileName: file });
|
|
35
|
+
}
|
|
36
|
+
return entries.sort((a, b) => a.id.localeCompare(b.id));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Generates a README.md index table from scanned canon entries.
|
|
40
|
+
*/
|
|
41
|
+
function generateIndexReadme(sectionTitle, description, entries) {
|
|
42
|
+
const lines = [
|
|
43
|
+
`# ${sectionTitle}`,
|
|
44
|
+
'',
|
|
45
|
+
`> ${description}`,
|
|
46
|
+
'',
|
|
47
|
+
];
|
|
48
|
+
if (entries.length === 0) {
|
|
49
|
+
lines.push('_No entries yet._');
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push('<!-- GENERATED: INDEX -->');
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
lines.push('| ID | Title | Confidence | Status |');
|
|
55
|
+
lines.push('|----|-------|------------|--------|');
|
|
56
|
+
for (const e of entries) {
|
|
57
|
+
lines.push(`| ${e.id} | [${e.title}](./${e.fileName}) | ${e.confidence} | ${e.status} |`);
|
|
58
|
+
}
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push(`_${entries.length} entries indexed._`);
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push('<!-- GENERATED: INDEX -->');
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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.getCanonIndexTargets = getCanonIndexTargets;
|
|
7
|
+
exports.getSnapshotIndexPaths = getSnapshotIndexPaths;
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
/**
|
|
10
|
+
* Returns the canonical index targets for a given architecture directory.
|
|
11
|
+
* This is the single source of truth for which index files are
|
|
12
|
+
* generated/snapshotted/validated across canon rebuild stages.
|
|
13
|
+
*/
|
|
14
|
+
function getCanonIndexTargets(archDir) {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
scanDir: node_path_1.default.join(archDir, 'knowledge', 'axioms'),
|
|
18
|
+
outputFile: node_path_1.default.join(archDir, 'knowledge', 'axioms', 'README.md'),
|
|
19
|
+
sectionTitle: 'Axioms',
|
|
20
|
+
description: 'Fundamental architectural invariants that MUST always hold.',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
scanDir: node_path_1.default.join(archDir, 'knowledge', 'decisions'),
|
|
24
|
+
outputFile: node_path_1.default.join(archDir, 'knowledge', 'decisions', 'README.md'),
|
|
25
|
+
sectionTitle: 'Architectural Decisions',
|
|
26
|
+
description: 'ADRs documenting key architectural choices and their rationale.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
scanDir: node_path_1.default.join(archDir, 'knowledge', 'conventions'),
|
|
30
|
+
outputFile: node_path_1.default.join(archDir, 'knowledge', 'conventions', 'README.md'),
|
|
31
|
+
sectionTitle: 'Conventions',
|
|
32
|
+
description: 'Coding and design conventions followed across this codebase.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
scanDir: node_path_1.default.join(archDir, 'knowledge', 'anti-patterns'),
|
|
36
|
+
outputFile: node_path_1.default.join(archDir, 'knowledge', 'anti-patterns', 'README.md'),
|
|
37
|
+
sectionTitle: 'Anti-Patterns',
|
|
38
|
+
description: 'Known pitfalls and patterns that MUST be avoided.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
scanDir: node_path_1.default.join(archDir, 'domains'),
|
|
42
|
+
outputFile: node_path_1.default.join(archDir, 'domains', 'README.md'),
|
|
43
|
+
sectionTitle: 'Domains',
|
|
44
|
+
description: 'Bounded contexts and domain boundaries identified in the architecture.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
scanDir: node_path_1.default.join(archDir, 'contracts'),
|
|
48
|
+
outputFile: node_path_1.default.join(archDir, 'contracts', 'README.md'),
|
|
49
|
+
sectionTitle: 'Contracts',
|
|
50
|
+
description: 'Interface contracts between domains (UI-backend shapes, command outcomes).',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns relative paths of all README files to snapshot during rebuild.
|
|
56
|
+
* Includes top-level and parent READMEs that are not auto-generated
|
|
57
|
+
* but should be preserved for rollback.
|
|
58
|
+
*/
|
|
59
|
+
function getSnapshotIndexPaths(archDir) {
|
|
60
|
+
// Top-level hand-crafted READMEs (not regenerable, but worth snapshotting)
|
|
61
|
+
const topLevel = [
|
|
62
|
+
'README.md',
|
|
63
|
+
'knowledge/README.md',
|
|
64
|
+
];
|
|
65
|
+
// Auto-generated sub-category READMEs (from getCanonIndexTargets)
|
|
66
|
+
const generated = getCanonIndexTargets(archDir).map((t) => node_path_1.default.relative(archDir, t.outputFile));
|
|
67
|
+
return [...topLevel, ...generated];
|
|
68
|
+
}
|