@noimosai/platform-mcp 0.1.2 → 0.1.4
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 +17 -48
- package/bin/noimos-platform-mcp.mjs +4 -5
- package/dist/cli/cli.js +567 -0
- package/dist/cli/command-help.js +395 -0
- package/dist/cli/download.js +72 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/input-error.js +48 -0
- package/dist/cli/local-agents.js +199 -0
- package/dist/cli/mcp-supervisor.js +136 -0
- package/dist/cli/noimos-cli.js +72 -0
- package/dist/cli/project-archive.js +27 -0
- package/dist/cli/project-files.js +25 -0
- package/dist/cli/project-sync.js +259 -0
- package/dist/cli/setup-lock.js +1 -0
- package/dist/cli/setup.js +347 -0
- package/dist/cli/skills.js +142 -0
- package/dist/cli/team-harness-targets.js +337 -0
- package/dist/cli/team-harness.js +1562 -0
- package/dist/cli/team-resources.js +222 -0
- package/dist/cli/team-skills.js +457 -0
- package/dist/cli/update-bootstrap.js +437 -0
- package/dist/cli/update-release.js +212 -0
- package/dist/cli/version.js +1 -0
- package/dist/cli/wallet.js +85 -0
- package/dist/client/agent-run.js +1425 -0
- package/dist/client/browser.js +905 -0
- package/dist/client/distribution.js +2 -0
- package/dist/client/evm-wallet.js +214 -0
- package/dist/client/execution-context.js +14 -0
- package/dist/client/failure-recovery.js +128 -0
- package/dist/client/index.js +4 -225
- package/dist/client/local-input.js +33 -12
- package/dist/client/local-profile-lock.js +52 -0
- package/dist/client/local-profile.js +181 -0
- package/dist/client/local-projects.js +131 -0
- package/dist/client/local-setup.js +93 -0
- package/dist/client/realtime-voice.js +184 -0
- package/dist/client/tool-name-hints.js +48 -0
- package/dist/client/wallet-access.js +214 -0
- package/dist/mcp/agent-server.js +345 -0
- package/dist/mcp/context.js +86 -0
- package/dist/mcp/host-schema.js +76 -0
- package/dist/mcp/icon.js +1 -0
- package/dist/mcp/instructions.js +26 -0
- package/dist/mcp/local-projects.js +42 -0
- package/dist/mcp/local-upload.js +7 -13
- package/dist/mcp/noimos-runtime.js +18 -0
- package/dist/mcp/noimos-server.js +38 -0
- package/dist/mcp/operations.js +40 -0
- package/dist/mcp/parallel-result.js +17 -0
- package/dist/mcp/project-sync.js +105 -0
- package/dist/mcp/results-reply.js +15 -0
- package/dist/mcp/runtime.js +65 -11
- package/dist/mcp/server.js +382 -40
- package/dist/mcp/team-skill.js +63 -0
- package/dist/mcp/update-notice.js +75 -0
- package/dist/mcp/version.js +1 -0
- package/dist/schema/accountRequestMethods.js +1 -0
- package/dist/schema/boundedBody.js +30 -0
- package/dist/schema/cronValidator.js +150 -0
- package/dist/schema/platformApiKey.js +14 -0
- package/dist/schema/platformAppAuth.js +11 -0
- package/dist/schema/platformBillingConstants.js +4 -0
- package/dist/schema/platformCapabilityFamilies.js +293 -0
- package/dist/schema/platformConnectionCatalog.js +39 -0
- package/dist/schema/platformConnectionPolicy.js +33 -0
- package/dist/schema/platformCustomTools.js +148 -0
- package/dist/schema/platformEndUserApi.js +62 -0
- package/dist/schema/platformFunding.js +10 -0
- package/dist/schema/platformHttpConstraints.js +12 -0
- package/dist/schema/platformLocalSetup.js +25 -0
- package/dist/schema/platformMcpTools.js +7 -0
- package/dist/schema/platformMetaTools.js +63 -0
- package/dist/schema/platformMonitorSchedule.js +123 -0
- package/dist/schema/platformMonitors.js +159 -0
- package/dist/schema/platformOAuth.js +13 -0
- package/dist/schema/platformProjectPermissions.js +33 -0
- package/dist/schema/platformPublicExecution.js +202 -21
- package/dist/schema/platformRoutes.js +122 -0
- package/dist/schema/platformSkills.js +11 -0
- package/dist/schema/platformTeamSecretName.js +3 -0
- package/dist/schema/platformTeamSkillSlug.js +1 -0
- package/dist/schema/platformTeamSkills.js +302 -0
- package/dist/schema/platformToolAccess.js +62 -0
- package/dist/schema/platformTriggers.js +153 -0
- package/dist/schema/platformWorkflows.js +100 -0
- package/dist/schema/scheduleTimeZone.js +15 -0
- package/dist/schema/toolDiscovery.js +71 -0
- package/dist/schema/toolInputRepair.js +122 -0
- package/dist/schema/toolResultContext.js +51 -0
- package/dist/schema/untrustedContent.js +35 -0
- package/npm-shrinkwrap.json +73 -0
- package/package.json +5 -3
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants as osConstants, homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { realpath, unlink } from "node:fs/promises";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { localProfilePath, profileDirectory, readLocalProfile, readPrivateJson, writePrivateJson } from "../client/local-profile.js";
|
|
8
|
+
import { executable, managedCliChildEnv as childMarker, probeMcp } from "./local-agents.js";
|
|
9
|
+
import { mcpWorkerEnv, superviseMcp } from "./mcp-supervisor.js";
|
|
10
|
+
import { withProfileLock } from "./setup-lock.js";
|
|
11
|
+
import { compareCliVersions, confirmCliRelease, confirmReleaseManifest, fetchCliRelease, ReleaseManifestError, releaseCooldownMs, validCliVersion, verifyCliTarball } from "./update-release.js";
|
|
12
|
+
const targetSchema = z.object({ version: z.string().refine(validCliVersion), integrity: z.string().regex(/^sha512-[A-Za-z0-9+/]{86}==$/).optional(), verifiedProjectId: z.string().min(1).optional() }).strict();
|
|
13
|
+
const pendingSchema = targetSchema.extend({ verifiedProjectId: z.string().min(1), source: z.enum(["auto", "manual"]) }).strict();
|
|
14
|
+
const stateSchema = z.object({
|
|
15
|
+
managedBy: z.literal("relvo-cli"), protocol: z.literal(1), installationId: z.string().regex(/^[a-f0-9]{24}$/),
|
|
16
|
+
auto: z.boolean(), checkedAt: z.number().int().nonnegative(), active: targetSchema,
|
|
17
|
+
previous: targetSchema.optional(), pending: pendingSchema.optional(), pin: z.string().refine(validCliVersion).optional(),
|
|
18
|
+
quarantined: z.string().refine(validCliVersion).optional(),
|
|
19
|
+
}).strict();
|
|
20
|
+
const availableSchema = z.object({ version: z.string().refine(validCliVersion), announcedAt: z.number().int().nonnegative() }).strict();
|
|
21
|
+
const day = 24 * 60 * 60 * 1000;
|
|
22
|
+
const updateHelp = "Use relvo update [status|auto on|auto off|pin <exact-version>|rollback].";
|
|
23
|
+
function launchArgs(version, args) {
|
|
24
|
+
return ["--yes", "--ignore-scripts", "--registry=https://registry.npmjs.org", "--@relvo:registry=https://registry.npmjs.org", `--package=@relvo/cli@${version}`, "relvo", ...args];
|
|
25
|
+
}
|
|
26
|
+
function ready(profile, now) {
|
|
27
|
+
return profile?.state === "ready" && !profile.pendingAck && Date.parse(profile.grant.expiresAt) > now;
|
|
28
|
+
}
|
|
29
|
+
async function readState(path, installationId) {
|
|
30
|
+
try {
|
|
31
|
+
const state = stateSchema.parse(await readPrivateJson(path));
|
|
32
|
+
if (state.installationId !== installationId)
|
|
33
|
+
throw new Error("Different installation");
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
38
|
+
return null;
|
|
39
|
+
throw new Error("Relvo update state is unsafe or belongs to another installation. It was preserved.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const availablePath = (statePath) => join(dirname(statePath), "update-available.json");
|
|
43
|
+
async function readAvailable(path) {
|
|
44
|
+
try {
|
|
45
|
+
return availableSchema.parse(await readPrivateJson(path));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function noteAvailable(path, state, profile, options, now, signal) {
|
|
52
|
+
try {
|
|
53
|
+
const release = await fetchCliRelease("latest", options.registryFetch, signal, { now, cooldownMs: 0 });
|
|
54
|
+
if (!release || compareCliVersions(release.version, state.active.version) <= 0 || release.version === state.quarantined)
|
|
55
|
+
return;
|
|
56
|
+
await confirmReleaseManifest(release, profile.consoleUrl, profile.local, options.manifestFetch, signal);
|
|
57
|
+
if ((await readAvailable(path))?.version !== release.version)
|
|
58
|
+
await writePrivateJson(path, { version: release.version, announcedAt: 0 });
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
}
|
|
62
|
+
async function announceAvailable(path, active, quarantined, now, error) {
|
|
63
|
+
const available = await readAvailable(path);
|
|
64
|
+
if (!available || available.version === quarantined || compareCliVersions(available.version, active) <= 0 || now - available.announcedAt < day)
|
|
65
|
+
return;
|
|
66
|
+
error(`Relvo ${available.version} is available; this installation uses ${active}. To install it, run relvo update.`);
|
|
67
|
+
await writePrivateJson(path, { ...available, announcedAt: now }).catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
export async function removeOwnedUpdateState(profilePath, installationId) {
|
|
70
|
+
const path = join(dirname(localProfilePath(dirname(dirname(dirname(profilePath))))), "updates.json");
|
|
71
|
+
try {
|
|
72
|
+
await profileDirectory(profilePath);
|
|
73
|
+
await profileDirectory(localProfilePath(dirname(dirname(dirname(profilePath)))));
|
|
74
|
+
const state = await readState(path, installationId);
|
|
75
|
+
if (!state)
|
|
76
|
+
return "missing";
|
|
77
|
+
await unlink(path);
|
|
78
|
+
await unlink(availablePath(path)).catch(() => { });
|
|
79
|
+
return "removed";
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return "preserved";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function freshState(version, installationId) {
|
|
86
|
+
return { managedBy: "relvo-cli", protocol: 1, installationId, auto: true, checkedAt: 0, active: { version } };
|
|
87
|
+
}
|
|
88
|
+
function status(version, state, loggedIn, disabled) {
|
|
89
|
+
return { protocol: 1, bootstrapVersion: version, installation: loggedIn ? "ready" : "not-ready", autoUpdate: loggedIn && (state?.auto ?? true) && !disabled && !state?.pin,
|
|
90
|
+
activeVersion: state?.active.version ?? version, pendingVersion: state?.pending?.version ?? null, previousVersion: state?.previous?.version ?? null, pinnedVersion: state?.pin ?? null,
|
|
91
|
+
quarantinedVersion: state?.quarantined ?? null };
|
|
92
|
+
}
|
|
93
|
+
async function diagnose(target, profile, path, env, home, launcher, signal) {
|
|
94
|
+
const registration = { client: "codex", name: `relvo-${profile.installationId}`, command: launcher, args: launchArgs(target.version, ["mcp", "--profile", path]) };
|
|
95
|
+
await probeMcp(registration, { ...env, [childMarker]: "1" }, home, profile.grant.developerProjectId, signal);
|
|
96
|
+
}
|
|
97
|
+
async function delegate(launcher, version, args, env, home, error) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const child = spawn(launcher, launchArgs(version, args), { env: { ...env, HOME: home, [childMarker]: "1" }, stdio: "inherit" });
|
|
100
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
101
|
+
const forward = signals.map((signal) => { const listener = () => { child.kill(signal); }; process.on(signal, listener); return { signal, listener }; });
|
|
102
|
+
let settled = false;
|
|
103
|
+
const finish = (code) => { if (settled)
|
|
104
|
+
return; settled = true; for (const { signal, listener } of forward)
|
|
105
|
+
process.removeListener(signal, listener); resolve(code); };
|
|
106
|
+
child.once("error", () => { error("Relvo could not start the selected version. The command was not replayed; inspect its result before retrying."); finish(1); });
|
|
107
|
+
child.once("close", (code, signal) => finish(code ?? (signal ? 128 + (osConstants.signals[signal] ?? 0) : 1)));
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function selectedMcpVersion(profilePath, statePath, now) {
|
|
111
|
+
const profile = await readLocalProfile(profilePath);
|
|
112
|
+
if (!ready(profile, now))
|
|
113
|
+
return null;
|
|
114
|
+
const state = await readState(statePath, profile.installationId);
|
|
115
|
+
return state?.pending?.verifiedProjectId === profile.grant.developerProjectId ? state.pending.version : state?.active.version ?? null;
|
|
116
|
+
}
|
|
117
|
+
async function superviseManagedMcp(args, env, entry, options) {
|
|
118
|
+
const home = await realpath(options.home ?? homedir()), canonical = localProfilePath(home), statePath = join(dirname(canonical), "updates.json");
|
|
119
|
+
const { [childMarker]: _marker, ...plain } = env, shutdown = new AbortController();
|
|
120
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"], stop = () => { shutdown.abort(); };
|
|
121
|
+
for (const signal of signals)
|
|
122
|
+
process.once(signal, stop);
|
|
123
|
+
process.stdout.on("error", stop);
|
|
124
|
+
const start = (replacing) => {
|
|
125
|
+
const child = spawn(process.execPath, [entry, ...args], { env: { ...plain, HOME: home, [mcpWorkerEnv]: "1", ...(replacing ? {} : { [childMarker]: "1" }) }, stdio: ["pipe", "pipe", "inherit"] });
|
|
126
|
+
child.stdin.on("error", () => { });
|
|
127
|
+
return { stdin: child.stdin, stdout: child.stdout, kill: () => { child.kill(); }, exited: new Promise(settle => {
|
|
128
|
+
child.once("error", () => { settle(1); });
|
|
129
|
+
child.once("close", (code, signal) => { settle(code ?? (signal ? 128 + (osConstants.signals[signal] ?? 0) : 1)); });
|
|
130
|
+
}) };
|
|
131
|
+
};
|
|
132
|
+
try {
|
|
133
|
+
return await superviseMcp({ input: process.stdin, output: process.stdout, start, version: options.version, stop: shutdown.signal, target: () => selectedMcpVersion(canonical, statePath, (options.now ?? Date.now)()) });
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
for (const signal of signals)
|
|
137
|
+
process.removeListener(signal, stop);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export async function runManagedEntry(args, env, output, error, serve, options) {
|
|
141
|
+
const help = args.some((arg) => ["help", "--help", "-h"].includes(arg)), entry = options.backgroundEntryPoint;
|
|
142
|
+
const runCurrent = args[0] === "mcp" && !help && entry && env[mcpWorkerEnv] !== "1" && env.RELVO_AUTO_UPDATE !== "0" ? () => superviseManagedMcp(args, env, entry, options) : serve;
|
|
143
|
+
if (env[childMarker] === "1" || !args.length || help)
|
|
144
|
+
return runCurrent();
|
|
145
|
+
if (options.updateWorker && (args.length !== 1 || args[0] !== "--relvo-update-worker"))
|
|
146
|
+
return 1;
|
|
147
|
+
const explicit = args[0] === "update", disabled = env.RELVO_AUTO_UPDATE === "0", now = options.now ?? Date.now;
|
|
148
|
+
const mcp = args[0] === "mcp", shutdown = new AbortController();
|
|
149
|
+
const endSession = () => shutdown.abort();
|
|
150
|
+
const sessionSignal = options.sessionSignal ? AbortSignal.any([shutdown.signal, options.sessionSignal]) : shutdown.signal;
|
|
151
|
+
const updateSignal = mcp ? sessionSignal : options.updateWorker ? AbortSignal.timeout(60_000) : undefined;
|
|
152
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
153
|
+
let command;
|
|
154
|
+
let runRetained = runCurrent;
|
|
155
|
+
if (mcp) {
|
|
156
|
+
process.stdin.once("end", endSession);
|
|
157
|
+
process.stdin.once("close", endSession);
|
|
158
|
+
for (const signal of signals)
|
|
159
|
+
process.once(signal, endSession);
|
|
160
|
+
}
|
|
161
|
+
let selected;
|
|
162
|
+
try {
|
|
163
|
+
if (!validCliVersion(options.version))
|
|
164
|
+
throw new Error("The bundled Relvo version is invalid.");
|
|
165
|
+
const home = await realpath(options.home ?? homedir()), canonical = localProfilePath(home);
|
|
166
|
+
let profilePath = canonical;
|
|
167
|
+
if (args[0] === "mcp") {
|
|
168
|
+
const { values, tokens } = parseArgs({ args: args.slice(1), tokens: true, options: { profile: { type: "string" } } });
|
|
169
|
+
if (tokens.filter((token) => token.kind === "option").length !== 1 || !values.profile)
|
|
170
|
+
throw new Error("Exactly one managed profile must be selected.");
|
|
171
|
+
if (values.profile !== canonical)
|
|
172
|
+
throw new Error("Automatic updates require the canonical managed Relvo profile.");
|
|
173
|
+
profilePath = values.profile;
|
|
174
|
+
}
|
|
175
|
+
const maintenance = args[0] === "doctor" || args[0] === "logout";
|
|
176
|
+
const profile = await readLocalProfile(profilePath), usable = ready(profile, now());
|
|
177
|
+
const statePath = join(dirname(canonical), "updates.json");
|
|
178
|
+
const state = profile ? await readState(statePath, profile.installationId) : null;
|
|
179
|
+
const retained = state?.active;
|
|
180
|
+
if (!options.updateWorker && retained && retained.version !== options.version)
|
|
181
|
+
runRetained = async () => {
|
|
182
|
+
let launcher;
|
|
183
|
+
try {
|
|
184
|
+
launcher = options.launcher ?? await executable("npx", env);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
error("Relvo could not locate the launcher for its selected version. The command was not replayed.");
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
return delegate(launcher, retained.version, args, env, home, error);
|
|
191
|
+
};
|
|
192
|
+
if (["--version", "-v"].includes(args[0]) && args.length === 1) {
|
|
193
|
+
output(state?.active.version ?? options.version);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
if (explicit && args[1] === "status" && args.length === 2) {
|
|
197
|
+
output(JSON.stringify(status(options.version, state, usable, disabled), null, 2));
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
if (maintenance) {
|
|
201
|
+
if (state?.active.version && state.active.version !== options.version)
|
|
202
|
+
return delegate(options.launcher ?? await executable("npx", env), state.active.version, args, env, home, error);
|
|
203
|
+
return runCurrent();
|
|
204
|
+
}
|
|
205
|
+
if (!usable) {
|
|
206
|
+
if (options.updateWorker)
|
|
207
|
+
return 0;
|
|
208
|
+
if (explicit)
|
|
209
|
+
throw new Error("Run relvo setup to complete a current login before changing updates.");
|
|
210
|
+
if (state?.active.version && state.active.version !== options.version)
|
|
211
|
+
return delegate(options.launcher ?? await executable("npx", env), state.active.version, args, env, home, error);
|
|
212
|
+
return runCurrent();
|
|
213
|
+
}
|
|
214
|
+
if (explicit && !(args.length === 1 || args.length === 3 && args[1] === "auto" && ["on", "off"].includes(args[2]) || args.length === 3 && args[1] === "pin" && validCliVersion(args[2]) || args.length === 2 && args[1] === "rollback"))
|
|
215
|
+
throw new Error(updateHelp);
|
|
216
|
+
const backgroundEntryPoint = options.backgroundEntryPoint;
|
|
217
|
+
if (!explicit && !mcp && !options.updateWorker && backgroundEntryPoint && !disabled && (state?.auto ?? true) && !state?.pin && !state?.pending) {
|
|
218
|
+
if (now() - (state?.checkedAt ?? 0) >= day) {
|
|
219
|
+
try {
|
|
220
|
+
await withProfileLock(canonical, async () => {
|
|
221
|
+
const current = await readLocalProfile(canonical), saved = await readState(statePath, profile.installationId) ?? freshState(options.version, profile.installationId);
|
|
222
|
+
if (!ready(current, now()) || current.installationId !== profile.installationId || !saved.auto || saved.pin || saved.pending || now() - saved.checkedAt < day)
|
|
223
|
+
return;
|
|
224
|
+
saved.checkedAt = now();
|
|
225
|
+
await writePrivateJson(statePath, stateSchema.parse(saved));
|
|
226
|
+
const child = spawn(process.execPath, [backgroundEntryPoint, "--relvo-update-worker"], { env: { ...env, HOME: home }, detached: true, stdio: "ignore" });
|
|
227
|
+
child.once("error", () => { });
|
|
228
|
+
child.unref();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
catch { }
|
|
232
|
+
}
|
|
233
|
+
await announceAvailable(availablePath(statePath), retained?.version ?? options.version, state?.quarantined, now(), error);
|
|
234
|
+
return runRetained();
|
|
235
|
+
}
|
|
236
|
+
await profileDirectory(canonical);
|
|
237
|
+
await withProfileLock(canonical, async () => {
|
|
238
|
+
const currentProfile = await readLocalProfile(profilePath);
|
|
239
|
+
if (!ready(currentProfile, now()) || currentProfile.installationId !== profile.installationId)
|
|
240
|
+
throw new Error("Relvo login changed before update verification. Rerun the command.");
|
|
241
|
+
const saved = await readState(statePath, profile.installationId) ?? freshState(options.version, profile.installationId);
|
|
242
|
+
if (options.updateWorker && (disabled || !saved.auto || saved.pin || saved.pending?.source === "manual"))
|
|
243
|
+
return;
|
|
244
|
+
const save = () => writePrivateJson(statePath, stateSchema.parse(saved));
|
|
245
|
+
const launcher = async () => options.launcher ?? executable("npx", env);
|
|
246
|
+
const stillPublished = async (target) => {
|
|
247
|
+
if (!target.integrity)
|
|
248
|
+
return true;
|
|
249
|
+
try {
|
|
250
|
+
return await confirmCliRelease({ version: target.version, integrity: target.integrity }, options.registryFetch, updateSignal);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const startCommand = () => {
|
|
257
|
+
if (command)
|
|
258
|
+
return;
|
|
259
|
+
if (selected && selected.version !== options.version) {
|
|
260
|
+
command = launcher().then((path) => delegate(path, selected.version, args, env, home, error));
|
|
261
|
+
if (mcp)
|
|
262
|
+
void command.then(endSession, endSession);
|
|
263
|
+
}
|
|
264
|
+
else
|
|
265
|
+
command = runCurrent();
|
|
266
|
+
};
|
|
267
|
+
if (explicit && args[1] === "auto") {
|
|
268
|
+
saved.auto = args[2] === "on";
|
|
269
|
+
if (saved.auto) {
|
|
270
|
+
delete saved.pin;
|
|
271
|
+
saved.checkedAt = 0;
|
|
272
|
+
}
|
|
273
|
+
else if (saved.pending?.source === "auto")
|
|
274
|
+
delete saved.pending;
|
|
275
|
+
await save();
|
|
276
|
+
output(`Relvo automatic updates ${saved.auto ? "enabled" : "disabled"}. Changes apply on the next start.`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (explicit && args[1] === "rollback") {
|
|
280
|
+
if (!saved.previous)
|
|
281
|
+
throw new Error("No previously verified Relvo version is available.");
|
|
282
|
+
await diagnose(saved.previous, currentProfile, profilePath, env, home, await launcher());
|
|
283
|
+
saved.pending = { ...saved.previous, verifiedProjectId: currentProfile.grant.developerProjectId, source: "manual" };
|
|
284
|
+
saved.auto = false;
|
|
285
|
+
delete saved.pin;
|
|
286
|
+
await save();
|
|
287
|
+
output(`Relvo ${saved.pending.version} is staged for the next start. Automatic updates are disabled.`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!explicit && !disabled && saved.pending && (!mcp || saved.pending.verifiedProjectId === currentProfile.grant.developerProjectId)) {
|
|
291
|
+
const candidate = saved.pending, published = await stillPublished(candidate);
|
|
292
|
+
if (published === undefined)
|
|
293
|
+
error("Relvo could not re-check the staged version with the npm registry. It stays staged and the current version is retained.");
|
|
294
|
+
else {
|
|
295
|
+
try {
|
|
296
|
+
if (!published)
|
|
297
|
+
throw new Error("The registry no longer publishes the verified bytes.");
|
|
298
|
+
if (!mcp)
|
|
299
|
+
await diagnose(candidate, currentProfile, profilePath, env, home, await launcher(), updateSignal);
|
|
300
|
+
const { source, ...target } = candidate;
|
|
301
|
+
void source;
|
|
302
|
+
saved.previous = saved.active;
|
|
303
|
+
saved.active = target;
|
|
304
|
+
delete saved.pending;
|
|
305
|
+
delete saved.quarantined;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
saved.quarantined = candidate.version;
|
|
309
|
+
delete saved.pending;
|
|
310
|
+
saved.checkedAt = now();
|
|
311
|
+
if (saved.pin === saved.quarantined)
|
|
312
|
+
delete saved.pin;
|
|
313
|
+
error("The staged Relvo version failed its read-only check. The previously verified version is retained.");
|
|
314
|
+
}
|
|
315
|
+
await save();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
selected = saved.active;
|
|
319
|
+
if (mcp)
|
|
320
|
+
startCommand();
|
|
321
|
+
if (mcp && saved.pending && !disabled) {
|
|
322
|
+
const published = await stillPublished(saved.pending);
|
|
323
|
+
if (published === undefined)
|
|
324
|
+
return;
|
|
325
|
+
try {
|
|
326
|
+
if (!published)
|
|
327
|
+
throw new Error("The registry no longer publishes the verified bytes.");
|
|
328
|
+
await diagnose(saved.pending, currentProfile, profilePath, env, home, await launcher(), sessionSignal);
|
|
329
|
+
sessionSignal.throwIfAborted();
|
|
330
|
+
saved.pending.verifiedProjectId = currentProfile.grant.developerProjectId;
|
|
331
|
+
await save();
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
if (!sessionSignal.aborted) {
|
|
335
|
+
saved.quarantined = saved.pending.version;
|
|
336
|
+
delete saved.pending;
|
|
337
|
+
saved.checkedAt = now();
|
|
338
|
+
if (saved.pin === saved.quarantined)
|
|
339
|
+
delete saved.pin;
|
|
340
|
+
await save();
|
|
341
|
+
error("Relvo's staged update could not be verified for the current project. The active version was retained.");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const pinned = explicit && args[1] === "pin" ? args[2] : undefined;
|
|
347
|
+
const check = explicit || options.updateWorker || !disabled && saved.auto && !saved.pin && now() - saved.checkedAt >= day;
|
|
348
|
+
if (!check && !disabled && !mcp && now() - saved.checkedAt >= day) {
|
|
349
|
+
saved.checkedAt = now();
|
|
350
|
+
await save();
|
|
351
|
+
try {
|
|
352
|
+
const release = await fetchCliRelease("latest", options.registryFetch, undefined, { now: now(), cooldownMs: 0 });
|
|
353
|
+
if (!release || compareCliVersions(release.version, saved.active.version) <= 0 || release.version === saved.quarantined)
|
|
354
|
+
return;
|
|
355
|
+
await confirmReleaseManifest(release, currentProfile.consoleUrl, currentProfile.local, options.manifestFetch);
|
|
356
|
+
error(`Relvo ${release.version} is available; this installation uses ${saved.active.version}. To install it, run relvo update${saved.pin ? ` pin ${release.version}` : ""}.`);
|
|
357
|
+
}
|
|
358
|
+
catch { }
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (!check)
|
|
362
|
+
return;
|
|
363
|
+
saved.checkedAt = now();
|
|
364
|
+
await save();
|
|
365
|
+
if (options.updateWorker)
|
|
366
|
+
await noteAvailable(availablePath(statePath), saved, currentProfile, options, now(), updateSignal);
|
|
367
|
+
try {
|
|
368
|
+
const release = await fetchCliRelease(pinned ?? "latest", options.registryFetch, updateSignal, { now: now(), cooldownMs: explicit ? 0 : releaseCooldownMs });
|
|
369
|
+
if (!release || !pinned && compareCliVersions(release.version, saved.active.version) <= 0) {
|
|
370
|
+
if (explicit)
|
|
371
|
+
output("Relvo is already using the newest release.");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
await confirmReleaseManifest(release, currentProfile.consoleUrl, currentProfile.local, options.manifestFetch, updateSignal);
|
|
375
|
+
await verifyCliTarball(release, options.registryFetch, updateSignal);
|
|
376
|
+
await diagnose(release, currentProfile, profilePath, env, home, await launcher(), updateSignal);
|
|
377
|
+
if (mcp)
|
|
378
|
+
sessionSignal.throwIfAborted();
|
|
379
|
+
if (options.updateWorker) {
|
|
380
|
+
if (!await confirmCliRelease(release, options.registryFetch, updateSignal))
|
|
381
|
+
throw new Error("The registry no longer publishes the verified bytes.");
|
|
382
|
+
saved.previous = saved.active;
|
|
383
|
+
saved.active = { ...release, verifiedProjectId: currentProfile.grant.developerProjectId };
|
|
384
|
+
delete saved.pending;
|
|
385
|
+
}
|
|
386
|
+
else
|
|
387
|
+
saved.pending = { ...release, verifiedProjectId: currentProfile.grant.developerProjectId, source: explicit ? "manual" : "auto" };
|
|
388
|
+
delete saved.quarantined;
|
|
389
|
+
if (pinned) {
|
|
390
|
+
saved.pin = pinned;
|
|
391
|
+
saved.auto = false;
|
|
392
|
+
}
|
|
393
|
+
await save();
|
|
394
|
+
const message = `Relvo ${release.version} passed its read-only check and is staged for the next start. The current command keeps its selected version.`;
|
|
395
|
+
(explicit ? output : error)(message);
|
|
396
|
+
}
|
|
397
|
+
catch (failure) {
|
|
398
|
+
const refusal = failure instanceof ReleaseManifestError ? failure.message : undefined;
|
|
399
|
+
if (explicit)
|
|
400
|
+
throw new Error(`${refusal ?? "Relvo update could not be verified."} The active version and login were preserved${refusal ? "." : "; rerun relvo update later."}`);
|
|
401
|
+
if (!mcp || !sessionSignal.aborted)
|
|
402
|
+
error(`${refusal ?? "Relvo update check failed."} The current version remains usable; the next automatic check is in 24 hours.`);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
if (options.updateWorker)
|
|
406
|
+
return 0;
|
|
407
|
+
if (explicit)
|
|
408
|
+
return 0;
|
|
409
|
+
if (command)
|
|
410
|
+
return command;
|
|
411
|
+
if (!selected || selected.version === options.version)
|
|
412
|
+
return runCurrent();
|
|
413
|
+
return delegate(options.launcher ?? await executable("npx", env), selected.version, args, env, home, error);
|
|
414
|
+
}
|
|
415
|
+
catch (failure) {
|
|
416
|
+
if (options.updateWorker)
|
|
417
|
+
return 0;
|
|
418
|
+
if (command) {
|
|
419
|
+
error("Relvo update preparation stopped. The running session was retained.");
|
|
420
|
+
return command;
|
|
421
|
+
}
|
|
422
|
+
if (explicit) {
|
|
423
|
+
error(failure instanceof Error ? failure.message : "Relvo update failed without changing the login.");
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
error("Relvo update management is unavailable. Retaining the selected version; local configuration was preserved.");
|
|
427
|
+
return runRetained();
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
if (mcp) {
|
|
431
|
+
process.stdin.removeListener("end", endSession);
|
|
432
|
+
process.stdin.removeListener("close", endSession);
|
|
433
|
+
for (const signal of signals)
|
|
434
|
+
process.removeListener(signal, endSession);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const releaseCooldownMs = 72 * 60 * 60 * 1000;
|
|
3
|
+
const releaseError = () => new Error("Unable to verify a compatible Relvo CLI release from the official npm registry.");
|
|
4
|
+
const metadataLimit = 1024 * 1024;
|
|
5
|
+
const tarballLimit = 16 * 1024 * 1024;
|
|
6
|
+
const tarballUrl = (version) => `https://registry.npmjs.org/@relvo/cli/-/cli-${version}.tgz`;
|
|
7
|
+
export function validCliVersion(value) {
|
|
8
|
+
return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value)
|
|
9
|
+
&& value.split(".").every((part) => Number.isSafeInteger(Number(part)));
|
|
10
|
+
}
|
|
11
|
+
export function compareCliVersions(a, b) {
|
|
12
|
+
if (!validCliVersion(a) || !validCliVersion(b))
|
|
13
|
+
throw new Error("Invalid Relvo CLI version.");
|
|
14
|
+
const left = a.split(".").map(Number), right = b.split(".").map(Number);
|
|
15
|
+
for (let index = 0; index < 3; index++) {
|
|
16
|
+
if (left[index] !== right[index])
|
|
17
|
+
return left[index] < right[index] ? -1 : 1;
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
function record(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function compatibleNode(engine) {
|
|
25
|
+
if (typeof engine !== "string")
|
|
26
|
+
return false;
|
|
27
|
+
const match = /^>=(0|[1-9]\d*)(?:\.(0|[1-9]\d*)\.(0|[1-9]\d*))?$/.exec(engine);
|
|
28
|
+
if (!match)
|
|
29
|
+
return false;
|
|
30
|
+
const minimum = `${match[1]}.${match[2] ?? "0"}.${match[3] ?? "0"}`;
|
|
31
|
+
return validCliVersion(minimum) && compareCliVersions(process.versions.node, minimum) >= 0;
|
|
32
|
+
}
|
|
33
|
+
function releaseMetadata(value, selector) {
|
|
34
|
+
if (!record(value) || value.name !== "@relvo/cli" || typeof value.version !== "string"
|
|
35
|
+
|| !validCliVersion(value.version) || (selector !== "latest" && selector !== value.version)
|
|
36
|
+
|| value.relvoUpdateProtocol !== 1 || (value.deprecated !== undefined && value.deprecated !== "")
|
|
37
|
+
|| !record(value.engines) || !compatibleNode(value.engines.node) || !record(value.dist))
|
|
38
|
+
throw releaseError();
|
|
39
|
+
const { integrity, tarball } = value.dist;
|
|
40
|
+
if (tarball !== tarballUrl(value.version)
|
|
41
|
+
|| typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity))
|
|
42
|
+
throw releaseError();
|
|
43
|
+
const digest = integrity.slice("sha512-".length), bytes = Buffer.from(digest, "base64");
|
|
44
|
+
if (bytes.length !== 64 || bytes.toString("base64") !== digest)
|
|
45
|
+
throw releaseError();
|
|
46
|
+
return { version: value.version, integrity };
|
|
47
|
+
}
|
|
48
|
+
async function readLimited(response, limit, signal, consume) {
|
|
49
|
+
if (!response.ok || !response.body) {
|
|
50
|
+
await response.body?.cancel().catch(() => { });
|
|
51
|
+
throw releaseError();
|
|
52
|
+
}
|
|
53
|
+
const length = response.headers.get("content-length");
|
|
54
|
+
if (length !== null && (!/^\d+$/.test(length) || Number(length) > limit)) {
|
|
55
|
+
await response.body.cancel();
|
|
56
|
+
throw releaseError();
|
|
57
|
+
}
|
|
58
|
+
const reader = response.body.getReader();
|
|
59
|
+
let total = 0;
|
|
60
|
+
try {
|
|
61
|
+
for (;;) {
|
|
62
|
+
signal.throwIfAborted();
|
|
63
|
+
const part = await reader.read();
|
|
64
|
+
if (part.done)
|
|
65
|
+
break;
|
|
66
|
+
total += part.value.byteLength;
|
|
67
|
+
if (total > limit)
|
|
68
|
+
throw releaseError();
|
|
69
|
+
consume(part.value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
await reader.cancel().catch(() => { });
|
|
74
|
+
throw releaseError();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
signal.throwIfAborted();
|
|
80
|
+
}
|
|
81
|
+
const requestSignal = (signal, milliseconds) => signal ? AbortSignal.any([signal, AbortSignal.timeout(milliseconds)]) : AbortSignal.timeout(milliseconds);
|
|
82
|
+
async function download(url, accept, transport, signal, limit, consume) {
|
|
83
|
+
signal.throwIfAborted();
|
|
84
|
+
const response = await transport(url, { method: "GET", headers: { accept }, redirect: "error", credentials: "omit", signal });
|
|
85
|
+
await readLimited(response, limit, signal, consume);
|
|
86
|
+
}
|
|
87
|
+
async function fetchPackument(transport, signal) {
|
|
88
|
+
const chunks = [];
|
|
89
|
+
await download("https://registry.npmjs.org/@relvo%2fcli", "application/json", transport, requestSignal(signal, 3000), metadataLimit, (chunk) => chunks.push(chunk));
|
|
90
|
+
const document = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
91
|
+
if (!record(document) || document.name !== "@relvo/cli" || !record(document.versions))
|
|
92
|
+
throw releaseError();
|
|
93
|
+
const tags = document["dist-tags"];
|
|
94
|
+
return { versions: document.versions, latest: record(tags) ? tags.latest : undefined, times: record(document.time) ? document.time : {} };
|
|
95
|
+
}
|
|
96
|
+
function exactRelease(document, selector) {
|
|
97
|
+
const value = document.versions[selector], refused = (detail) => new ReleaseManifestError(`${releaseError().message} ${selector} ${detail}`);
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
throw refused("is not published.");
|
|
100
|
+
if (record(value) && value.deprecated)
|
|
101
|
+
throw refused("is deprecated. Choose another version.");
|
|
102
|
+
if (record(value) && record(value.engines) && !compatibleNode(value.engines.node))
|
|
103
|
+
throw refused(`does not support Node.js ${process.versions.node}.`);
|
|
104
|
+
return releaseMetadata(value, selector);
|
|
105
|
+
}
|
|
106
|
+
function selectRelease(document, selector, settled) {
|
|
107
|
+
if (selector !== "latest")
|
|
108
|
+
return exactRelease(document, selector);
|
|
109
|
+
const tag = document.latest;
|
|
110
|
+
if (typeof tag !== "string" || !validCliVersion(tag))
|
|
111
|
+
throw releaseError();
|
|
112
|
+
if (!settled)
|
|
113
|
+
return releaseMetadata(document.versions[tag], tag);
|
|
114
|
+
let best = null;
|
|
115
|
+
for (const [version, entry] of Object.entries(document.versions)) {
|
|
116
|
+
if (!validCliVersion(version) || compareCliVersions(version, tag) > 0)
|
|
117
|
+
continue;
|
|
118
|
+
const time = document.times[version], published = typeof time === "string" ? Date.parse(time) : Number.NaN;
|
|
119
|
+
if (!(published <= settled.now - settled.cooldownMs))
|
|
120
|
+
continue;
|
|
121
|
+
let release;
|
|
122
|
+
try {
|
|
123
|
+
release = releaseMetadata(entry, version);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!best || compareCliVersions(release.version, best.version) > 0)
|
|
129
|
+
best = release;
|
|
130
|
+
}
|
|
131
|
+
return best;
|
|
132
|
+
}
|
|
133
|
+
export async function verifyCliTarball(release, transport = fetch, signal) {
|
|
134
|
+
try {
|
|
135
|
+
const hash = createHash("sha512");
|
|
136
|
+
await download(tarballUrl(release.version), "application/octet-stream", transport, requestSignal(signal, 20000), tarballLimit, (chunk) => hash.update(chunk));
|
|
137
|
+
if (`sha512-${hash.digest("base64")}` !== release.integrity)
|
|
138
|
+
throw releaseError();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
throw releaseError();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function fetchCliRelease(selector, transport = fetch, signal, settled) {
|
|
145
|
+
if (selector !== "latest" && !validCliVersion(selector))
|
|
146
|
+
throw releaseError();
|
|
147
|
+
try {
|
|
148
|
+
return selectRelease(await fetchPackument(transport, signal), selector, settled);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw error instanceof ReleaseManifestError ? error : releaseError();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export const releaseManifestPath = "/relvo/cli-releases.json";
|
|
155
|
+
export class ReleaseManifestError extends Error {
|
|
156
|
+
}
|
|
157
|
+
const manifestRefusal = (detail) => new ReleaseManifestError(`${detail} It was not adopted.`);
|
|
158
|
+
export function releaseManifestUrl(consoleUrl, local) {
|
|
159
|
+
let url;
|
|
160
|
+
try {
|
|
161
|
+
url = new URL(consoleUrl);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw manifestRefusal("The Relvo console URL in the login is invalid.");
|
|
165
|
+
}
|
|
166
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
167
|
+
if (url.username || url.password || (url.protocol !== "https:" && !(local && loopback && url.protocol === "http:"))) {
|
|
168
|
+
throw manifestRefusal("The Relvo console URL in the login is not a trusted origin.");
|
|
169
|
+
}
|
|
170
|
+
return `${url.origin}${releaseManifestPath}`;
|
|
171
|
+
}
|
|
172
|
+
export function parseReleaseManifest(text) {
|
|
173
|
+
let document;
|
|
174
|
+
try {
|
|
175
|
+
document = JSON.parse(text);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw manifestRefusal("The Relvo console's release manifest is not valid JSON.");
|
|
179
|
+
}
|
|
180
|
+
if (!record(document) || document.schema !== 1 || document.package !== "@relvo/cli" || !record(document.releases)) {
|
|
181
|
+
throw manifestRefusal("The Relvo console's release manifest has an unexpected shape.");
|
|
182
|
+
}
|
|
183
|
+
const releases = {};
|
|
184
|
+
for (const [version, integrity] of Object.entries(document.releases)) {
|
|
185
|
+
if (validCliVersion(version) && typeof integrity === "string" && /^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity))
|
|
186
|
+
releases[version] = integrity;
|
|
187
|
+
}
|
|
188
|
+
return releases;
|
|
189
|
+
}
|
|
190
|
+
export async function confirmReleaseManifest(release, consoleUrl, local, transport = fetch, signal) {
|
|
191
|
+
const url = releaseManifestUrl(consoleUrl, local), chunks = [];
|
|
192
|
+
try {
|
|
193
|
+
await download(url, "application/json", transport, requestSignal(signal, 3000), metadataLimit, (chunk) => chunks.push(chunk));
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw manifestRefusal(`Relvo ${release.version} could not be checked against the Relvo console's release manifest.`);
|
|
197
|
+
}
|
|
198
|
+
const recorded = parseReleaseManifest(Buffer.concat(chunks).toString("utf8"))[release.version];
|
|
199
|
+
if (recorded === undefined)
|
|
200
|
+
throw manifestRefusal(`Relvo ${release.version} is on npm but is not listed in the Relvo console's release manifest.`);
|
|
201
|
+
if (recorded !== release.integrity)
|
|
202
|
+
throw manifestRefusal(`npm's tarball for Relvo ${release.version} differs from the Relvo console's release manifest.`);
|
|
203
|
+
}
|
|
204
|
+
export async function confirmCliRelease(release, transport = fetch, signal) {
|
|
205
|
+
try {
|
|
206
|
+
const current = releaseMetadata((await fetchPackument(transport, signal)).versions[release.version], release.version);
|
|
207
|
+
return current.integrity === release.integrity;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw releaseError();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLATFORM_CLI_VERSION = "0.1.18";
|