camstack 1.2.212 → 1.2.214

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.
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ hubRequest
4
+ } from "./chunk-BYNP7S4W.js";
5
+ import {
6
+ isOlder,
7
+ latestOnNpm
8
+ } from "./chunk-37A556YF.js";
9
+ import "./chunk-JPGJK5DC.js";
10
+ import "./chunk-LMMQX4CK.js";
11
+
12
+ // src/mcp/doctor-command.ts
13
+ import { parseArgs } from "util";
14
+ import { tryConnectHub } from "@camstack/develop";
15
+
16
+ // src/mcp/doctor.ts
17
+ import {
18
+ explainTlsFailure,
19
+ hubCaFileForServer,
20
+ resolveHubTls
21
+ } from "@camstack/develop";
22
+ var MIN_NODE_MAJOR = 24;
23
+ function nodeCheck(version) {
24
+ const major = Number.parseInt(version.replace(/^v/, "").split(".")[0] ?? "", 10);
25
+ if (Number.isNaN(major)) {
26
+ return {
27
+ id: "node",
28
+ label: "Node.js version",
29
+ status: "unknown",
30
+ detail: `cannot read "${version}"`
31
+ };
32
+ }
33
+ if (major >= MIN_NODE_MAJOR) {
34
+ return {
35
+ id: "node",
36
+ label: "Node.js version",
37
+ status: "ok",
38
+ detail: `${version} (>= ${MIN_NODE_MAJOR})`
39
+ };
40
+ }
41
+ return {
42
+ id: "node",
43
+ label: "Node.js version",
44
+ status: "fail",
45
+ detail: `${version} is older than the required ${MIN_NODE_MAJOR}`,
46
+ fix: `Install Node.js ${MIN_NODE_MAJOR} or newer, then reinstall: npm install -g camstack@latest`
47
+ };
48
+ }
49
+ async function versionCheck(deps) {
50
+ const latest = await latestOnNpm("camstack", deps.fetch);
51
+ if (latest.latest === null) {
52
+ return {
53
+ id: "version",
54
+ label: "CLI version",
55
+ status: "unknown",
56
+ detail: `${deps.cliVersion} installed; npm latest unreadable (${latest.error ?? "no answer"})`
57
+ };
58
+ }
59
+ if (isOlder(deps.cliVersion, latest.latest)) {
60
+ return {
61
+ id: "version",
62
+ label: "CLI version",
63
+ status: "fail",
64
+ detail: `${deps.cliVersion} installed, ${latest.latest} is on npm`,
65
+ fix: "npm install -g camstack@latest, then restart the MCP in your client (OpenCode and similar keep the old process)."
66
+ };
67
+ }
68
+ return {
69
+ id: "version",
70
+ label: "CLI version",
71
+ status: "ok",
72
+ detail: `${deps.cliVersion} (npm latest ${latest.latest})`
73
+ };
74
+ }
75
+ var NO_HUB = "no hub to check (see the session line)";
76
+ async function runDoctorChecks(deps) {
77
+ const checks = [nodeCheck(deps.nodeVersion)];
78
+ let outcome;
79
+ try {
80
+ outcome = await deps.connect();
81
+ } catch (err) {
82
+ const message = err instanceof Error ? err.message : String(err);
83
+ const tls = explainTlsFailure("the hub", err);
84
+ outcome = {
85
+ ok: false,
86
+ reason: "no-session",
87
+ message: tls ?? message,
88
+ action: "camstack login <host>"
89
+ };
90
+ }
91
+ if (!outcome.ok) {
92
+ checks.push(
93
+ { id: "hub", label: "Hub reachable", status: "unknown", detail: NO_HUB },
94
+ { id: "ca", label: "Hub CA pinned", status: "unknown", detail: NO_HUB },
95
+ {
96
+ id: "session",
97
+ label: "Session or token file",
98
+ status: "fail",
99
+ detail: outcome.message,
100
+ fix: `${outcome.action}; without a TTY: camstack login <host> --token-file <path-to-0600-file>`
101
+ },
102
+ { id: "scope", label: "Token scope", status: "unknown", detail: "no credential to inspect" },
103
+ await versionCheck(deps)
104
+ );
105
+ return checks;
106
+ }
107
+ const { connection } = outcome;
108
+ const server = connection.server;
109
+ const probe = await deps.probeHub(server);
110
+ const tlsExplained = probe.reachable ? null : explainTlsFailure(server, probe.cause);
111
+ checks.push(
112
+ probe.reachable ? { id: "hub", label: "Hub reachable", status: "ok", detail: server } : {
113
+ id: "hub",
114
+ label: "Hub reachable",
115
+ status: "fail",
116
+ detail: `${server}: ${probe.error ?? "no answer"}`,
117
+ fix: tlsExplained === null ? "Check the hub is running and the address is right (camstack login <host>)." : "Pin the hub CA (see the next line)."
118
+ }
119
+ );
120
+ const pinned = resolveHubTls(server, deps.env ?? process.env, deps.sessionDir);
121
+ if (!server.startsWith("https:")) {
122
+ checks.push({
123
+ id: "ca",
124
+ label: "Hub CA pinned",
125
+ status: "unknown",
126
+ detail: "plain http hub: TLS is not used"
127
+ });
128
+ } else if (pinned.ca) {
129
+ checks.push({
130
+ id: "ca",
131
+ label: "Hub CA pinned",
132
+ status: "ok",
133
+ detail: pinned.caFile ?? "pinned"
134
+ });
135
+ } else if (tlsExplained !== null) {
136
+ checks.push({
137
+ id: "ca",
138
+ label: "Hub CA pinned",
139
+ status: "fail",
140
+ detail: "no pinned CA and the hub certificate is not publicly trusted",
141
+ fix: `Save the hub's certificate or CA PEM to ${hubCaFileForServer(server, deps.sessionDir)} (or set $CAMSTACK_HUB_CA_FILE).`
142
+ });
143
+ } else {
144
+ checks.push({
145
+ id: "ca",
146
+ label: "Hub CA pinned",
147
+ status: "unknown",
148
+ detail: "no pin; fine while the hub certificate is publicly trusted"
149
+ });
150
+ }
151
+ checks.push({
152
+ id: "session",
153
+ label: "Session or token file",
154
+ status: "ok",
155
+ detail: `usable credential from ${connection.source}`
156
+ });
157
+ checks.push(
158
+ connection.tier === "admin" ? { id: "scope", label: "Token scope", status: "ok", detail: "admin" } : {
159
+ id: "scope",
160
+ label: "Token scope",
161
+ status: "unknown",
162
+ detail: "not admin: read tools work, configure and update tools are refused",
163
+ fix: connection.adminHint ?? "camstack login <host> as an administrator"
164
+ }
165
+ );
166
+ checks.push(await versionCheck(deps));
167
+ return checks;
168
+ }
169
+ var MARK = {
170
+ ok: "ok ",
171
+ fail: "FAIL ",
172
+ unknown: "unknown"
173
+ };
174
+ function formatDoctor(checks) {
175
+ const lines = ["camstack mcp doctor", ""];
176
+ for (const c of checks) {
177
+ lines.push(`[${MARK[c.status]}] ${c.label}: ${c.detail}`);
178
+ if (c.fix && c.status !== "ok") lines.push(` fix: ${c.fix}`);
179
+ }
180
+ const failed = checks.filter((c) => c.status === "fail").length;
181
+ lines.push("", failed === 0 ? "No failing check." : `${failed} failing check(s).`);
182
+ return lines.join("\n");
183
+ }
184
+ function doctorExitCode(checks) {
185
+ return checks.some((c) => c.status === "fail") ? 1 : 0;
186
+ }
187
+
188
+ // src/mcp/doctor-command.ts
189
+ async function probeHub(server) {
190
+ try {
191
+ const res = await hubRequest(`${server}/health`, { timeoutMs: 5e3 });
192
+ return res.ok ? { reachable: true } : { reachable: false, error: `answered ${res.status}` };
193
+ } catch (err) {
194
+ return {
195
+ reachable: false,
196
+ error: err instanceof Error ? err.message : String(err),
197
+ cause: err instanceof Error && err.cause !== void 0 ? err.cause : err
198
+ };
199
+ }
200
+ }
201
+ async function runDoctorCommand(args, cliVersion) {
202
+ const parsed = parseArgs({
203
+ args: [...args],
204
+ options: { server: { type: "string", short: "s" }, "token-file": { type: "string" } },
205
+ allowPositionals: false,
206
+ strict: true
207
+ });
208
+ const server = parsed.values.server;
209
+ const tokenFile = parsed.values["token-file"];
210
+ const checks = await runDoctorChecks({
211
+ cliVersion,
212
+ nodeVersion: process.version,
213
+ connect: () => tryConnectHub({
214
+ workspaceRoot: process.cwd(),
215
+ ...server ? { server } : {},
216
+ ...tokenFile ? { tokenFile } : {}
217
+ }),
218
+ probeHub,
219
+ fetch: (url, init) => fetch(url, init)
220
+ });
221
+ process.stdout.write(`${formatDoctor(checks)}
222
+ `);
223
+ return doctorExitCode(checks);
224
+ }
225
+ export {
226
+ runDoctorCommand
227
+ };