@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,262 @@
|
|
|
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.getCanonsBaseDir = getCanonsBaseDir;
|
|
7
|
+
exports.isCanonsAvailable = isCanonsAvailable;
|
|
8
|
+
exports.syncCanons = syncCanons;
|
|
9
|
+
exports.resolveCanonFile = resolveCanonFile;
|
|
10
|
+
exports.copyCanonContent = copyCanonContent;
|
|
11
|
+
exports.isBusinessCanonConfigured = isBusinessCanonConfigured;
|
|
12
|
+
exports.getBusinessCanonDir = getBusinessCanonDir;
|
|
13
|
+
exports.syncBusinessCanon = syncBusinessCanon;
|
|
14
|
+
exports.copyBusinessCanonContent = copyBusinessCanonContent;
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
16
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
17
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
18
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
19
|
+
const CANON_REPO_NAME = 'collab-architecture';
|
|
20
|
+
const CANONS_SUBDIR = 'canons';
|
|
21
|
+
const CANON_REPO_URL = 'https://github.com/uxmaltech/collab-architecture.git';
|
|
22
|
+
const CANON_BRANCH = 'main';
|
|
23
|
+
const ALLOWED_PREFIX = 'prompts/';
|
|
24
|
+
/**
|
|
25
|
+
* Returns the base directory for the collab-architecture canon repo.
|
|
26
|
+
* Respects the COLLAB_HOME environment variable, defaulting to ~/.collab.
|
|
27
|
+
*/
|
|
28
|
+
function getCanonsBaseDir() {
|
|
29
|
+
const collabHome = process.env.COLLAB_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), '.collab');
|
|
30
|
+
return node_path_1.default.join(collabHome, CANONS_SUBDIR, CANON_REPO_NAME);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks whether the canons directory exists and contains prompts.
|
|
34
|
+
*/
|
|
35
|
+
function isCanonsAvailable() {
|
|
36
|
+
const dir = getCanonsBaseDir();
|
|
37
|
+
return node_fs_1.default.existsSync(node_path_1.default.join(dir, 'prompts'));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Clones or pulls the collab-architecture canons repository.
|
|
41
|
+
* Returns true on success, false on failure.
|
|
42
|
+
*/
|
|
43
|
+
function syncCanons(log) {
|
|
44
|
+
const canonsDir = getCanonsBaseDir();
|
|
45
|
+
const parentDir = node_path_1.default.dirname(canonsDir);
|
|
46
|
+
const print = log ?? console.log;
|
|
47
|
+
try {
|
|
48
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(canonsDir, '.git'))) {
|
|
49
|
+
print(`Updating canons in ${canonsDir}...`);
|
|
50
|
+
(0, node_child_process_1.execFileSync)('git', ['-C', canonsDir, 'fetch', 'origin', CANON_BRANCH], {
|
|
51
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
52
|
+
});
|
|
53
|
+
(0, node_child_process_1.execFileSync)('git', ['-C', canonsDir, 'checkout', CANON_BRANCH], {
|
|
54
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
55
|
+
});
|
|
56
|
+
(0, node_child_process_1.execFileSync)('git', ['-C', canonsDir, 'reset', '--hard', `origin/${CANON_BRANCH}`], {
|
|
57
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
print(`Cloning canons into ${canonsDir}...`);
|
|
62
|
+
node_fs_1.default.mkdirSync(parentDir, { recursive: true });
|
|
63
|
+
(0, node_child_process_1.execFileSync)('git', ['clone', '--branch', CANON_BRANCH, '--single-branch', CANON_REPO_URL, canonsDir], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
print(`Failed to sync canons: ${String(err)}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reads a prompt file from the canons directory.
|
|
74
|
+
* Only files under prompts/ are allowed — canon content is never read directly.
|
|
75
|
+
* Returns the file content as a string, or null if the file does not exist.
|
|
76
|
+
*/
|
|
77
|
+
function resolveCanonFile(relativePath) {
|
|
78
|
+
if (!relativePath.startsWith(ALLOWED_PREFIX)) {
|
|
79
|
+
throw new Error(`Canon resolver only allows reading from '${ALLOWED_PREFIX}'. ` +
|
|
80
|
+
`Requested: '${relativePath}'. Canon content must never be copied to projects.`);
|
|
81
|
+
}
|
|
82
|
+
const filePath = node_path_1.default.join(getCanonsBaseDir(), relativePath);
|
|
83
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
87
|
+
}
|
|
88
|
+
/** Directories and root files to copy from canon repo to target. */
|
|
89
|
+
const CANON_COPY_DIRS = [
|
|
90
|
+
'domains',
|
|
91
|
+
'knowledge',
|
|
92
|
+
'contracts',
|
|
93
|
+
'governance',
|
|
94
|
+
'graph',
|
|
95
|
+
'schema',
|
|
96
|
+
'prompts',
|
|
97
|
+
'embeddings',
|
|
98
|
+
'evolution',
|
|
99
|
+
];
|
|
100
|
+
const CANON_COPY_ROOT_FILES = ['AGENTS.md', 'README.md'];
|
|
101
|
+
/**
|
|
102
|
+
* Counts files recursively inside a directory.
|
|
103
|
+
*/
|
|
104
|
+
function countFiles(dir) {
|
|
105
|
+
if (!node_fs_1.default.existsSync(dir)) {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
let count = 0;
|
|
109
|
+
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (entry.isFile()) {
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
114
|
+
else if (entry.isDirectory()) {
|
|
115
|
+
count += countFiles(node_path_1.default.join(dir, entry.name));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Copies the full collab-architecture canon content to a target directory.
|
|
122
|
+
* This is used by the file-only pipeline to create `docs/architecture/uxmaltech/`.
|
|
123
|
+
*
|
|
124
|
+
* Copies: domains/, knowledge/, contracts/, governance/, graph/, schema/,
|
|
125
|
+
* prompts/, embeddings/, evolution/, AGENTS.md, README.md
|
|
126
|
+
*
|
|
127
|
+
* Returns the number of files copied.
|
|
128
|
+
*/
|
|
129
|
+
function copyCanonContent(targetDir, log) {
|
|
130
|
+
const sourceDir = getCanonsBaseDir();
|
|
131
|
+
const print = log ?? (() => { });
|
|
132
|
+
if (!node_fs_1.default.existsSync(sourceDir)) {
|
|
133
|
+
throw new Error(`Canon source not found at ${sourceDir}. Run syncCanons() first.`);
|
|
134
|
+
}
|
|
135
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
136
|
+
let totalFiles = 0;
|
|
137
|
+
for (const dir of CANON_COPY_DIRS) {
|
|
138
|
+
const src = node_path_1.default.join(sourceDir, dir);
|
|
139
|
+
const dest = node_path_1.default.join(targetDir, dir);
|
|
140
|
+
if (!node_fs_1.default.existsSync(src)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
node_fs_1.default.cpSync(src, dest, { recursive: true });
|
|
144
|
+
const fileCount = countFiles(dest);
|
|
145
|
+
totalFiles += fileCount;
|
|
146
|
+
print(` Copied ${dir}/ (${fileCount} files)`);
|
|
147
|
+
}
|
|
148
|
+
for (const file of CANON_COPY_ROOT_FILES) {
|
|
149
|
+
const src = node_path_1.default.join(sourceDir, file);
|
|
150
|
+
const dest = node_path_1.default.join(targetDir, file);
|
|
151
|
+
if (!node_fs_1.default.existsSync(src)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
node_fs_1.default.copyFileSync(src, dest);
|
|
155
|
+
totalFiles++;
|
|
156
|
+
print(` Copied ${file}`);
|
|
157
|
+
}
|
|
158
|
+
return totalFiles;
|
|
159
|
+
}
|
|
160
|
+
// ────────────────────────────────────────────────────────────────
|
|
161
|
+
// Business canon support
|
|
162
|
+
// ────────────────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Returns true if the config has a business canon configured.
|
|
165
|
+
*/
|
|
166
|
+
function isBusinessCanonConfigured(config) {
|
|
167
|
+
return !!config.canons?.business?.repo;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Returns the local directory where the business canon is cloned.
|
|
171
|
+
*/
|
|
172
|
+
function getBusinessCanonDir(config) {
|
|
173
|
+
const canon = config.canons?.business;
|
|
174
|
+
if (!canon) {
|
|
175
|
+
throw new Error('No business canon configured.');
|
|
176
|
+
}
|
|
177
|
+
const repoName = canon.repo.split('/').pop() ?? canon.repo;
|
|
178
|
+
const collabHome = process.env.COLLAB_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), '.collab');
|
|
179
|
+
return node_path_1.default.join(collabHome, CANONS_SUBDIR, repoName);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Clones or pulls the business canon repository.
|
|
183
|
+
* Uses the workspace GitHub token if available, otherwise falls back to default git credentials.
|
|
184
|
+
*/
|
|
185
|
+
function syncBusinessCanon(config, log, token) {
|
|
186
|
+
const canon = config.canons?.business;
|
|
187
|
+
if (!canon) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const canonsDir = getBusinessCanonDir(config);
|
|
191
|
+
const parentDir = node_path_1.default.dirname(canonsDir);
|
|
192
|
+
const print = log ?? console.log;
|
|
193
|
+
const branch = canon.branch || 'main';
|
|
194
|
+
// Build repo URL — inject token for private repo access if available
|
|
195
|
+
let repoUrl;
|
|
196
|
+
if (token) {
|
|
197
|
+
repoUrl = `https://x-access-token:${token}@github.com/${canon.repo}.git`;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
repoUrl = `https://github.com/${canon.repo}.git`;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(canonsDir, '.git'))) {
|
|
204
|
+
print(`Updating business canon in ${canonsDir}...`);
|
|
205
|
+
(0, node_child_process_1.execFileSync)('git', ['-C', canonsDir, 'fetch', 'origin', branch], {
|
|
206
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
207
|
+
});
|
|
208
|
+
(0, node_child_process_1.execFileSync)('git', ['-C', canonsDir, 'checkout', branch], {
|
|
209
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
210
|
+
});
|
|
211
|
+
(0, node_child_process_1.execFileSync)('git', ['-C', canonsDir, 'reset', '--hard', `origin/${branch}`], {
|
|
212
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
print(`Cloning business canon into ${canonsDir}...`);
|
|
217
|
+
node_fs_1.default.mkdirSync(parentDir, { recursive: true });
|
|
218
|
+
(0, node_child_process_1.execFileSync)('git', ['clone', '--branch', branch, '--single-branch', repoUrl, canonsDir], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
219
|
+
}
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
print(`Failed to sync business canon: ${String(err)}`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Copies the business canon content to a target directory.
|
|
229
|
+
* Copies all directories and root files found in the canon.
|
|
230
|
+
*/
|
|
231
|
+
function copyBusinessCanonContent(config, targetDir, log) {
|
|
232
|
+
const sourceDir = getBusinessCanonDir(config);
|
|
233
|
+
const print = log ?? (() => { });
|
|
234
|
+
if (!node_fs_1.default.existsSync(sourceDir)) {
|
|
235
|
+
throw new Error(`Business canon source not found at ${sourceDir}. Run syncBusinessCanon() first.`);
|
|
236
|
+
}
|
|
237
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
238
|
+
let totalFiles = 0;
|
|
239
|
+
// Copy same directories as framework canon (where they exist)
|
|
240
|
+
for (const dir of CANON_COPY_DIRS) {
|
|
241
|
+
const src = node_path_1.default.join(sourceDir, dir);
|
|
242
|
+
const dest = node_path_1.default.join(targetDir, dir);
|
|
243
|
+
if (!node_fs_1.default.existsSync(src)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
node_fs_1.default.cpSync(src, dest, { recursive: true });
|
|
247
|
+
const fileCount = countFiles(dest);
|
|
248
|
+
totalFiles += fileCount;
|
|
249
|
+
print(` Copied ${dir}/ (${fileCount} files)`);
|
|
250
|
+
}
|
|
251
|
+
for (const file of CANON_COPY_ROOT_FILES) {
|
|
252
|
+
const src = node_path_1.default.join(sourceDir, file);
|
|
253
|
+
const dest = node_path_1.default.join(targetDir, file);
|
|
254
|
+
if (!node_fs_1.default.existsSync(src)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
node_fs_1.default.copyFileSync(src, dest);
|
|
258
|
+
totalFiles++;
|
|
259
|
+
print(` Copied ${file}`);
|
|
260
|
+
}
|
|
261
|
+
return totalFiles;
|
|
262
|
+
}
|
|
@@ -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.generateCanonScaffold = generateCanonScaffold;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const canon_1 = require("../templates/canon");
|
|
10
|
+
/**
|
|
11
|
+
* Defines the complete canonical architecture scaffold tree.
|
|
12
|
+
* Each entry maps a relative file path to its template content.
|
|
13
|
+
*/
|
|
14
|
+
function buildScaffoldEntries() {
|
|
15
|
+
return [
|
|
16
|
+
// Governance
|
|
17
|
+
{ relativePath: 'governance/what-enters-the-canon.md', content: canon_1.whatEntersTheCanonTemplate },
|
|
18
|
+
{ relativePath: 'governance/implementation-process.md', content: canon_1.implementationProcessTemplate },
|
|
19
|
+
{ relativePath: 'governance/schema-versioning.md', content: canon_1.schemaVersioningTemplate },
|
|
20
|
+
{ relativePath: 'governance/confidence-levels.md', content: canon_1.confidenceLevelsTemplate },
|
|
21
|
+
{ relativePath: 'governance/review-process.md', content: canon_1.reviewProcessTemplate },
|
|
22
|
+
// Knowledge
|
|
23
|
+
{ relativePath: 'knowledge/axioms/README.md', content: canon_1.knowledgeAxiomsReadme },
|
|
24
|
+
{ relativePath: 'knowledge/decisions/README.md', content: canon_1.knowledgeDecisionsReadme },
|
|
25
|
+
{ relativePath: 'knowledge/conventions/README.md', content: canon_1.knowledgeConventionsReadme },
|
|
26
|
+
{ relativePath: 'knowledge/anti-patterns/README.md', content: canon_1.knowledgeAntiPatternsReadme },
|
|
27
|
+
// Domains
|
|
28
|
+
{ relativePath: 'domains/README.md', content: canon_1.domainReadme },
|
|
29
|
+
// Contracts
|
|
30
|
+
{ relativePath: 'contracts/README.md', content: canon_1.contractsReadme },
|
|
31
|
+
// Evolution
|
|
32
|
+
{ relativePath: 'evolution/changelog.md', content: canon_1.changelogTemplate },
|
|
33
|
+
{ relativePath: 'evolution/upgrade-guide.md', content: canon_1.upgradeGuideTemplate },
|
|
34
|
+
{ relativePath: 'evolution/deprecated.md', content: canon_1.deprecatedTemplate },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function generateCanonScaffold(ctx) {
|
|
38
|
+
const archDir = ctx.config.architectureDir;
|
|
39
|
+
const entries = buildScaffoldEntries();
|
|
40
|
+
let created = 0;
|
|
41
|
+
let skipped = 0;
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const target = node_path_1.default.join(archDir, entry.relativePath);
|
|
44
|
+
// Never overwrite existing files — the user may have customized them.
|
|
45
|
+
if (!ctx.executor.dryRun && node_fs_1.default.existsSync(target)) {
|
|
46
|
+
ctx.logger.debug(`Scaffold file already exists, skipping: ${entry.relativePath}`);
|
|
47
|
+
skipped++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
ctx.executor.ensureDirectory(node_path_1.default.dirname(target));
|
|
51
|
+
ctx.executor.writeFile(target, entry.content, {
|
|
52
|
+
description: `scaffold ${entry.relativePath}`,
|
|
53
|
+
});
|
|
54
|
+
created++;
|
|
55
|
+
}
|
|
56
|
+
ctx.logger.info(`Canon scaffold: ${created} file(s) created, ${skipped} existing file(s) preserved.`);
|
|
57
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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.checkGhAuth = checkGhAuth;
|
|
7
|
+
exports.detectProviderCli = detectProviderCli;
|
|
8
|
+
exports.detectAllClis = detectAllClis;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
/** CLI commands for each provider. */
|
|
14
|
+
const CLI_COMMANDS = {
|
|
15
|
+
codex: 'codex',
|
|
16
|
+
claude: 'claude',
|
|
17
|
+
gemini: 'gemini',
|
|
18
|
+
copilot: 'gh',
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Tries to run `<command> --version` and returns the trimmed stdout.
|
|
22
|
+
* Returns null if the command is not found or fails.
|
|
23
|
+
*/
|
|
24
|
+
function probeVersion(command) {
|
|
25
|
+
try {
|
|
26
|
+
const output = (0, node_child_process_1.execSync)(`${command} --version`, {
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
timeout: 5_000,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
30
|
+
});
|
|
31
|
+
return output.trim() || null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a command exists on the system PATH.
|
|
39
|
+
*/
|
|
40
|
+
function commandExists(command) {
|
|
41
|
+
try {
|
|
42
|
+
const whichCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
43
|
+
(0, node_child_process_1.execSync)(`${whichCmd} ${command}`, {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
timeout: 3_000,
|
|
46
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Config file paths for each provider CLI (relative to home dir). */
|
|
55
|
+
const CLI_CONFIG_PATHS = {
|
|
56
|
+
codex: '.codex/config.toml',
|
|
57
|
+
claude: '.claude.json',
|
|
58
|
+
gemini: '.gemini/settings.json',
|
|
59
|
+
copilot: '',
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Reads the configured model from a provider's CLI config file.
|
|
63
|
+
* Returns null if the config doesn't exist or can't be parsed.
|
|
64
|
+
*/
|
|
65
|
+
function readCliConfigModel(provider) {
|
|
66
|
+
try {
|
|
67
|
+
const configRelPath = CLI_CONFIG_PATHS[provider];
|
|
68
|
+
if (!configRelPath) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const homeDir = node_os_1.default.homedir();
|
|
72
|
+
const configPath = node_path_1.default.join(homeDir, configRelPath);
|
|
73
|
+
if (!node_fs_1.default.existsSync(configPath)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const content = node_fs_1.default.readFileSync(configPath, 'utf8');
|
|
77
|
+
if (provider === 'codex') {
|
|
78
|
+
// TOML: look for top-level `model = "..."` line
|
|
79
|
+
const match = content.match(/^\s*model\s*=\s*"([^"]+)"/m);
|
|
80
|
+
return match?.[1] ?? null;
|
|
81
|
+
}
|
|
82
|
+
// JSON-based configs (claude, gemini)
|
|
83
|
+
const parsed = JSON.parse(content);
|
|
84
|
+
return parsed.model ?? parsed.defaultModel ?? null;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Checks whether `gh auth status` reports an authenticated session.
|
|
92
|
+
*/
|
|
93
|
+
function checkGhAuth() {
|
|
94
|
+
try {
|
|
95
|
+
(0, node_child_process_1.execSync)('gh auth status', {
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
timeout: 5_000,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Detects whether the official CLI for a provider is installed.
|
|
108
|
+
* Also reads the CLI config to detect the currently configured model.
|
|
109
|
+
*
|
|
110
|
+
* - codex → `codex` (OpenAI Codex CLI)
|
|
111
|
+
* - claude → `claude` (Claude Code CLI)
|
|
112
|
+
* - gemini → `gemini` (Google Gemini CLI)
|
|
113
|
+
* - copilot → `gh` (GitHub CLI with auth)
|
|
114
|
+
*/
|
|
115
|
+
function detectProviderCli(provider) {
|
|
116
|
+
const command = CLI_COMMANDS[provider];
|
|
117
|
+
const available = commandExists(command);
|
|
118
|
+
if (!available) {
|
|
119
|
+
return { command, available: false };
|
|
120
|
+
}
|
|
121
|
+
// Copilot requires gh auth — not just gh installed
|
|
122
|
+
if (provider === 'copilot') {
|
|
123
|
+
const authenticated = checkGhAuth();
|
|
124
|
+
if (!authenticated) {
|
|
125
|
+
return { command, available: false };
|
|
126
|
+
}
|
|
127
|
+
const version = probeVersion(command);
|
|
128
|
+
return { command, available: true, version: version ?? undefined };
|
|
129
|
+
}
|
|
130
|
+
const version = probeVersion(command);
|
|
131
|
+
const configuredModel = readCliConfigModel(provider);
|
|
132
|
+
return {
|
|
133
|
+
command,
|
|
134
|
+
available: true,
|
|
135
|
+
version: version ?? undefined,
|
|
136
|
+
configuredModel: configuredModel ?? undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Detects all provider CLIs at once. Returns a record keyed by provider.
|
|
141
|
+
*/
|
|
142
|
+
function detectAllClis() {
|
|
143
|
+
return {
|
|
144
|
+
codex: detectProviderCli('codex'),
|
|
145
|
+
claude: detectProviderCli('claude'),
|
|
146
|
+
gemini: detectProviderCli('gemini'),
|
|
147
|
+
copilot: detectProviderCli('copilot'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCommandContext = createCommandContext;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const executor_1 = require("./executor");
|
|
6
|
+
const logger_1 = require("./logger");
|
|
7
|
+
/**
|
|
8
|
+
* Builds a {@link CommandContext} from a Commander action's `command` ref.
|
|
9
|
+
* Reads global options, resolves the workspace, and wires up logging.
|
|
10
|
+
*/
|
|
11
|
+
function createCommandContext(command) {
|
|
12
|
+
const options = command.optsWithGlobals();
|
|
13
|
+
const cwd = options.cwd ? options.cwd : process.cwd();
|
|
14
|
+
const logger = (0, logger_1.createLogger)({ verbose: options.verbose, quiet: options.quiet });
|
|
15
|
+
const dryRun = Boolean(options.dryRun);
|
|
16
|
+
return {
|
|
17
|
+
cwd,
|
|
18
|
+
logger,
|
|
19
|
+
config: (0, config_1.loadCollabConfig)(cwd),
|
|
20
|
+
executor: new executor_1.Executor(logger, { dryRun, cwd }),
|
|
21
|
+
dryRun,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMPOSE_ENV_ORDER = exports.COMPOSE_ENV_DEFAULTS = void 0;
|
|
4
|
+
exports.scopedComposeDefaults = scopedComposeDefaults;
|
|
5
|
+
exports.COMPOSE_ENV_DEFAULTS = {
|
|
6
|
+
COLLAB_NETWORK: 'collab-network',
|
|
7
|
+
QDRANT_HOST: '127.0.0.1',
|
|
8
|
+
QDRANT_IMAGE: 'qdrant/qdrant:v1.13.4',
|
|
9
|
+
QDRANT_PORT: '6333',
|
|
10
|
+
QDRANT_VOLUME: 'collab-qdrant-data',
|
|
11
|
+
NEBULA_HOST: '127.0.0.1',
|
|
12
|
+
NEBULA_VERSION: 'v3.6.0',
|
|
13
|
+
NEBULA_METAD_PORT: '9559',
|
|
14
|
+
NEBULA_METAD_HTTP_PORT: '19559',
|
|
15
|
+
NEBULA_STORAGED_PORT: '9779',
|
|
16
|
+
NEBULA_STORAGED_HTTP_PORT: '19779',
|
|
17
|
+
NEBULA_GRAPHD_PORT: '9669',
|
|
18
|
+
NEBULA_GRAPHD_HTTP_PORT: '19669',
|
|
19
|
+
NEBULA_METAD_VOLUME: 'collab-nebula-metad0',
|
|
20
|
+
NEBULA_STORAGED_VOLUME: 'collab-nebula-storaged0',
|
|
21
|
+
MCP_HOST: '127.0.0.1',
|
|
22
|
+
MCP_IMAGE: 'ghcr.io/uxmaltech/collab-architecture-mcp:latest',
|
|
23
|
+
MCP_PORT: '7337',
|
|
24
|
+
MCP_CONTAINER_PORT: '7337',
|
|
25
|
+
MCP_VOLUME: 'collab-mcp-data',
|
|
26
|
+
MCP_ENV: 'local',
|
|
27
|
+
MCP_API_KEYS: '',
|
|
28
|
+
MCP_TECHNICAL_SCOPES: 'uxmaltech',
|
|
29
|
+
};
|
|
30
|
+
exports.COMPOSE_ENV_ORDER = Object.keys(exports.COMPOSE_ENV_DEFAULTS);
|
|
31
|
+
/**
|
|
32
|
+
* Returns compose env defaults with workspace-scoped resource names.
|
|
33
|
+
* Prefixes network, volume, and project names with the workspace name
|
|
34
|
+
* to avoid collisions between workspaces running simultaneously.
|
|
35
|
+
*/
|
|
36
|
+
function scopedComposeDefaults(workspaceName) {
|
|
37
|
+
const slug = workspaceName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
38
|
+
const prefix = slug ? `collab-${slug}` : 'collab';
|
|
39
|
+
return {
|
|
40
|
+
...exports.COMPOSE_ENV_DEFAULTS,
|
|
41
|
+
COLLAB_NETWORK: `${prefix}-network`,
|
|
42
|
+
QDRANT_VOLUME: `${prefix}-qdrant-data`,
|
|
43
|
+
NEBULA_METAD_VOLUME: `${prefix}-nebula-metad0`,
|
|
44
|
+
NEBULA_STORAGED_VOLUME: `${prefix}-nebula-storaged0`,
|
|
45
|
+
MCP_VOLUME: `${prefix}-mcp-data`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureComposeEnvFile = ensureComposeEnvFile;
|
|
4
|
+
const compose_defaults_1 = require("./compose-defaults");
|
|
5
|
+
const env_file_1 = require("./env-file");
|
|
6
|
+
function ensureComposeEnvFile(envFilePath, logger, executor, overrides, defaults = compose_defaults_1.COMPOSE_ENV_DEFAULTS) {
|
|
7
|
+
const existing = (0, env_file_1.readEnvFile)(envFilePath);
|
|
8
|
+
const merged = (0, env_file_1.mergeEnvWithDefaults)(existing, defaults);
|
|
9
|
+
if (overrides) {
|
|
10
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
11
|
+
merged[key] = value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
(0, env_file_1.writeEnvFile)(envFilePath, merged, compose_defaults_1.COMPOSE_ENV_ORDER, executor);
|
|
15
|
+
if (Object.keys(existing).length === 0) {
|
|
16
|
+
logger.info(executor?.dryRun ? `[dry-run] Would create env file: ${envFilePath}` : `Created env file: ${envFilePath}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logger.debug(executor?.dryRun
|
|
20
|
+
? `[dry-run] Would update env file while preserving overrides: ${envFilePath}`
|
|
21
|
+
: `Updated env file while preserving overrides: ${envFilePath}`);
|
|
22
|
+
}
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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.getComposeFilePaths = getComposeFilePaths;
|
|
7
|
+
exports.fileExists = fileExists;
|
|
8
|
+
exports.selectInfraComposeFile = selectInfraComposeFile;
|
|
9
|
+
exports.selectMcpComposeFile = selectMcpComposeFile;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
function getComposeFilePaths(config, outputDirectory) {
|
|
13
|
+
const targetDirectory = outputDirectory
|
|
14
|
+
? node_path_1.default.resolve(config.workspaceDir, outputDirectory)
|
|
15
|
+
: config.workspaceDir;
|
|
16
|
+
return {
|
|
17
|
+
consolidated: node_path_1.default.resolve(targetDirectory, config.compose.consolidatedFile),
|
|
18
|
+
infra: node_path_1.default.resolve(targetDirectory, config.compose.infraFile),
|
|
19
|
+
mcp: node_path_1.default.resolve(targetDirectory, config.compose.mcpFile),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function fileExists(filePath) {
|
|
23
|
+
return node_fs_1.default.existsSync(filePath);
|
|
24
|
+
}
|
|
25
|
+
function selectInfraComposeFile(paths) {
|
|
26
|
+
if (fileExists(paths.infra)) {
|
|
27
|
+
return { file: paths.infra, source: 'split' };
|
|
28
|
+
}
|
|
29
|
+
return { file: paths.consolidated, source: 'consolidated' };
|
|
30
|
+
}
|
|
31
|
+
function selectMcpComposeFile(paths) {
|
|
32
|
+
if (fileExists(paths.mcp)) {
|
|
33
|
+
return { file: paths.mcp, source: 'split' };
|
|
34
|
+
}
|
|
35
|
+
return { file: paths.consolidated, source: 'consolidated' };
|
|
36
|
+
}
|