@iicp/client 0.7.101 → 0.7.103
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/LICENSE +83 -61
- package/README.md +19 -2
- package/dist/backends/openai_compat.d.ts +3 -0
- package/dist/backends/openai_compat.d.ts.map +1 -1
- package/dist/backends/openai_compat.js +119 -0
- package/dist/backends/openai_compat.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +236 -34
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +26 -3
- package/dist/client.js.map +1 -1
- package/dist/iicp_tcp.d.ts +38 -0
- package/dist/iicp_tcp.d.ts.map +1 -1
- package/dist/iicp_tcp.js +164 -0
- package/dist/iicp_tcp.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp_negotiation.d.ts.map +1 -1
- package/dist/mcp_negotiation.js +13 -0
- package/dist/mcp_negotiation.js.map +1 -1
- package/dist/native_call_identity.d.ts +12 -0
- package/dist/native_call_identity.d.ts.map +1 -0
- package/dist/native_call_identity.js +44 -0
- package/dist/native_call_identity.js.map +1 -0
- package/dist/native_response_sequence.d.ts +32 -0
- package/dist/native_response_sequence.d.ts.map +1 -0
- package/dist/native_response_sequence.js +59 -0
- package/dist/native_response_sequence.js.map +1 -0
- package/dist/node.d.ts +5 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +59 -7
- package/dist/node.js.map +1 -1
- package/dist/operator_profile.d.ts +18 -0
- package/dist/operator_profile.d.ts.map +1 -0
- package/dist/operator_profile.js +22 -0
- package/dist/operator_profile.js.map +1 -0
- package/dist/relay_session.d.ts +16 -4
- package/dist/relay_session.d.ts.map +1 -1
- package/dist/relay_session.js +116 -1
- package/dist/relay_session.js.map +1 -1
- package/dist/runtime_health.d.ts +77 -0
- package/dist/runtime_health.d.ts.map +1 -0
- package/dist/runtime_health.js +65 -0
- package/dist/runtime_health.js.map +1 -0
- package/dist/selection.d.ts +32 -0
- package/dist/selection.d.ts.map +1 -1
- package/dist/selection.js +54 -0
- package/dist/selection.js.map +1 -1
- package/dist/service.d.ts +7 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +44 -0
- package/dist/service.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -4
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
44
44
|
const readline = require("node:readline/promises");
|
|
45
45
|
const node_process_1 = require("node:process");
|
|
46
46
|
const node_js_1 = require("./node.js");
|
|
47
|
+
const runtime_health_js_1 = require("./runtime_health.js");
|
|
47
48
|
const client_js_1 = require("./client.js");
|
|
48
49
|
const node_log_js_1 = require("./node_log.js");
|
|
49
50
|
const cip_policy_js_1 = require("./cip_policy.js");
|
|
@@ -53,6 +54,8 @@ const index_js_2 = require("./backends/index.js");
|
|
|
53
54
|
const recovery_js_1 = require("./recovery.js");
|
|
54
55
|
const identity_js_1 = require("./identity.js");
|
|
55
56
|
const delegation_js_1 = require("./delegation.js");
|
|
57
|
+
const operator_profile_js_1 = require("./operator_profile.js");
|
|
58
|
+
const updater_js_1 = require("./updater.js");
|
|
56
59
|
const mcp_policy_js_1 = require("./mcp_policy.js");
|
|
57
60
|
const mcp_negotiation_js_1 = require("./mcp_negotiation.js");
|
|
58
61
|
/**
|
|
@@ -170,6 +173,7 @@ function printHelp() {
|
|
|
170
173
|
` list List node configs saved under ~/.iicp/nodes/\n` +
|
|
171
174
|
` serve Register and serve a node\n` +
|
|
172
175
|
` doctor Check local health, directory presence, and recovery action\n` +
|
|
176
|
+
` healthcheck Check local runtime liveness/readiness snapshot\n` +
|
|
173
177
|
` query <prompt> Discover mesh nodes and submit a chat task\n` +
|
|
174
178
|
` credits Show your operator wallet plus this node's credit ledger\n` +
|
|
175
179
|
` proxy Run the local OpenAI/Ollama/Anthropic-compat gateway (loopback; no registration)\n` +
|
|
@@ -693,18 +697,6 @@ async function runServe(opts) {
|
|
|
693
697
|
allowCoordinator: true,
|
|
694
698
|
});
|
|
695
699
|
}
|
|
696
|
-
// 2-C: co-host the compat proxy on loopback alongside the node, supervised so a
|
|
697
|
-
// proxy failure logs but never drops the network-facing node. Forced to 127.0.0.1.
|
|
698
|
-
if (opts.withProxy) {
|
|
699
|
-
const pport = envInt("IICP_PROXY_PORT", 9483);
|
|
700
|
-
const pclient = new client_js_1.IicpClient({
|
|
701
|
-
directory_url: opts.directoryUrl,
|
|
702
|
-
region: opts.region,
|
|
703
|
-
});
|
|
704
|
-
const pserver = (0, index_js_1.createProxyServer)(pclient);
|
|
705
|
-
pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
|
|
706
|
-
pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
|
|
707
|
-
}
|
|
708
700
|
// Phase 2 (#529/#55) — capture any cached node_token so the node can prove
|
|
709
701
|
// ownership on re-registration (IICP-E050 token path).
|
|
710
702
|
let savedNodeToken;
|
|
@@ -717,6 +709,31 @@ async function runServe(opts) {
|
|
|
717
709
|
savedNodeToken = saved.node_token ?? undefined;
|
|
718
710
|
opts = applySavedNode(opts, saved);
|
|
719
711
|
}
|
|
712
|
+
const _op = (0, identity_js_1.loadOperator)();
|
|
713
|
+
const managedDecision = (0, operator_profile_js_1.evaluateManagedOperator)({
|
|
714
|
+
mode: (process.env.IICP_OPERATOR_PROFILE ?? "convenience").trim().toLowerCase(),
|
|
715
|
+
authentication_configured: !!_op && (0, identity_js_1.operatorIsKeyBacked)(_op),
|
|
716
|
+
identity_storage_protected: !!_op && (0, identity_js_1.operatorIsKeyBacked)(_op) && (0, identity_js_1.operatorIsEncrypted)(_op),
|
|
717
|
+
auto_update_requested: (0, updater_js_1.autoUpdateEnabled)(),
|
|
718
|
+
update_authenticated: envBool("IICP_MANAGED_UPDATE_AUTHENTICATED"),
|
|
719
|
+
rollback_verified: envBool("IICP_MANAGED_ROLLBACK_VERIFIED"),
|
|
720
|
+
upnp_requested: opts.autoDetectNat && !envBool("IICP_SKIP_UPNP"),
|
|
721
|
+
tunnel_requested: opts.tunnel !== false,
|
|
722
|
+
upnp_approved: envBool("IICP_MANAGED_UPNP_APPROVED"),
|
|
723
|
+
tunnel_approved: envBool("IICP_MANAGED_TUNNEL_APPROVED"),
|
|
724
|
+
});
|
|
725
|
+
if (!managedDecision.accepted) {
|
|
726
|
+
process.stderr.write(`ERROR: managed operator startup rejected: ${managedDecision.reason}\n`);
|
|
727
|
+
return 2;
|
|
728
|
+
}
|
|
729
|
+
// Start the optional co-hosted listener only after the operator profile passes.
|
|
730
|
+
if (opts.withProxy) {
|
|
731
|
+
const pport = envInt("IICP_PROXY_PORT", 9483);
|
|
732
|
+
const pclient = new client_js_1.IicpClient({ directory_url: opts.directoryUrl, region: opts.region });
|
|
733
|
+
const pserver = (0, index_js_1.createProxyServer)(pclient);
|
|
734
|
+
pserver.on("error", (e) => process.stderr.write(`co-hosted proxy error (node continues): ${String(e)}\n`));
|
|
735
|
+
pserver.listen(pport, "127.0.0.1", () => process.stdout.write(`co-hosted proxy → http://127.0.0.1:${pport} (OpenAI/Ollama/Anthropic compat)\n`));
|
|
736
|
+
}
|
|
720
737
|
// #410/#414 — built-in backend-url fallback applied LAST (after flag/env/saved-config),
|
|
721
738
|
// so a bare `serve --model x` works without --backend-url. An `anthropic` backend
|
|
722
739
|
// defaults to the Anthropic API, not localhost Ollama. Mirrors Python cli.py ~704.
|
|
@@ -984,7 +1001,6 @@ async function runServe(opts) {
|
|
|
984
1001
|
// #463/#464 — bind the operator identity: issue a delegation FROM the (key-backed) operator
|
|
985
1002
|
// identity for this node and advertise the public display_name. The directory verifies the
|
|
986
1003
|
// delegation (operator_pub == operator_id) and records the operator. Never sends the secret/contact.
|
|
987
|
-
const _op = (0, identity_js_1.loadOperator)();
|
|
988
1004
|
let _opDelegation;
|
|
989
1005
|
let _opDisplayName;
|
|
990
1006
|
let _opCreatedAt;
|
|
@@ -1959,6 +1975,50 @@ async function runDoctor(argv) {
|
|
|
1959
1975
|
process.stdout.write(" Note restart is automatic only when supervised services set IICP_SUPERVISED=1\n");
|
|
1960
1976
|
return 0;
|
|
1961
1977
|
}
|
|
1978
|
+
function runHealthcheck(argv) {
|
|
1979
|
+
const { values } = safeParseArgs({ args: argv, options: { node: { type: "string" }, json: { type: "boolean" }, ready: { type: "boolean" }, help: { type: "boolean", short: "h" } }, allowPositionals: false });
|
|
1980
|
+
if (values.help) {
|
|
1981
|
+
process.stdout.write("usage: iicp-node healthcheck --node NAME [--json] [--ready]\n");
|
|
1982
|
+
return 0;
|
|
1983
|
+
}
|
|
1984
|
+
const node = values.node ?? process.env.IICP_NODE_NAME;
|
|
1985
|
+
if (!node) {
|
|
1986
|
+
process.stderr.write("ERROR: healthcheck requires --node NAME\n");
|
|
1987
|
+
return 2;
|
|
1988
|
+
}
|
|
1989
|
+
let file;
|
|
1990
|
+
try {
|
|
1991
|
+
file = (0, runtime_health_js_1.runtimeHealthPath)(node);
|
|
1992
|
+
}
|
|
1993
|
+
catch (error) {
|
|
1994
|
+
process.stderr.write(`ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1995
|
+
return 2;
|
|
1996
|
+
}
|
|
1997
|
+
let snapshot;
|
|
1998
|
+
try {
|
|
1999
|
+
snapshot = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2000
|
+
}
|
|
2001
|
+
catch (error) {
|
|
2002
|
+
process.stderr.write(`INDETERMINATE: runtime-health snapshot unavailable: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
2003
|
+
return 2;
|
|
2004
|
+
}
|
|
2005
|
+
const progress = snapshot.progress;
|
|
2006
|
+
const staleAfter = Number(progress?.runtime?.stale_after_ms ?? 0);
|
|
2007
|
+
const age = Date.now() - fs.statSync(file).mtimeMs;
|
|
2008
|
+
if (age < 0 || !Number.isFinite(staleAfter) || staleAfter <= 0) {
|
|
2009
|
+
process.stderr.write("INDETERMINATE: snapshot freshness could not be established\n");
|
|
2010
|
+
return 2;
|
|
2011
|
+
}
|
|
2012
|
+
if (values.json)
|
|
2013
|
+
process.stdout.write(`${JSON.stringify(snapshot, null, 2)}\n`);
|
|
2014
|
+
else
|
|
2015
|
+
process.stdout.write(`IICP node health — ${node}\n liveness ${String(snapshot.liveness ?? "indeterminate")}\n readiness ${String(snapshot.readiness ?? "not_ready")}\n reasons ${Array.isArray(snapshot.reason_codes) ? snapshot.reason_codes.join(", ") : ""}\n`);
|
|
2016
|
+
if (age > staleAfter)
|
|
2017
|
+
return 1;
|
|
2018
|
+
if (values.ready)
|
|
2019
|
+
return snapshot.readiness === "ready" ? 0 : 1;
|
|
2020
|
+
return snapshot.liveness === "live" ? 0 : snapshot.liveness === "not_live" ? 1 : 2;
|
|
2021
|
+
}
|
|
1962
2022
|
/**
|
|
1963
2023
|
* Resolve a passphrase: $IICP_OPERATOR_PASSPHRASE if set (headless/CI), else an interactive
|
|
1964
2024
|
* readline prompt (this command is operator-run, so a prompt is fine here — only `serve` must
|
|
@@ -2687,22 +2747,126 @@ async function runMcpGateway(argv) {
|
|
|
2687
2747
|
}
|
|
2688
2748
|
}
|
|
2689
2749
|
let mcpRpcId = 0;
|
|
2690
|
-
|
|
2750
|
+
let legacySessionId;
|
|
2751
|
+
let legacyInitialization;
|
|
2752
|
+
class McpLegacySessionExpired extends Error {
|
|
2753
|
+
}
|
|
2754
|
+
async function mcpResponseJson(response) {
|
|
2755
|
+
const raw = await response.text();
|
|
2756
|
+
if ((response.headers.get("content-type") ?? "").includes("text/event-stream")) {
|
|
2757
|
+
const events = raw.split(/\r?\n/).filter((line) => line.startsWith("data: ")).map((line) => line.slice(6));
|
|
2758
|
+
if (!events.length)
|
|
2759
|
+
throw new Error("MCP server returned an empty event stream");
|
|
2760
|
+
return JSON.parse(events.at(-1) ?? "");
|
|
2761
|
+
}
|
|
2762
|
+
return JSON.parse(raw);
|
|
2763
|
+
}
|
|
2764
|
+
async function initializeLegacySession() {
|
|
2765
|
+
if (legacyInitialization)
|
|
2766
|
+
return legacyInitialization;
|
|
2767
|
+
legacyInitialization = (async () => {
|
|
2768
|
+
const id = ++mcpRpcId;
|
|
2769
|
+
const headers = {
|
|
2770
|
+
"Content-Type": "application/json",
|
|
2771
|
+
"MCP-Protocol-Version": mcp_negotiation_js_1.LEGACY_MCP_REVISION,
|
|
2772
|
+
"Accept": "application/json, text/event-stream",
|
|
2773
|
+
};
|
|
2774
|
+
const init = await fetch(`${mcpUrl}/mcp`, {
|
|
2775
|
+
method: "POST",
|
|
2776
|
+
headers,
|
|
2777
|
+
body: JSON.stringify({
|
|
2778
|
+
jsonrpc: "2.0", id, method: "initialize",
|
|
2779
|
+
params: { protocolVersion: mcp_negotiation_js_1.LEGACY_MCP_REVISION, capabilities: {}, clientInfo: { name: "iicp-mcp-gateway", version: "0.7" } },
|
|
2780
|
+
}),
|
|
2781
|
+
signal: AbortSignal.timeout(30_000),
|
|
2782
|
+
});
|
|
2783
|
+
if (!init.ok)
|
|
2784
|
+
throw new Error(`MCP legacy initialization failed: ${init.status}`);
|
|
2785
|
+
const initData = await mcpResponseJson(init);
|
|
2786
|
+
if (initData["error"])
|
|
2787
|
+
throw new Error("MCP server rejected legacy initialization");
|
|
2788
|
+
const sessionId = init.headers.get("mcp-session-id");
|
|
2789
|
+
if (!sessionId)
|
|
2790
|
+
throw new Error("MCP server did not return a legacy session identifier");
|
|
2791
|
+
const initialized = await fetch(`${mcpUrl}/mcp`, {
|
|
2792
|
+
method: "POST",
|
|
2793
|
+
headers: { ...headers, "Mcp-Session-Id": sessionId },
|
|
2794
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }),
|
|
2795
|
+
signal: AbortSignal.timeout(30_000),
|
|
2796
|
+
});
|
|
2797
|
+
if (!initialized.ok)
|
|
2798
|
+
throw new Error(`MCP legacy initialized notification failed: ${initialized.status}`);
|
|
2799
|
+
legacySessionId = sessionId;
|
|
2800
|
+
})();
|
|
2801
|
+
try {
|
|
2802
|
+
await legacyInitialization;
|
|
2803
|
+
}
|
|
2804
|
+
finally {
|
|
2805
|
+
legacyInitialization = undefined;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
async function legacyToolCall(rpc, replaySafe) {
|
|
2809
|
+
if (!legacySessionId)
|
|
2810
|
+
await initializeLegacySession();
|
|
2811
|
+
const send = async () => {
|
|
2812
|
+
if (!legacySessionId)
|
|
2813
|
+
throw new Error("legacy MCP session identifier is unavailable");
|
|
2814
|
+
const response = await fetch(`${mcpUrl}/mcp`, {
|
|
2815
|
+
method: "POST",
|
|
2816
|
+
headers: {
|
|
2817
|
+
"Content-Type": "application/json",
|
|
2818
|
+
"MCP-Protocol-Version": mcp_negotiation_js_1.LEGACY_MCP_REVISION,
|
|
2819
|
+
"Mcp-Session-Id": legacySessionId,
|
|
2820
|
+
"Accept": "application/json, text/event-stream",
|
|
2821
|
+
},
|
|
2822
|
+
body: JSON.stringify(rpc),
|
|
2823
|
+
signal: AbortSignal.timeout(30_000),
|
|
2824
|
+
});
|
|
2825
|
+
if (response.status === 401 || response.status === 404) {
|
|
2826
|
+
legacySessionId = undefined;
|
|
2827
|
+
throw new McpLegacySessionExpired("MCP legacy session expired");
|
|
2828
|
+
}
|
|
2829
|
+
if (!response.ok)
|
|
2830
|
+
throw new Error(`MCP server unreachable: ${response.status}`);
|
|
2831
|
+
return mcpResponseJson(response);
|
|
2832
|
+
};
|
|
2833
|
+
try {
|
|
2834
|
+
return await send();
|
|
2835
|
+
}
|
|
2836
|
+
catch (error) {
|
|
2837
|
+
if (!(error instanceof McpLegacySessionExpired))
|
|
2838
|
+
throw error;
|
|
2839
|
+
if (!replaySafe)
|
|
2840
|
+
throw new McpLegacySessionExpired("MCP legacy session expired; caller retry required");
|
|
2841
|
+
await initializeLegacySession();
|
|
2842
|
+
try {
|
|
2843
|
+
return await send();
|
|
2844
|
+
}
|
|
2845
|
+
catch (retryError) {
|
|
2846
|
+
if (retryError instanceof McpLegacySessionExpired)
|
|
2847
|
+
throw new Error("MCP legacy session expired after one reinitialization");
|
|
2848
|
+
throw retryError;
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
async function callMcp(toolName, args, replaySafe = false) {
|
|
2691
2853
|
mcpRpcId += 1;
|
|
2692
2854
|
const params = { name: toolName, arguments: args };
|
|
2693
2855
|
const modern = mcpRevision === mcp_negotiation_js_1.MODERN_MCP_REVISION
|
|
2694
2856
|
? (0, mcp_negotiation_js_1.buildModernMcpRequest)({ requestId: mcpRpcId, method: "tools/call", name: toolName, params, extensions: mcpExtensions })
|
|
2695
2857
|
: null;
|
|
2696
2858
|
const rpc = modern?.body ?? { jsonrpc: "2.0", id: mcpRpcId, method: "tools/call", params };
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2859
|
+
const data = modern
|
|
2860
|
+
? await (async () => {
|
|
2861
|
+
const resp = await fetch(`${mcpUrl}/mcp`, {
|
|
2862
|
+
method: "POST", headers: { "Content-Type": "application/json", ...modern.headers },
|
|
2863
|
+
body: JSON.stringify(rpc), signal: AbortSignal.timeout(30_000),
|
|
2864
|
+
});
|
|
2865
|
+
if (!resp.ok)
|
|
2866
|
+
throw new Error(`MCP server unreachable: ${resp.status}`);
|
|
2867
|
+
return resp.json();
|
|
2868
|
+
})()
|
|
2869
|
+
: await legacyToolCall(rpc, replaySafe);
|
|
2706
2870
|
if (modern)
|
|
2707
2871
|
(0, mcp_negotiation_js_1.validateModernMcpResponse)(data, mcpServerName);
|
|
2708
2872
|
if (data["error"])
|
|
@@ -2780,15 +2944,15 @@ async function runMcpGateway(argv) {
|
|
|
2780
2944
|
}
|
|
2781
2945
|
const taskId = body["task_id"] ?? (0, node_crypto_1.randomUUID)();
|
|
2782
2946
|
try {
|
|
2783
|
-
const result = await callMcp(toolName, args);
|
|
2947
|
+
const result = await callMcp(toolName, args, payload["mcp_replay_safe"] === true);
|
|
2784
2948
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2785
2949
|
res.end(JSON.stringify({ task_id: taskId, status: "completed", result, policy_receipt: toolPolicy.receipt(toolName, "allowed", argumentCount) }));
|
|
2786
2950
|
}
|
|
2787
2951
|
catch (err) {
|
|
2788
2952
|
const msg = err.message ?? "error";
|
|
2789
|
-
const code = msg.includes("unreachable") ? 502 : 422;
|
|
2953
|
+
const code = err instanceof McpLegacySessionExpired ? 409 : msg.includes("unreachable") ? 502 : 422;
|
|
2790
2954
|
res.writeHead(code, { "Content-Type": "application/json" });
|
|
2791
|
-
res.end(JSON.stringify({ error: msg }));
|
|
2955
|
+
res.end(JSON.stringify(err instanceof McpLegacySessionExpired ? { error: "mcp_session_expired_retry_required", retryable: true } : { error: msg }));
|
|
2792
2956
|
}
|
|
2793
2957
|
return;
|
|
2794
2958
|
}
|
|
@@ -2846,7 +3010,8 @@ async function runService(argv) {
|
|
|
2846
3010
|
` --node NAME Saved node name to serve (required)\n` +
|
|
2847
3011
|
` --name NAME Override service label/unit name\n` +
|
|
2848
3012
|
` --platform KIND auto | launchd | systemd (default auto)\n` +
|
|
2849
|
-
` --
|
|
3013
|
+
` --no-start Install and enable without starting the service\n` +
|
|
3014
|
+
` --dry-run Print unit/actions without changing files or service state\n`);
|
|
2850
3015
|
return subcmd ? 0 : 2;
|
|
2851
3016
|
}
|
|
2852
3017
|
if (!["install", "status", "restart", "uninstall"].includes(subcmd)) {
|
|
@@ -2859,6 +3024,7 @@ async function runService(argv) {
|
|
|
2859
3024
|
name: { type: "string" },
|
|
2860
3025
|
platform: { type: "string" },
|
|
2861
3026
|
"dry-run": { type: "boolean" },
|
|
3027
|
+
"no-start": { type: "boolean" },
|
|
2862
3028
|
help: { type: "boolean", short: "h" },
|
|
2863
3029
|
},
|
|
2864
3030
|
allowPositionals: false,
|
|
@@ -2868,17 +3034,37 @@ async function runService(argv) {
|
|
|
2868
3034
|
const node = values.node;
|
|
2869
3035
|
if (!node)
|
|
2870
3036
|
throw new CliError("service requires --node NAME");
|
|
2871
|
-
const { renderServiceUnit } = await import("./service.js");
|
|
3037
|
+
const { managerActions, renderServiceUnit } = await import("./service.js");
|
|
2872
3038
|
const unit = renderServiceUnit(node, values.name, values.platform ?? "auto");
|
|
3039
|
+
const dryRun = values["dry-run"] === true;
|
|
3040
|
+
const noStart = values["no-start"] === true;
|
|
3041
|
+
const printAction = (command, args) => process.stdout.write(`$ ${[command, ...args].map((part) => JSON.stringify(part)).join(" ")}\n`);
|
|
3042
|
+
const execute = (operation) => {
|
|
3043
|
+
for (const action of managerActions(unit, operation, noStart)) {
|
|
3044
|
+
if (dryRun) {
|
|
3045
|
+
printAction(action.command, action.args);
|
|
3046
|
+
continue;
|
|
3047
|
+
}
|
|
3048
|
+
const result = (0, node_child_process_1.spawnSync)(action.command, action.args, { stdio: "inherit" });
|
|
3049
|
+
if (result.error && !action.tolerateFailure)
|
|
3050
|
+
throw new CliError(`${action.command} failed: ${result.error.message}`);
|
|
3051
|
+
if ((result.status ?? (result.error ? 1 : 0)) !== 0 && !action.tolerateFailure) {
|
|
3052
|
+
throw new CliError(`${action.command} exited with status ${result.status ?? "unknown"}`);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
2873
3056
|
if (subcmd === "install") {
|
|
2874
|
-
if (
|
|
3057
|
+
if (dryRun) {
|
|
2875
3058
|
process.stdout.write(`# ${unit.platform} service: ${unit.name}\n# path: ${unit.path}\n${unit.content}`);
|
|
2876
3059
|
}
|
|
2877
3060
|
else {
|
|
2878
3061
|
fs.mkdirSync(path.dirname(unit.path), { recursive: true });
|
|
2879
|
-
|
|
3062
|
+
const temporary = `${unit.path}.tmp-${process.pid}`;
|
|
3063
|
+
fs.writeFileSync(temporary, unit.content, { mode: 0o600 });
|
|
3064
|
+
fs.renameSync(temporary, unit.path);
|
|
2880
3065
|
process.stdout.write(`Installed ${unit.platform} service unit: ${unit.path}\n`);
|
|
2881
3066
|
}
|
|
3067
|
+
execute("install");
|
|
2882
3068
|
process.stdout.write(`status: ${unit.statusHint}\n`);
|
|
2883
3069
|
process.stdout.write(`restart: ${unit.restartHint}\n`);
|
|
2884
3070
|
process.stdout.write(`logs: ${unit.logHint}\n`);
|
|
@@ -2886,11 +3072,25 @@ async function runService(argv) {
|
|
|
2886
3072
|
return 0;
|
|
2887
3073
|
}
|
|
2888
3074
|
if (subcmd === "status")
|
|
2889
|
-
|
|
3075
|
+
execute("status");
|
|
2890
3076
|
if (subcmd === "restart")
|
|
2891
|
-
|
|
2892
|
-
if (subcmd === "uninstall")
|
|
2893
|
-
|
|
3077
|
+
execute("restart");
|
|
3078
|
+
if (subcmd === "uninstall") {
|
|
3079
|
+
execute("uninstall");
|
|
3080
|
+
if (dryRun)
|
|
3081
|
+
process.stdout.write(`$ ${JSON.stringify("rm")} ${JSON.stringify("-f")} ${JSON.stringify(unit.path)}\n`);
|
|
3082
|
+
else
|
|
3083
|
+
fs.rmSync(unit.path, { force: true });
|
|
3084
|
+
if (unit.platform === "systemd") {
|
|
3085
|
+
if (dryRun)
|
|
3086
|
+
printAction("systemctl", ["--user", "daemon-reload"]);
|
|
3087
|
+
else {
|
|
3088
|
+
const result = (0, node_child_process_1.spawnSync)("systemctl", ["--user", "daemon-reload"], { stdio: "inherit" });
|
|
3089
|
+
if (result.error || result.status !== 0)
|
|
3090
|
+
throw new CliError("systemctl --user daemon-reload failed after uninstall");
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
2894
3094
|
return 0;
|
|
2895
3095
|
}
|
|
2896
3096
|
async function dispatch(argv) {
|
|
@@ -2905,6 +3105,8 @@ async function dispatch(argv) {
|
|
|
2905
3105
|
return runCredits(argv.slice(1));
|
|
2906
3106
|
if (cmd === "doctor")
|
|
2907
3107
|
return runDoctor(argv.slice(1));
|
|
3108
|
+
if (cmd === "healthcheck")
|
|
3109
|
+
return runHealthcheck(argv.slice(1));
|
|
2908
3110
|
if (cmd === "operator")
|
|
2909
3111
|
return runOperator(argv.slice(1));
|
|
2910
3112
|
if (cmd === "proxy")
|