@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,195 @@
|
|
|
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.scanRepository = scanRepository;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
/** Directories always excluded from scanning. */
|
|
10
|
+
const EXCLUDED_DIRS = new Set([
|
|
11
|
+
'node_modules',
|
|
12
|
+
'.git',
|
|
13
|
+
'dist',
|
|
14
|
+
'build',
|
|
15
|
+
'out',
|
|
16
|
+
'.next',
|
|
17
|
+
'.nuxt',
|
|
18
|
+
'coverage',
|
|
19
|
+
'__pycache__',
|
|
20
|
+
'.venv',
|
|
21
|
+
'vendor',
|
|
22
|
+
'target',
|
|
23
|
+
]);
|
|
24
|
+
/** File extensions considered for context. */
|
|
25
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
26
|
+
'.ts',
|
|
27
|
+
'.tsx',
|
|
28
|
+
'.js',
|
|
29
|
+
'.jsx',
|
|
30
|
+
'.py',
|
|
31
|
+
'.rs',
|
|
32
|
+
'.go',
|
|
33
|
+
'.java',
|
|
34
|
+
'.rb',
|
|
35
|
+
'.php',
|
|
36
|
+
'.vue',
|
|
37
|
+
'.svelte',
|
|
38
|
+
]);
|
|
39
|
+
const CONFIG_FILES = [
|
|
40
|
+
'package.json',
|
|
41
|
+
'tsconfig.json',
|
|
42
|
+
'Cargo.toml',
|
|
43
|
+
'composer.json',
|
|
44
|
+
'go.mod',
|
|
45
|
+
'Gemfile',
|
|
46
|
+
'requirements.txt',
|
|
47
|
+
'pyproject.toml',
|
|
48
|
+
'.eslintrc.json',
|
|
49
|
+
'.prettierrc',
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Detects the primary language and framework from manifest files.
|
|
53
|
+
*/
|
|
54
|
+
function detectStack(workspaceDir) {
|
|
55
|
+
const pkgPath = node_path_1.default.join(workspaceDir, 'package.json');
|
|
56
|
+
if (node_fs_1.default.existsSync(pkgPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf8'));
|
|
59
|
+
const deps = {
|
|
60
|
+
...(pkg.dependencies ?? {}),
|
|
61
|
+
...(pkg.devDependencies ?? {}),
|
|
62
|
+
};
|
|
63
|
+
const depNames = Object.keys(deps);
|
|
64
|
+
const language = depNames.some((d) => d === 'typescript' || d.startsWith('@types/'))
|
|
65
|
+
? 'TypeScript'
|
|
66
|
+
: 'JavaScript';
|
|
67
|
+
let framework = null;
|
|
68
|
+
if (depNames.includes('next'))
|
|
69
|
+
framework = 'Next.js';
|
|
70
|
+
else if (depNames.includes('nuxt'))
|
|
71
|
+
framework = 'Nuxt';
|
|
72
|
+
else if (depNames.includes('react'))
|
|
73
|
+
framework = 'React';
|
|
74
|
+
else if (depNames.includes('vue'))
|
|
75
|
+
framework = 'Vue';
|
|
76
|
+
else if (depNames.includes('svelte'))
|
|
77
|
+
framework = 'Svelte';
|
|
78
|
+
else if (depNames.includes('express'))
|
|
79
|
+
framework = 'Express';
|
|
80
|
+
else if (depNames.includes('fastify'))
|
|
81
|
+
framework = 'Fastify';
|
|
82
|
+
else if (depNames.includes('commander'))
|
|
83
|
+
framework = 'CLI (Commander)';
|
|
84
|
+
return { language, framework, dependencies: depNames.slice(0, 30) };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Fall through
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const cargoPath = node_path_1.default.join(workspaceDir, 'Cargo.toml');
|
|
91
|
+
if (node_fs_1.default.existsSync(cargoPath)) {
|
|
92
|
+
return { language: 'Rust', framework: null, dependencies: [] };
|
|
93
|
+
}
|
|
94
|
+
const goModPath = node_path_1.default.join(workspaceDir, 'go.mod');
|
|
95
|
+
if (node_fs_1.default.existsSync(goModPath)) {
|
|
96
|
+
return { language: 'Go', framework: null, dependencies: [] };
|
|
97
|
+
}
|
|
98
|
+
const pyprojectPath = node_path_1.default.join(workspaceDir, 'pyproject.toml');
|
|
99
|
+
const requirementsPath = node_path_1.default.join(workspaceDir, 'requirements.txt');
|
|
100
|
+
if (node_fs_1.default.existsSync(pyprojectPath) || node_fs_1.default.existsSync(requirementsPath)) {
|
|
101
|
+
return { language: 'Python', framework: null, dependencies: [] };
|
|
102
|
+
}
|
|
103
|
+
const composerPath = node_path_1.default.join(workspaceDir, 'composer.json');
|
|
104
|
+
if (node_fs_1.default.existsSync(composerPath)) {
|
|
105
|
+
return { language: 'PHP', framework: null, dependencies: [] };
|
|
106
|
+
}
|
|
107
|
+
return { language: 'Unknown', framework: null, dependencies: [] };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Walks the file tree and returns a structured summary.
|
|
111
|
+
*/
|
|
112
|
+
function walkTree(dir, rootDir, depth, maxDepth) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
const sourceFiles = [];
|
|
115
|
+
if (depth > maxDepth) {
|
|
116
|
+
return { lines, sourceFiles };
|
|
117
|
+
}
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { lines, sourceFiles };
|
|
124
|
+
}
|
|
125
|
+
// Sort: directories first, then files
|
|
126
|
+
entries.sort((a, b) => {
|
|
127
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
128
|
+
return -1;
|
|
129
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
130
|
+
return 1;
|
|
131
|
+
return a.name.localeCompare(b.name);
|
|
132
|
+
});
|
|
133
|
+
const indent = ' '.repeat(depth);
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (entry.name.startsWith('.') && depth === 0 && entry.name !== '.github') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (entry.isDirectory()) {
|
|
139
|
+
if (EXCLUDED_DIRS.has(entry.name))
|
|
140
|
+
continue;
|
|
141
|
+
lines.push(`${indent}${entry.name}/`);
|
|
142
|
+
const sub = walkTree(node_path_1.default.join(dir, entry.name), rootDir, depth + 1, maxDepth);
|
|
143
|
+
lines.push(...sub.lines);
|
|
144
|
+
sourceFiles.push(...sub.sourceFiles);
|
|
145
|
+
}
|
|
146
|
+
else if (entry.isFile()) {
|
|
147
|
+
const ext = node_path_1.default.extname(entry.name);
|
|
148
|
+
if (SOURCE_EXTENSIONS.has(ext) || CONFIG_FILES.includes(entry.name)) {
|
|
149
|
+
const relPath = node_path_1.default.relative(rootDir, node_path_1.default.join(dir, entry.name));
|
|
150
|
+
sourceFiles.push(relPath);
|
|
151
|
+
if (depth <= maxDepth) {
|
|
152
|
+
lines.push(`${indent}${entry.name}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { lines, sourceFiles };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Scans a repository and produces a structured context for AI analysis.
|
|
161
|
+
*/
|
|
162
|
+
function scanRepository(workspaceDir, options = {}) {
|
|
163
|
+
const budgetChars = options.budgetChars ?? 4000;
|
|
164
|
+
const repoName = node_path_1.default.basename(workspaceDir);
|
|
165
|
+
const stack = detectStack(workspaceDir);
|
|
166
|
+
const { lines, sourceFiles } = walkTree(workspaceDir, workspaceDir, 0, 4);
|
|
167
|
+
// Trim structure to budget
|
|
168
|
+
let structure = lines.join('\n');
|
|
169
|
+
if (structure.length > budgetChars) {
|
|
170
|
+
structure = structure.slice(0, budgetChars) + '\n... (truncated)';
|
|
171
|
+
}
|
|
172
|
+
// Identify key files (entry points, configs, main modules)
|
|
173
|
+
const keyFilePatterns = [
|
|
174
|
+
/^src\/index\.\w+$/,
|
|
175
|
+
/^src\/main\.\w+$/,
|
|
176
|
+
/^src\/app\.\w+$/,
|
|
177
|
+
/^index\.\w+$/,
|
|
178
|
+
/^main\.\w+$/,
|
|
179
|
+
/package\.json$/,
|
|
180
|
+
/tsconfig\.json$/,
|
|
181
|
+
/README\.md$/i,
|
|
182
|
+
];
|
|
183
|
+
const keyFiles = sourceFiles
|
|
184
|
+
.filter((f) => keyFilePatterns.some((p) => p.test(f)))
|
|
185
|
+
.slice(0, 10);
|
|
186
|
+
return {
|
|
187
|
+
name: repoName,
|
|
188
|
+
language: stack.language,
|
|
189
|
+
framework: stack.framework,
|
|
190
|
+
dependencies: stack.dependencies,
|
|
191
|
+
structure,
|
|
192
|
+
keyFiles,
|
|
193
|
+
totalSourceFiles: sourceFiles.length,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REBUILD_HEALTH_OPTIONS = void 0;
|
|
4
|
+
exports.loadRuntimeEnv = loadRuntimeEnv;
|
|
5
|
+
exports.waitForInfraHealth = waitForInfraHealth;
|
|
6
|
+
exports.waitForMcpHealth = waitForMcpHealth;
|
|
7
|
+
exports.logServiceHealth = logServiceHealth;
|
|
8
|
+
exports.dryRunHealthOptions = dryRunHealthOptions;
|
|
9
|
+
const compose_defaults_1 = require("./compose-defaults");
|
|
10
|
+
const env_file_1 = require("./env-file");
|
|
11
|
+
const health_checker_1 = require("./health-checker");
|
|
12
|
+
/**
|
|
13
|
+
* Default health-check options used by standalone canon-rebuild stages
|
|
14
|
+
* that need to verify MCP reachability before calling API endpoints.
|
|
15
|
+
*/
|
|
16
|
+
exports.REBUILD_HEALTH_OPTIONS = {
|
|
17
|
+
timeoutMs: 5_000,
|
|
18
|
+
retries: 3,
|
|
19
|
+
retryDelayMs: 1_000,
|
|
20
|
+
};
|
|
21
|
+
function parsePort(value, fallback) {
|
|
22
|
+
if (!value) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
const parsed = Number.parseInt(value, 10);
|
|
26
|
+
if (Number.isNaN(parsed) || parsed < 1 || parsed > 65_535) {
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
function loadRuntimeEnv(config) {
|
|
32
|
+
const existing = (0, env_file_1.readEnvFile)(config.envFile);
|
|
33
|
+
return (0, env_file_1.mergeEnvWithDefaults)(existing, compose_defaults_1.COMPOSE_ENV_DEFAULTS);
|
|
34
|
+
}
|
|
35
|
+
async function waitForInfraHealth(env, options) {
|
|
36
|
+
const qdrantHost = env.QDRANT_HOST || '127.0.0.1';
|
|
37
|
+
const qdrantPort = parsePort(env.QDRANT_PORT, 6333);
|
|
38
|
+
const nebulaHost = env.NEBULA_HOST || '127.0.0.1';
|
|
39
|
+
const nebulaPort = parsePort(env.NEBULA_GRAPHD_PORT, 9669);
|
|
40
|
+
const qdrant = await (0, health_checker_1.checkHttpHealth)('qdrant', `http://${qdrantHost}:${qdrantPort}/collections`, options);
|
|
41
|
+
const nebula = await (0, health_checker_1.checkTcpHealth)('nebula-graphd', nebulaHost, nebulaPort, options);
|
|
42
|
+
const checks = [qdrant, nebula];
|
|
43
|
+
return {
|
|
44
|
+
ok: checks.every((item) => item.ok),
|
|
45
|
+
checks: checks.filter((item) => item.ok).map((item) => item.detail),
|
|
46
|
+
errors: checks
|
|
47
|
+
.filter((item) => !item.ok)
|
|
48
|
+
.map((item) => `${item.name}: ${item.error ?? item.detail}`),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function waitForMcpHealth(env, options) {
|
|
52
|
+
const host = env.MCP_HOST || '127.0.0.1';
|
|
53
|
+
const port = parsePort(env.MCP_PORT, 7337);
|
|
54
|
+
const mcp = await (0, health_checker_1.checkHttpHealth)('mcp', `http://${host}:${port}/health`, options);
|
|
55
|
+
return {
|
|
56
|
+
ok: mcp.ok,
|
|
57
|
+
checks: mcp.ok ? [mcp.detail] : [],
|
|
58
|
+
errors: mcp.ok ? [] : [`${mcp.name}: ${mcp.error ?? mcp.detail}`],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function logServiceHealth(logger, title, summary) {
|
|
62
|
+
if (summary.ok) {
|
|
63
|
+
logger.result(`[PASS] ${title}`);
|
|
64
|
+
for (const line of summary.checks) {
|
|
65
|
+
logger.result(` ${line}`);
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
logger.result(`[FAIL] ${title}`);
|
|
70
|
+
for (const line of summary.errors) {
|
|
71
|
+
logger.result(` ${line}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function dryRunHealthOptions(executor, options) {
|
|
75
|
+
return {
|
|
76
|
+
...options,
|
|
77
|
+
dryRun: executor.dryRun,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -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.resolveCommandPath = resolveCommandPath;
|
|
7
|
+
exports.shellQuote = shellQuote;
|
|
8
|
+
exports.toShellCommand = toShellCommand;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
/** Checks whether the file at `filePath` has the execute permission. */
|
|
12
|
+
function isExecutable(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
node_fs_1.default.accessSync(filePath, node_fs_1.default.constants.X_OK);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Searches `$PATH` for an executable named `commandName`.
|
|
23
|
+
* Returns the full path on success, or `null` when not found.
|
|
24
|
+
*/
|
|
25
|
+
function resolveCommandPath(commandName) {
|
|
26
|
+
const pathEnv = process.env.PATH ?? '';
|
|
27
|
+
const directories = pathEnv.split(node_path_1.default.delimiter).filter(Boolean);
|
|
28
|
+
for (const directory of directories) {
|
|
29
|
+
const fullPath = node_path_1.default.join(directory, commandName);
|
|
30
|
+
if (node_fs_1.default.existsSync(fullPath) && isExecutable(fullPath)) {
|
|
31
|
+
return fullPath;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wraps `argument` in single-quotes if it contains shell-special characters.
|
|
38
|
+
* Safe arguments (alphanumeric + common punctuation) are returned as-is.
|
|
39
|
+
*/
|
|
40
|
+
function shellQuote(argument) {
|
|
41
|
+
if (/^[a-zA-Z0-9_./:@%+=,-]+$/.test(argument)) {
|
|
42
|
+
return argument;
|
|
43
|
+
}
|
|
44
|
+
return `'${argument.replace(/'/g, `'"'"'`)}'`;
|
|
45
|
+
}
|
|
46
|
+
/** Joins an argument list into a shell-safe command string. */
|
|
47
|
+
function toShellCommand(parts) {
|
|
48
|
+
return parts.map((part) => shellQuote(part)).join(' ');
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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.loadState = loadState;
|
|
7
|
+
exports.saveState = saveState;
|
|
8
|
+
exports.toStateKey = toStateKey;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const EMPTY_STATE = {
|
|
12
|
+
generatedFiles: {},
|
|
13
|
+
workflows: {},
|
|
14
|
+
};
|
|
15
|
+
function loadState(config) {
|
|
16
|
+
if (!node_fs_1.default.existsSync(config.stateFile)) {
|
|
17
|
+
return { ...EMPTY_STATE };
|
|
18
|
+
}
|
|
19
|
+
const raw = node_fs_1.default.readFileSync(config.stateFile, 'utf8');
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
return {
|
|
22
|
+
generatedFiles: parsed.generatedFiles ?? {},
|
|
23
|
+
workflows: parsed.workflows ?? {},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function saveState(config, state, executor) {
|
|
27
|
+
const content = `${JSON.stringify(state, null, 2)}\n`;
|
|
28
|
+
if (executor) {
|
|
29
|
+
executor.writeFile(config.stateFile, content, { description: 'write state file' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(config.stateFile), { recursive: true });
|
|
33
|
+
node_fs_1.default.writeFileSync(config.stateFile, content, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
function toStateKey(config, filePath) {
|
|
36
|
+
const relative = node_path_1.default.relative(config.workspaceDir, filePath);
|
|
37
|
+
return relative === '' ? node_path_1.default.basename(filePath) : relative;
|
|
38
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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.readCheckState = readCheckState;
|
|
7
|
+
exports.writeCheckState = writeCheckState;
|
|
8
|
+
exports.shouldCheck = shouldCheck;
|
|
9
|
+
exports.fetchLatestVersion = fetchLatestVersion;
|
|
10
|
+
exports.checkForUpdate = checkForUpdate;
|
|
11
|
+
exports.maybeNotifyUpdate = maybeNotifyUpdate;
|
|
12
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const semver_1 = __importDefault(require("semver"));
|
|
16
|
+
const version_1 = require("./version");
|
|
17
|
+
const NPM_PACKAGE_NAME = '@uxmaltech/collab-cli';
|
|
18
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
19
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
20
|
+
// ── State file helpers ──────────────────────────────────────────────
|
|
21
|
+
function getCheckFilePath() {
|
|
22
|
+
const collabHome = process.env.COLLAB_HOME ?? node_path_1.default.join(node_os_1.default.homedir(), '.collab');
|
|
23
|
+
return node_path_1.default.join(collabHome, 'update-check.json');
|
|
24
|
+
}
|
|
25
|
+
function readCheckState(filePath) {
|
|
26
|
+
const target = filePath ?? getCheckFilePath();
|
|
27
|
+
try {
|
|
28
|
+
const raw = node_fs_1.default.readFileSync(target, 'utf8');
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (typeof parsed.lastCheck === 'string' && typeof parsed.latestVersion === 'string') {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function writeCheckState(state, filePath) {
|
|
40
|
+
const target = filePath ?? getCheckFilePath();
|
|
41
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(target), { recursive: true });
|
|
42
|
+
node_fs_1.default.writeFileSync(target, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
43
|
+
}
|
|
44
|
+
function shouldCheck(state) {
|
|
45
|
+
if (!state)
|
|
46
|
+
return true;
|
|
47
|
+
const elapsed = Date.now() - Date.parse(state.lastCheck);
|
|
48
|
+
return elapsed > CHECK_INTERVAL_MS;
|
|
49
|
+
}
|
|
50
|
+
// ── Registry fetch ──────────────────────────────────────────────────
|
|
51
|
+
async function fetchLatestVersion(packageName) {
|
|
52
|
+
const name = packageName ?? NPM_PACKAGE_NAME;
|
|
53
|
+
const url = `https://registry.npmjs.org/${name}/latest`;
|
|
54
|
+
try {
|
|
55
|
+
const response = await fetch(url, {
|
|
56
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
57
|
+
headers: { Accept: 'application/json' },
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok)
|
|
60
|
+
return null;
|
|
61
|
+
const payload = (await response.json());
|
|
62
|
+
if (typeof payload.version === 'string' && payload.version.length > 0) {
|
|
63
|
+
return payload.version;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ── Update check ────────────────────────────────────────────────────
|
|
72
|
+
async function checkForUpdate(stateFilePath) {
|
|
73
|
+
try {
|
|
74
|
+
const currentVersion = (0, version_1.readCliVersion)();
|
|
75
|
+
const latestVersion = await fetchLatestVersion();
|
|
76
|
+
if (!latestVersion)
|
|
77
|
+
return null;
|
|
78
|
+
const state = {
|
|
79
|
+
lastCheck: new Date().toISOString(),
|
|
80
|
+
latestVersion,
|
|
81
|
+
};
|
|
82
|
+
writeCheckState(state, stateFilePath);
|
|
83
|
+
return {
|
|
84
|
+
updateAvailable: semver_1.default.gt(latestVersion, currentVersion),
|
|
85
|
+
currentVersion,
|
|
86
|
+
latestVersion,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ── Daily notification banner ───────────────────────────────────────
|
|
94
|
+
async function maybeNotifyUpdate() {
|
|
95
|
+
try {
|
|
96
|
+
const state = readCheckState();
|
|
97
|
+
if (shouldCheck(state)) {
|
|
98
|
+
const result = await checkForUpdate();
|
|
99
|
+
if (result?.updateAvailable) {
|
|
100
|
+
printUpdateBanner(result.currentVersion, result.latestVersion);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// If we have a cached state that indicates an update is available,
|
|
105
|
+
// show the banner even if we don't re-check
|
|
106
|
+
if (state) {
|
|
107
|
+
const currentVersion = (0, version_1.readCliVersion)();
|
|
108
|
+
if (semver_1.default.gt(state.latestVersion, currentVersion)) {
|
|
109
|
+
printUpdateBanner(currentVersion, state.latestVersion);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Silent failure — update check should never break the CLI
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function printUpdateBanner(currentVersion, latestVersion) {
|
|
118
|
+
const line1 = ` Update available: ${currentVersion} → ${latestVersion}`;
|
|
119
|
+
const line2 = ' Run: npm install -g @uxmaltech/collab-cli';
|
|
120
|
+
const line3 = ' Or: collab upgrade';
|
|
121
|
+
const maxLen = Math.max(line1.length, line2.length, line3.length);
|
|
122
|
+
const border = '─'.repeat(maxLen + 2);
|
|
123
|
+
process.stderr.write('\n');
|
|
124
|
+
process.stderr.write(`╭${border}╮\n`);
|
|
125
|
+
process.stderr.write(`│${line1.padEnd(maxLen + 2)}│\n`);
|
|
126
|
+
process.stderr.write(`│${line2.padEnd(maxLen + 2)}│\n`);
|
|
127
|
+
process.stderr.write(`│${line3.padEnd(maxLen + 2)}│\n`);
|
|
128
|
+
process.stderr.write(`╰${border}╯\n`);
|
|
129
|
+
process.stderr.write('\n');
|
|
130
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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.readCliVersion = readCliVersion;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
/** Resolved path to the CLI's own package.json. */
|
|
10
|
+
const PACKAGE_JSON_PATH = node_path_1.default.resolve(__dirname, '../../package.json');
|
|
11
|
+
/**
|
|
12
|
+
* Reads the CLI version from the CLI's own `package.json`.
|
|
13
|
+
* Returns `'0.0.0'` when the file cannot be read or parsed.
|
|
14
|
+
*/
|
|
15
|
+
function readCliVersion() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = node_fs_1.default.readFileSync(PACKAGE_JSON_PATH, 'utf8');
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (typeof parsed.version === 'string' && parsed.version.length > 0) {
|
|
20
|
+
return parsed.version;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Fall back to a neutral value when package metadata cannot be read.
|
|
25
|
+
}
|
|
26
|
+
return '0.0.0';
|
|
27
|
+
}
|