@uxmaltech/collab-cli 0.1.7 → 0.1.8
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/dist/commands/init.js +48 -2
- package/dist/lib/ecosystem.js +5 -0
- package/dist/lib/github-auth.js +2 -2
- package/dist/lib/mcp-contract.js +100 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -15,6 +15,7 @@ const compose_renderer_1 = require("../lib/compose-renderer");
|
|
|
15
15
|
const errors_1 = require("../lib/errors");
|
|
16
16
|
const github_search_1 = require("../lib/github-search");
|
|
17
17
|
const infra_type_1 = require("../lib/infra-type");
|
|
18
|
+
const mcp_contract_1 = require("../lib/mcp-contract");
|
|
18
19
|
const mode_1 = require("../lib/mode");
|
|
19
20
|
const parsers_1 = require("../lib/parsers");
|
|
20
21
|
const orchestrator_1 = require("../lib/orchestrator");
|
|
@@ -55,7 +56,7 @@ function inferComposeMode(config) {
|
|
|
55
56
|
}
|
|
56
57
|
return 'consolidated';
|
|
57
58
|
}
|
|
58
|
-
async function resolveWizardSelection(options, config, logger) {
|
|
59
|
+
async function resolveWizardSelection(options, config, logger, dryRun) {
|
|
59
60
|
const defaults = {
|
|
60
61
|
mode: (0, mode_1.parseMode)(options.mode, config.mode),
|
|
61
62
|
composeMode: parseComposeMode(options.composeMode, inferComposeMode(config)),
|
|
@@ -75,6 +76,7 @@ async function resolveWizardSelection(options, config, logger) {
|
|
|
75
76
|
throw new errors_1.CliError('--mcp-url is required with --infra-type remote in non-interactive mode.');
|
|
76
77
|
}
|
|
77
78
|
mcpUrl = (0, infra_type_1.validateMcpUrl)(options.mcpUrl);
|
|
79
|
+
await validateMcpServerContract(mcpUrl, logger, dryRun);
|
|
78
80
|
}
|
|
79
81
|
return {
|
|
80
82
|
mode,
|
|
@@ -104,6 +106,7 @@ async function resolveWizardSelection(options, config, logger) {
|
|
|
104
106
|
const rawUrl = options.mcpUrl
|
|
105
107
|
?? await (0, prompt_1.promptText)('MCP server base URL:', 'http://127.0.0.1:7337');
|
|
106
108
|
mcpUrl = (0, infra_type_1.validateMcpUrl)(rawUrl);
|
|
109
|
+
await validateMcpServerContract(mcpUrl, logger, dryRun);
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
// Skip compose-mode prompt when mode is file-only or infra is remote —
|
|
@@ -220,6 +223,45 @@ function buildGitHubAuthStage(effectiveConfig, logger, options) {
|
|
|
220
223
|
},
|
|
221
224
|
};
|
|
222
225
|
}
|
|
226
|
+
// ────────────────────────────────────────────────────────────────
|
|
227
|
+
// MCP contract validation (single-shot probe at URL-entry time)
|
|
228
|
+
// ────────────────────────────────────────────────────────────────
|
|
229
|
+
async function validateMcpServerContract(mcpUrl, logger, dryRun) {
|
|
230
|
+
const result = await (0, mcp_contract_1.probeMcpContract)(mcpUrl, {
|
|
231
|
+
contractRange: (0, ecosystem_1.getMcpContractRange)(),
|
|
232
|
+
dryRun,
|
|
233
|
+
});
|
|
234
|
+
if (result.skipped) {
|
|
235
|
+
logger.info('[dry-run] Skipped MCP contract validation.');
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
throw new errors_1.CliError(result.error ?? `MCP contract validation failed for ${mcpUrl}`);
|
|
240
|
+
}
|
|
241
|
+
const health = result.health;
|
|
242
|
+
logger.step(true, `MCP server reachable at ${mcpUrl}`);
|
|
243
|
+
logger.info(`Server version: ${health.version}`);
|
|
244
|
+
logger.info(`Contract version: ${health.contractVersion}`);
|
|
245
|
+
const deps = Object.entries(health.dependencies)
|
|
246
|
+
.map(([name, status]) => `${name} (${status})`)
|
|
247
|
+
.join(', ');
|
|
248
|
+
if (deps) {
|
|
249
|
+
logger.info(`Dependencies: ${deps}`);
|
|
250
|
+
}
|
|
251
|
+
if (result.contractCompatible === false) {
|
|
252
|
+
logger.warn(`MCP contract version ${health.contractVersion} may be incompatible ` +
|
|
253
|
+
`(CLI expects ${(0, ecosystem_1.getMcpContractRange)()}). Some features may not work correctly.`);
|
|
254
|
+
}
|
|
255
|
+
const degraded = Object.entries(health.dependencies)
|
|
256
|
+
.filter(([, status]) => status !== 'up');
|
|
257
|
+
if (degraded.length > 0) {
|
|
258
|
+
const names = degraded.map(([name, status]) => `${name} (${status})`).join(', ');
|
|
259
|
+
logger.warn(`Some MCP dependencies are not fully ready: ${names}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// ────────────────────────────────────────────────────────────────
|
|
263
|
+
// Business canon helpers
|
|
264
|
+
// ────────────────────────────────────────────────────────────────
|
|
223
265
|
const LOCAL_PATH_RE = /^[/~.]/;
|
|
224
266
|
/**
|
|
225
267
|
* Expands `~`, resolves to absolute, and validates the path is an existing directory.
|
|
@@ -685,6 +727,9 @@ async function runInfraOnly(context, options) {
|
|
|
685
727
|
mcpUrl = options.mcpUrl ? (0, infra_type_1.validateMcpUrl)(options.mcpUrl) : effectiveConfig.mcpUrl;
|
|
686
728
|
effectiveConfig.infraType = infraType;
|
|
687
729
|
effectiveConfig.mcpUrl = mcpUrl;
|
|
730
|
+
if (mcpUrl) {
|
|
731
|
+
await validateMcpServerContract(mcpUrl, context.logger, context.executor.dryRun);
|
|
732
|
+
}
|
|
688
733
|
}
|
|
689
734
|
const composeMode = parseComposeMode(options.composeMode, inferComposeMode(effectiveConfig));
|
|
690
735
|
const infraLabel = infraType === 'remote' ? 'Remote MCP services' : 'Docker + MCP services';
|
|
@@ -909,7 +954,7 @@ Examples:
|
|
|
909
954
|
}
|
|
910
955
|
// ── Step 1: Configuration wizard ────────────────────────
|
|
911
956
|
context.logger.phaseHeader('collab init', 'Configuration');
|
|
912
|
-
const selections = await resolveWizardSelection(options, context.config, context.logger);
|
|
957
|
+
const selections = await resolveWizardSelection(options, context.config, context.logger, context.executor.dryRun);
|
|
913
958
|
const preserveExisting = configExistedBefore && !options.force;
|
|
914
959
|
const effectiveConfig = {
|
|
915
960
|
...(0, config_1.defaultCollabConfig)(context.config.workspaceDir),
|
|
@@ -919,6 +964,7 @@ Examples:
|
|
|
919
964
|
mcpUrl: preserveExisting ? context.config.mcpUrl : selections.mcpUrl,
|
|
920
965
|
};
|
|
921
966
|
// ── Step 2: Business canon configuration ──────────────────
|
|
967
|
+
context.logger.phaseHeader('collab init', 'Business Canon');
|
|
922
968
|
const canons = await resolveBusinessCanon(options, effectiveConfig, context.logger);
|
|
923
969
|
if (canons) {
|
|
924
970
|
effectiveConfig.canons = canons;
|
package/dist/lib/ecosystem.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getMcpContractRange = getMcpContractRange;
|
|
6
7
|
exports.checkEcosystemCompatibility = checkEcosystemCompatibility;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -31,6 +32,10 @@ function readManifest() {
|
|
|
31
32
|
}
|
|
32
33
|
return manifest;
|
|
33
34
|
}
|
|
35
|
+
/** Returns the required MCP contract version range from the ecosystem manifest. */
|
|
36
|
+
function getMcpContractRange() {
|
|
37
|
+
return readManifest().collabArchitectureMcpContractRange;
|
|
38
|
+
}
|
|
34
39
|
function readCliVersion() {
|
|
35
40
|
const packagePath = node_path_1.default.resolve(__dirname, '../../package.json');
|
|
36
41
|
const pkg = readJsonFile(packagePath);
|
package/dist/lib/github-auth.js
CHANGED
|
@@ -19,9 +19,9 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
19
19
|
const AUTH_FILENAME = 'github-auth.json';
|
|
20
20
|
/**
|
|
21
21
|
* Client ID for the collab-cli GitHub OAuth App.
|
|
22
|
-
* Override with COLLAB_GITHUB_CLIENT_ID env var
|
|
22
|
+
* Override with COLLAB_GITHUB_CLIENT_ID env var for custom OAuth apps.
|
|
23
23
|
*/
|
|
24
|
-
const DEFAULT_CLIENT_ID = process.env.COLLAB_GITHUB_CLIENT_ID ?? '';
|
|
24
|
+
const DEFAULT_CLIENT_ID = process.env.COLLAB_GITHUB_CLIENT_ID ?? 'Ov23liocAEoUmWO39r5B';
|
|
25
25
|
const DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
26
26
|
const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
27
27
|
const SCOPES = 'repo read:org read:project';
|
|
@@ -0,0 +1,100 @@
|
|
|
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.probeMcpContract = probeMcpContract;
|
|
7
|
+
const semver_1 = __importDefault(require("semver"));
|
|
8
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 5_000;
|
|
9
|
+
/**
|
|
10
|
+
* Probes the MCP server's /health endpoint once and validates
|
|
11
|
+
* the response contract structure and version compatibility.
|
|
12
|
+
*
|
|
13
|
+
* This is a quick, single-attempt probe — not a retry loop.
|
|
14
|
+
* Use it immediately after URL entry to give fast feedback.
|
|
15
|
+
*/
|
|
16
|
+
async function probeMcpContract(baseUrl, options = {}) {
|
|
17
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
18
|
+
const contractRange = options.contractRange;
|
|
19
|
+
const dryRun = options.dryRun ?? false;
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
return { ok: true, skipped: true };
|
|
22
|
+
}
|
|
23
|
+
const healthUrl = `${baseUrl}/health`;
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
|
+
let response;
|
|
27
|
+
try {
|
|
28
|
+
response = await fetch(healthUrl, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
signal: controller.signal,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
skipped: false,
|
|
38
|
+
error: `MCP server at ${baseUrl} did not respond within ${timeoutMs / 1000}s.`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
skipped: false,
|
|
45
|
+
error: `MCP server unreachable at ${baseUrl}: ${message}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
skipped: false,
|
|
55
|
+
error: `MCP server returned HTTP ${response.status} at ${healthUrl}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = await response.json();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
skipped: false,
|
|
66
|
+
error: `MCP server returned invalid JSON from ${healthUrl}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (!isValidHealthResponse(body)) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
skipped: false,
|
|
73
|
+
error: 'MCP /health response has unexpected structure. Expected: { status, version, contractVersion, dependencies }',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const health = body;
|
|
77
|
+
let contractCompatible;
|
|
78
|
+
if (contractRange) {
|
|
79
|
+
contractCompatible = semver_1.default.satisfies(health.contractVersion, contractRange, {
|
|
80
|
+
includePrerelease: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
skipped: false,
|
|
86
|
+
health,
|
|
87
|
+
contractCompatible,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Type guard for the expected /health response shape. */
|
|
91
|
+
function isValidHealthResponse(body) {
|
|
92
|
+
if (typeof body !== 'object' || body === null)
|
|
93
|
+
return false;
|
|
94
|
+
const obj = body;
|
|
95
|
+
return (typeof obj.status === 'string' &&
|
|
96
|
+
typeof obj.version === 'string' &&
|
|
97
|
+
typeof obj.contractVersion === 'string' &&
|
|
98
|
+
typeof obj.dependencies === 'object' &&
|
|
99
|
+
obj.dependencies !== null);
|
|
100
|
+
}
|