@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,85 @@
|
|
|
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.Executor = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const shell_1 = require("./shell");
|
|
12
|
+
/**
|
|
13
|
+
* Central side-effect executor.
|
|
14
|
+
* All file writes and subprocess calls go through this class so that
|
|
15
|
+
* `--dry-run` can suppress them while still logging what *would* happen.
|
|
16
|
+
*/
|
|
17
|
+
class Executor {
|
|
18
|
+
dryRun;
|
|
19
|
+
cwd;
|
|
20
|
+
logger;
|
|
21
|
+
constructor(logger, options) {
|
|
22
|
+
this.logger = logger;
|
|
23
|
+
this.dryRun = options.dryRun;
|
|
24
|
+
this.cwd = options.cwd;
|
|
25
|
+
}
|
|
26
|
+
run(commandName, args, options = {}) {
|
|
27
|
+
const commandLine = (0, shell_1.toShellCommand)([commandName, ...args]);
|
|
28
|
+
this.logger.command([commandName, ...args], { verboseOnly: options.verboseOnly });
|
|
29
|
+
if (this.dryRun) {
|
|
30
|
+
return {
|
|
31
|
+
status: 0,
|
|
32
|
+
stdout: '',
|
|
33
|
+
stderr: '',
|
|
34
|
+
command: commandLine,
|
|
35
|
+
simulated: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const result = (0, node_child_process_1.spawnSync)(commandName, args, {
|
|
39
|
+
cwd: options.cwd ?? this.cwd,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
});
|
|
42
|
+
if (result.error) {
|
|
43
|
+
const errorCode = result.error.code;
|
|
44
|
+
if (errorCode === 'ENOENT') {
|
|
45
|
+
throw new errors_1.CliError(`Command not found: ${commandName}`);
|
|
46
|
+
}
|
|
47
|
+
throw new errors_1.CliError(result.error.message);
|
|
48
|
+
}
|
|
49
|
+
const outcome = {
|
|
50
|
+
status: result.status ?? 1,
|
|
51
|
+
stdout: result.stdout ?? '',
|
|
52
|
+
stderr: result.stderr ?? '',
|
|
53
|
+
command: commandLine,
|
|
54
|
+
simulated: false,
|
|
55
|
+
};
|
|
56
|
+
if (options.check ?? true) {
|
|
57
|
+
if (outcome.status !== 0) {
|
|
58
|
+
throw new errors_1.CommandExecutionError(`Command failed: ${commandLine}`, {
|
|
59
|
+
command: commandLine,
|
|
60
|
+
exitCode: outcome.status,
|
|
61
|
+
stderr: outcome.stderr,
|
|
62
|
+
stdout: outcome.stdout,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return outcome;
|
|
67
|
+
}
|
|
68
|
+
ensureDirectory(directoryPath) {
|
|
69
|
+
if (this.dryRun) {
|
|
70
|
+
this.logger.info(`[dry-run] mkdir -p ${directoryPath}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
node_fs_1.default.mkdirSync(directoryPath, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
writeFile(filePath, content, options = {}) {
|
|
76
|
+
const description = options.description ?? 'write file';
|
|
77
|
+
if (this.dryRun) {
|
|
78
|
+
this.logger.info(`[dry-run] ${description}: ${filePath}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
82
|
+
node_fs_1.default.writeFileSync(filePath, content, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.Executor = Executor;
|
|
@@ -0,0 +1,204 @@
|
|
|
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.loadGitHubAuth = loadGitHubAuth;
|
|
7
|
+
exports.isGitHubAuthValid = isGitHubAuthValid;
|
|
8
|
+
exports.ensureAuthGitIgnore = ensureAuthGitIgnore;
|
|
9
|
+
exports.runGitHubDeviceFlow = runGitHubDeviceFlow;
|
|
10
|
+
exports.storeGitHubToken = storeGitHubToken;
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
/**
|
|
14
|
+
* GitHub OAuth Device Flow for workspace-scoped authentication.
|
|
15
|
+
*
|
|
16
|
+
* Token is stored in `.collab/github-auth.json` and MUST be in `.gitignore`.
|
|
17
|
+
* It is NEVER logged, stored in config.json, or exposed to stage pipelines.
|
|
18
|
+
*/
|
|
19
|
+
const AUTH_FILENAME = 'github-auth.json';
|
|
20
|
+
/**
|
|
21
|
+
* Client ID for the collab-cli GitHub OAuth App.
|
|
22
|
+
* Override with COLLAB_GITHUB_CLIENT_ID env var until the app is registered.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_CLIENT_ID = process.env.COLLAB_GITHUB_CLIENT_ID ?? '';
|
|
25
|
+
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
26
|
+
const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
27
|
+
const SCOPES = 'repo read:org read:project';
|
|
28
|
+
// ────────────────────────────────────────────────────────────────
|
|
29
|
+
// Token persistence
|
|
30
|
+
// ────────────────────────────────────────────────────────────────
|
|
31
|
+
function authFilePath(collabDir) {
|
|
32
|
+
return node_path_1.default.join(collabDir, AUTH_FILENAME);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Loads the GitHub auth token from `.collab/github-auth.json`.
|
|
36
|
+
* Returns null if the file does not exist or cannot be parsed.
|
|
37
|
+
*/
|
|
38
|
+
function loadGitHubAuth(collabDir) {
|
|
39
|
+
const filePath = authFilePath(collabDir);
|
|
40
|
+
if (!node_fs_1.default.existsSync(filePath)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
45
|
+
const parsed = JSON.parse(raw);
|
|
46
|
+
if (!parsed.token || parsed.provider !== 'github') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function saveGitHubAuth(collabDir, auth) {
|
|
56
|
+
node_fs_1.default.mkdirSync(collabDir, { recursive: true });
|
|
57
|
+
node_fs_1.default.writeFileSync(authFilePath(collabDir), JSON.stringify(auth, null, 2) + '\n', 'utf8');
|
|
58
|
+
}
|
|
59
|
+
// ────────────────────────────────────────────────────────────────
|
|
60
|
+
// Token validation
|
|
61
|
+
// ────────────────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Validates the GitHub token by calling `GET /user`.
|
|
64
|
+
* Returns true if the token is valid and has the expected scopes.
|
|
65
|
+
*/
|
|
66
|
+
async function isGitHubAuthValid(auth) {
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch('https://api.github.com/user', {
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${auth.token}`,
|
|
71
|
+
Accept: 'application/vnd.github+json',
|
|
72
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return response.ok;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ────────────────────────────────────────────────────────────────
|
|
82
|
+
// .gitignore management
|
|
83
|
+
// ────────────────────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Ensures `.collab/.gitignore` contains `github-auth.json`.
|
|
86
|
+
*/
|
|
87
|
+
function ensureAuthGitIgnore(collabDir) {
|
|
88
|
+
const gitignorePath = node_path_1.default.join(collabDir, '.gitignore');
|
|
89
|
+
let content = '';
|
|
90
|
+
if (node_fs_1.default.existsSync(gitignorePath)) {
|
|
91
|
+
content = node_fs_1.default.readFileSync(gitignorePath, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
if (content.includes(AUTH_FILENAME)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const newLine = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
97
|
+
node_fs_1.default.writeFileSync(gitignorePath, `${content}${newLine}${AUTH_FILENAME}\n`, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
// ────────────────────────────────────────────────────────────────
|
|
100
|
+
// OAuth Device Flow
|
|
101
|
+
// ────────────────────────────────────────────────────────────────
|
|
102
|
+
async function requestDeviceCode(clientId) {
|
|
103
|
+
const response = await fetch(DEVICE_CODE_URL, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
Accept: 'application/json',
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
client_id: clientId,
|
|
111
|
+
scope: SCOPES,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const text = await response.text();
|
|
116
|
+
throw new Error(`GitHub device code request failed (${response.status}): ${text}`);
|
|
117
|
+
}
|
|
118
|
+
return (await response.json());
|
|
119
|
+
}
|
|
120
|
+
async function pollForAccessToken(clientId, deviceCode, interval, expiresIn) {
|
|
121
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
122
|
+
let pollInterval = interval * 1000;
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
125
|
+
const response = await fetch(ACCESS_TOKEN_URL, {
|
|
126
|
+
method: 'POST',
|
|
127
|
+
headers: {
|
|
128
|
+
Accept: 'application/json',
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
},
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
client_id: clientId,
|
|
133
|
+
device_code: deviceCode,
|
|
134
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
const body = (await response.json());
|
|
138
|
+
if (body.access_token) {
|
|
139
|
+
return body.access_token;
|
|
140
|
+
}
|
|
141
|
+
if (body.error === 'authorization_pending') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (body.error === 'slow_down') {
|
|
145
|
+
pollInterval += 5000;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (body.error === 'expired_token') {
|
|
149
|
+
throw new Error('GitHub device code expired. Please try again.');
|
|
150
|
+
}
|
|
151
|
+
if (body.error === 'access_denied') {
|
|
152
|
+
throw new Error('GitHub authorization was denied by the user.');
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`GitHub OAuth error: ${body.error ?? 'unknown'}`);
|
|
155
|
+
}
|
|
156
|
+
throw new Error('GitHub device code expired. Please try again.');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Runs the GitHub OAuth Device Flow interactively.
|
|
160
|
+
*
|
|
161
|
+
* 1. Requests a device code from GitHub
|
|
162
|
+
* 2. Displays the user code and opens the browser
|
|
163
|
+
* 3. Polls until the user authorizes or the code expires
|
|
164
|
+
* 4. Stores the token in `.collab/github-auth.json`
|
|
165
|
+
*/
|
|
166
|
+
async function runGitHubDeviceFlow(collabDir, log) {
|
|
167
|
+
const print = log ?? console.log;
|
|
168
|
+
const clientId = process.env.COLLAB_GITHUB_CLIENT_ID ?? DEFAULT_CLIENT_ID;
|
|
169
|
+
if (!clientId) {
|
|
170
|
+
throw new Error('GitHub OAuth client ID not configured. Set COLLAB_GITHUB_CLIENT_ID environment variable.');
|
|
171
|
+
}
|
|
172
|
+
print('Authorizing collab-cli with GitHub...');
|
|
173
|
+
const deviceCode = await requestDeviceCode(clientId);
|
|
174
|
+
print('');
|
|
175
|
+
print(` Open: ${deviceCode.verification_uri}`);
|
|
176
|
+
print(` Code: ${deviceCode.user_code}`);
|
|
177
|
+
print('');
|
|
178
|
+
print('Waiting for authorization...');
|
|
179
|
+
const token = await pollForAccessToken(clientId, deviceCode.device_code, deviceCode.interval, deviceCode.expires_in);
|
|
180
|
+
const auth = {
|
|
181
|
+
provider: 'github',
|
|
182
|
+
token,
|
|
183
|
+
scopes: SCOPES.split(' '),
|
|
184
|
+
created_at: new Date().toISOString(),
|
|
185
|
+
};
|
|
186
|
+
saveGitHubAuth(collabDir, auth);
|
|
187
|
+
ensureAuthGitIgnore(collabDir);
|
|
188
|
+
print('GitHub authorization complete.');
|
|
189
|
+
return auth;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Stores a pre-existing token (e.g. from `--github-token` flag).
|
|
193
|
+
*/
|
|
194
|
+
function storeGitHubToken(collabDir, token) {
|
|
195
|
+
const auth = {
|
|
196
|
+
provider: 'github',
|
|
197
|
+
token,
|
|
198
|
+
scopes: SCOPES.split(' '),
|
|
199
|
+
created_at: new Date().toISOString(),
|
|
200
|
+
};
|
|
201
|
+
saveGitHubAuth(collabDir, auth);
|
|
202
|
+
ensureAuthGitIgnore(collabDir);
|
|
203
|
+
return auth;
|
|
204
|
+
}
|
package/dist/lib/hash.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sha256 = sha256;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
function sha256(content) {
|
|
6
|
+
return (0, node_crypto_1.createHash)('sha256').update(content, 'utf8').digest('hex');
|
|
7
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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.checkHttpHealth = checkHttpHealth;
|
|
7
|
+
exports.checkTcpHealth = checkTcpHealth;
|
|
8
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
10
|
+
const DEFAULT_RETRIES = 15;
|
|
11
|
+
const DEFAULT_RETRY_DELAY_MS = 2_000;
|
|
12
|
+
function normalizeOptions(options) {
|
|
13
|
+
return {
|
|
14
|
+
timeoutMs: options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
15
|
+
retries: options?.retries ?? DEFAULT_RETRIES,
|
|
16
|
+
retryDelayMs: options?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS,
|
|
17
|
+
dryRun: options?.dryRun ?? false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function wait(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
async function retryCheck(name, options, check) {
|
|
24
|
+
const normalized = normalizeOptions(options);
|
|
25
|
+
if (normalized.dryRun) {
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
ok: true,
|
|
29
|
+
attempts: 0,
|
|
30
|
+
detail: 'skipped in dry-run mode',
|
|
31
|
+
skipped: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let lastFailure = null;
|
|
35
|
+
for (let attempt = 1; attempt <= normalized.retries; attempt += 1) {
|
|
36
|
+
const result = await check(normalized.timeoutMs);
|
|
37
|
+
if (result.ok) {
|
|
38
|
+
return {
|
|
39
|
+
...result,
|
|
40
|
+
attempts: attempt,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
lastFailure = {
|
|
44
|
+
...result,
|
|
45
|
+
attempts: attempt,
|
|
46
|
+
};
|
|
47
|
+
if (attempt < normalized.retries) {
|
|
48
|
+
await wait(normalized.retryDelayMs);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return (lastFailure ?? {
|
|
52
|
+
name,
|
|
53
|
+
ok: false,
|
|
54
|
+
attempts: normalized.retries,
|
|
55
|
+
detail: 'health check failed without details',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function checkHttpHealth(name, url, options) {
|
|
59
|
+
return retryCheck(name, options ?? {}, async (timeoutMs) => {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
});
|
|
67
|
+
if (response.ok) {
|
|
68
|
+
return {
|
|
69
|
+
name,
|
|
70
|
+
ok: true,
|
|
71
|
+
detail: `HTTP ${response.status} from ${url}`,
|
|
72
|
+
statusCode: response.status,
|
|
73
|
+
attempts: 1,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
ok: false,
|
|
79
|
+
detail: `HTTP ${response.status} from ${url}`,
|
|
80
|
+
statusCode: response.status,
|
|
81
|
+
attempts: 1,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
name,
|
|
87
|
+
ok: false,
|
|
88
|
+
detail: `failed to reach ${url}`,
|
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
|
90
|
+
attempts: 1,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function checkTcpHealth(name, host, port, options) {
|
|
99
|
+
return retryCheck(name, options ?? {}, async (timeoutMs) => {
|
|
100
|
+
const connection = await new Promise((resolve) => {
|
|
101
|
+
let resolved = false;
|
|
102
|
+
const socket = node_net_1.default.createConnection({ host, port });
|
|
103
|
+
const settle = (result) => {
|
|
104
|
+
if (resolved) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
resolved = true;
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
resolve(result);
|
|
110
|
+
};
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
socket.destroy();
|
|
113
|
+
settle({ ok: false, error: `timeout after ${timeoutMs}ms` });
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
socket.once('connect', () => {
|
|
116
|
+
socket.end();
|
|
117
|
+
settle({ ok: true });
|
|
118
|
+
});
|
|
119
|
+
socket.once('error', (error) => {
|
|
120
|
+
socket.destroy();
|
|
121
|
+
settle({ ok: false, error: error.message });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
if (connection.ok) {
|
|
125
|
+
return {
|
|
126
|
+
name,
|
|
127
|
+
ok: true,
|
|
128
|
+
detail: `TCP ${host}:${port} reachable`,
|
|
129
|
+
attempts: 1,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
name,
|
|
134
|
+
ok: false,
|
|
135
|
+
detail: `TCP ${host}:${port} unreachable`,
|
|
136
|
+
error: connection.error,
|
|
137
|
+
attempts: 1,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLogger = createLogger;
|
|
4
|
+
const ansi_1 = require("./ansi");
|
|
5
|
+
const shell_1 = require("./shell");
|
|
6
|
+
class ConsoleLogger {
|
|
7
|
+
verbosity;
|
|
8
|
+
constructor(verbosity) {
|
|
9
|
+
this.verbosity = verbosity;
|
|
10
|
+
}
|
|
11
|
+
info(message) {
|
|
12
|
+
if (this.verbosity === 'quiet') {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
process.stdout.write(` ${message}\n`);
|
|
16
|
+
}
|
|
17
|
+
debug(message) {
|
|
18
|
+
if (this.verbosity !== 'verbose') {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
process.stdout.write(` ${(0, ansi_1.dim)(message)}\n`);
|
|
22
|
+
}
|
|
23
|
+
warn(message) {
|
|
24
|
+
process.stderr.write(` ${(0, ansi_1.yellow)('Warning:')} ${message}\n`);
|
|
25
|
+
}
|
|
26
|
+
error(message) {
|
|
27
|
+
process.stderr.write(` ${(0, ansi_1.red)('Error:')} ${message}\n`);
|
|
28
|
+
}
|
|
29
|
+
result(message) {
|
|
30
|
+
process.stdout.write(`${message}\n`);
|
|
31
|
+
}
|
|
32
|
+
command(parts, options) {
|
|
33
|
+
if (this.verbosity === 'quiet') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (options?.verboseOnly && this.verbosity !== 'verbose') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(` ${(0, ansi_1.dim)('$')} ${(0, ansi_1.dim)((0, shell_1.toShellCommand)(parts))}\n`);
|
|
40
|
+
}
|
|
41
|
+
stageHeader(index, total, title) {
|
|
42
|
+
if (this.verbosity === 'quiet') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const tag = (0, ansi_1.bold)((0, ansi_1.cyan)(`[${index}/${total}]`));
|
|
46
|
+
process.stdout.write(`\n ${tag} ${(0, ansi_1.bold)(title)}\n`);
|
|
47
|
+
}
|
|
48
|
+
step(ok, message) {
|
|
49
|
+
if (this.verbosity === 'quiet') {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const marker = ok ? (0, ansi_1.green)(ansi_1.CHECK) : (0, ansi_1.red)(ansi_1.CROSS);
|
|
53
|
+
process.stdout.write(` ${marker} ${message}\n`);
|
|
54
|
+
}
|
|
55
|
+
workflowHeader(workflow, mode) {
|
|
56
|
+
process.stdout.write(`\n ${(0, ansi_1.bold)(workflow)} ${(0, ansi_1.dim)(`\u2014 ${mode}`)}\n`);
|
|
57
|
+
}
|
|
58
|
+
repoHeader(repoName, index, total) {
|
|
59
|
+
if (this.verbosity === 'quiet')
|
|
60
|
+
return;
|
|
61
|
+
const tag = (0, ansi_1.bold)((0, ansi_1.cyan)(`[repo ${index}/${total}]`));
|
|
62
|
+
process.stdout.write(`\n ${tag} ${(0, ansi_1.bold)(repoName)}\n`);
|
|
63
|
+
}
|
|
64
|
+
phaseHeader(title, subtitle) {
|
|
65
|
+
if (this.verbosity === 'quiet')
|
|
66
|
+
return;
|
|
67
|
+
// Only clear when stdout is a real terminal (skip in pipes/tests)
|
|
68
|
+
if (process.stdout.isTTY) {
|
|
69
|
+
process.stdout.write(ansi_1.CLEAR_SCREEN);
|
|
70
|
+
}
|
|
71
|
+
const line = (0, ansi_1.dim)('\u2500'.repeat(48));
|
|
72
|
+
const sub = subtitle ? ` ${(0, ansi_1.dim)(subtitle)}` : '';
|
|
73
|
+
process.stdout.write(`\n ${line}\n ${(0, ansi_1.bold)((0, ansi_1.cyan)(title))}${sub}\n ${line}\n\n`);
|
|
74
|
+
}
|
|
75
|
+
summaryFooter(entries) {
|
|
76
|
+
process.stdout.write(`\n ${(0, ansi_1.dim)('\u2500'.repeat(40))}\n`);
|
|
77
|
+
process.stdout.write(` ${(0, ansi_1.bold)((0, ansi_1.green)(ansi_1.CHECK))} ${(0, ansi_1.bold)('Init complete')}\n\n`);
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
process.stdout.write(` ${(0, ansi_1.dim)(entry.label + ':')} ${entry.value}\n`);
|
|
80
|
+
}
|
|
81
|
+
process.stdout.write('\n');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function createLogger(options) {
|
|
85
|
+
const verbosity = options.quiet ? 'quiet' : options.verbose ? 'verbose' : 'normal';
|
|
86
|
+
return new ConsoleLogger(verbosity);
|
|
87
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveMcpHttpTimeoutMs = resolveMcpHttpTimeoutMs;
|
|
4
|
+
exports.resolveMcpApiKey = resolveMcpApiKey;
|
|
5
|
+
exports.getMcpBaseUrl = getMcpBaseUrl;
|
|
6
|
+
exports.ingestDocuments = ingestDocuments;
|
|
7
|
+
exports.triggerGraphSeed = triggerGraphSeed;
|
|
8
|
+
const service_health_1 = require("./service-health");
|
|
9
|
+
const DEFAULT_MCP_HTTP_TIMEOUT_MS = 30_000;
|
|
10
|
+
function parseTimeoutMs(value, fallback) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number.parseInt(value, 10);
|
|
15
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
async function fetchWithTimeout(url, init, timeoutMs, operation) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
|
+
try {
|
|
24
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
28
|
+
throw new Error(`${operation} timed out after ${timeoutMs}ms`);
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function resolveMcpHttpTimeoutMs(env) {
|
|
37
|
+
return parseTimeoutMs(env.MCP_HTTP_TIMEOUT_MS, DEFAULT_MCP_HTTP_TIMEOUT_MS);
|
|
38
|
+
}
|
|
39
|
+
function resolveMcpApiKey(env) {
|
|
40
|
+
const explicit = env.MCP_API_KEY?.trim();
|
|
41
|
+
if (explicit) {
|
|
42
|
+
return explicit;
|
|
43
|
+
}
|
|
44
|
+
const firstFromList = env.MCP_API_KEYS
|
|
45
|
+
?.split(/[,\s]+/)
|
|
46
|
+
.map((key) => key.trim())
|
|
47
|
+
.find((key) => key.length > 0);
|
|
48
|
+
return firstFromList || undefined;
|
|
49
|
+
}
|
|
50
|
+
function getMcpBaseUrl(config) {
|
|
51
|
+
const env = (0, service_health_1.loadRuntimeEnv)(config);
|
|
52
|
+
const host = env.MCP_HOST || '127.0.0.1';
|
|
53
|
+
const port = env.MCP_PORT || '7337';
|
|
54
|
+
return `http://${host}:${port}`;
|
|
55
|
+
}
|
|
56
|
+
async function ingestDocuments(baseUrl, payload, apiKey, timeoutMs = DEFAULT_MCP_HTTP_TIMEOUT_MS) {
|
|
57
|
+
const headers = {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
};
|
|
60
|
+
if (apiKey) {
|
|
61
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
62
|
+
}
|
|
63
|
+
const response = await fetchWithTimeout(`${baseUrl}/api/v1/ingest`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers,
|
|
66
|
+
body: JSON.stringify(payload),
|
|
67
|
+
}, timeoutMs, 'MCP ingest request');
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
const body = await response.text();
|
|
70
|
+
throw new Error(`MCP ingest failed (${response.status}): ${body}`);
|
|
71
|
+
}
|
|
72
|
+
return (await response.json());
|
|
73
|
+
}
|
|
74
|
+
async function triggerGraphSeed(baseUrl, apiKey, timeoutMs = DEFAULT_MCP_HTTP_TIMEOUT_MS) {
|
|
75
|
+
const headers = {};
|
|
76
|
+
if (apiKey) {
|
|
77
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
78
|
+
}
|
|
79
|
+
const response = await fetchWithTimeout(`${baseUrl}/api/v1/seed/graph`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers,
|
|
82
|
+
}, timeoutMs, 'MCP graph seed request');
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const body = await response.text();
|
|
85
|
+
throw new Error(`MCP graph seed failed (${response.status}): ${body}`);
|
|
86
|
+
}
|
|
87
|
+
return (await response.json());
|
|
88
|
+
}
|
package/dist/lib/mode.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MODE = exports.COLLAB_MODES = void 0;
|
|
4
|
+
exports.isCollabMode = isCollabMode;
|
|
5
|
+
exports.parseMode = parseMode;
|
|
6
|
+
exports.describeMode = describeMode;
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
/** Supported execution modes for the collab workspace. */
|
|
9
|
+
exports.COLLAB_MODES = ['file-only', 'indexed'];
|
|
10
|
+
const MODE_SET = new Set(exports.COLLAB_MODES);
|
|
11
|
+
/** Default mode used when none is explicitly configured. */
|
|
12
|
+
exports.DEFAULT_MODE = 'file-only';
|
|
13
|
+
/** Type guard that checks whether a string is a valid {@link CollabMode}. */
|
|
14
|
+
function isCollabMode(value) {
|
|
15
|
+
return MODE_SET.has(value);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parses a mode string from CLI flags or config, returning the fallback
|
|
19
|
+
* when undefined. Throws on invalid values.
|
|
20
|
+
*/
|
|
21
|
+
function parseMode(value, fallback = exports.DEFAULT_MODE) {
|
|
22
|
+
if (value === undefined) {
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
if (isCollabMode(value)) {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
throw new errors_1.CliError(`Invalid mode '${value}'. Valid values: ${exports.COLLAB_MODES.join(', ')}`);
|
|
29
|
+
}
|
|
30
|
+
/** Returns a human-readable description of a mode. */
|
|
31
|
+
function describeMode(mode) {
|
|
32
|
+
if (mode === 'indexed') {
|
|
33
|
+
return 'indexed (starts infra + MCP and enables retrieval services)';
|
|
34
|
+
}
|
|
35
|
+
return 'file-only (no infra/MCP startup, local file workflow only)';
|
|
36
|
+
}
|