@corvio/admin-cli 0.1.0-beta.3 → 0.1.0-beta.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/dist/cli.js +18 -1
- package/dist/core.js +203 -27
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
option,
|
|
13
13
|
readAdminCredential,
|
|
14
14
|
readTextFile,
|
|
15
|
+
resolveNpmUpdateStatus,
|
|
15
16
|
runMain,
|
|
16
17
|
sleep,
|
|
17
18
|
storeAdminCredential,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
writeResult,
|
|
20
21
|
} from "./core.js";
|
|
21
22
|
|
|
22
|
-
const VERSION = "0.1.0-beta.
|
|
23
|
+
const VERSION = "0.1.0-beta.4";
|
|
23
24
|
const DEFAULT_API_BASE = "https://api.corvio.ai/v1";
|
|
24
25
|
const USER_AGENT = `corvio-admin-cli/${VERSION}`;
|
|
25
26
|
|
|
@@ -37,6 +38,7 @@ Usage:
|
|
|
37
38
|
corvio-admin worker-jobs list --purpose <reason>
|
|
38
39
|
corvio-admin usage events|summary --purpose <reason>
|
|
39
40
|
corvio-admin receipts get <id>
|
|
41
|
+
corvio-admin update check
|
|
40
42
|
|
|
41
43
|
Authentication:
|
|
42
44
|
CORVIO_ADMIN_API_KEY=cva_... takes precedence over the stored device-link login.
|
|
@@ -192,6 +194,20 @@ async function capabilities(options) {
|
|
|
192
194
|
writeResult({ cli_version: VERSION, auth_source: credential.source, api: index, principal: identity }, { json: options.json });
|
|
193
195
|
}
|
|
194
196
|
|
|
197
|
+
async function updateCheck(options) {
|
|
198
|
+
const { api } = await client(options);
|
|
199
|
+
const index = await api.request("/admin");
|
|
200
|
+
const result = await resolveNpmUpdateStatus({
|
|
201
|
+
currentVersion: VERSION,
|
|
202
|
+
policy: index?.distribution?.admin_cli || {
|
|
203
|
+
package: "@corvio/admin-cli",
|
|
204
|
+
dist_tag: "beta",
|
|
205
|
+
},
|
|
206
|
+
userAgent: USER_AGENT,
|
|
207
|
+
});
|
|
208
|
+
writeResult(result, { json: options.json });
|
|
209
|
+
}
|
|
210
|
+
|
|
195
211
|
async function catalog(options) {
|
|
196
212
|
const { api } = await client(options);
|
|
197
213
|
writeResult(await api.request("/admin/data/catalog"), { json: options.json });
|
|
@@ -247,6 +263,7 @@ async function main({ options, positionals }) {
|
|
|
247
263
|
throw new CliError("auth action must be login, status, or logout.");
|
|
248
264
|
}
|
|
249
265
|
if (command === "capabilities") return capabilities(options);
|
|
266
|
+
if (command === "update" && (action || "check") === "check") return updateCheck(options);
|
|
250
267
|
if (command === "skill" && action === "download") return downloadSkill(options);
|
|
251
268
|
if (command === "catalog") return catalog(options);
|
|
252
269
|
if (command === "query") return query(options);
|
package/dist/core.js
CHANGED
|
@@ -105,10 +105,92 @@ export function normalizeApiBase(value, fallback) {
|
|
|
105
105
|
return raw.endsWith("/v1") ? raw : `${raw}/v1`;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function parseSemver(value) {
|
|
109
|
+
const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
return {
|
|
112
|
+
core: match.slice(1, 4).map((item) => Number.parseInt(item, 10)),
|
|
113
|
+
pre: match[4] ? match[4].split(".") : [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function compareVersions(left, right) {
|
|
118
|
+
const a = parseSemver(left);
|
|
119
|
+
const b = parseSemver(right);
|
|
120
|
+
if (!a || !b) return 0;
|
|
121
|
+
for (let index = 0; index < 3; index += 1) {
|
|
122
|
+
if (a.core[index] !== b.core[index]) return a.core[index] > b.core[index] ? 1 : -1;
|
|
123
|
+
}
|
|
124
|
+
if (a.pre.length === 0 && b.pre.length > 0) return 1;
|
|
125
|
+
if (b.pre.length === 0 && a.pre.length > 0) return -1;
|
|
126
|
+
for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index += 1) {
|
|
127
|
+
if (a.pre[index] === undefined) return -1;
|
|
128
|
+
if (b.pre[index] === undefined) return 1;
|
|
129
|
+
if (a.pre[index] === b.pre[index]) continue;
|
|
130
|
+
const aNumber = /^\d+$/.test(a.pre[index]) ? Number.parseInt(a.pre[index], 10) : null;
|
|
131
|
+
const bNumber = /^\d+$/.test(b.pre[index]) ? Number.parseInt(b.pre[index], 10) : null;
|
|
132
|
+
if (aNumber !== null && bNumber !== null) return aNumber > bNumber ? 1 : -1;
|
|
133
|
+
if (aNumber !== null) return -1;
|
|
134
|
+
if (bNumber !== null) return 1;
|
|
135
|
+
return a.pre[index] > b.pre[index] ? 1 : -1;
|
|
136
|
+
}
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function resolveNpmUpdateStatus({
|
|
141
|
+
currentVersion,
|
|
142
|
+
policy,
|
|
143
|
+
userAgent,
|
|
144
|
+
allowRegistryFailure = false,
|
|
145
|
+
fetchImpl = globalThis.fetch,
|
|
146
|
+
registryBase = process.env.CORVIO_NPM_REGISTRY_BASE || "https://registry.npmjs.org",
|
|
147
|
+
}) {
|
|
148
|
+
const packageName = String(policy?.package || "").trim();
|
|
149
|
+
const distTag = String(policy?.dist_tag || "beta").trim();
|
|
150
|
+
if (!packageName) throw new CliError("The API did not advertise an npm package.", { code: "update_policy_invalid" });
|
|
151
|
+
const encoded = encodeURIComponent(packageName).replace("%40", "@");
|
|
152
|
+
let latestVersion = null;
|
|
153
|
+
let registryError = null;
|
|
154
|
+
try {
|
|
155
|
+
const registry = String(registryBase).replace(/\/+$/, "");
|
|
156
|
+
const response = await fetchImpl(`${registry}/${encoded}`, {
|
|
157
|
+
headers: { accept: "application/json", "user-agent": userAgent },
|
|
158
|
+
signal: AbortSignal.timeout(2500),
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) throw new Error(`npm registry returned ${response.status}`);
|
|
161
|
+
const metadata = await response.json();
|
|
162
|
+
latestVersion = String(metadata?.["dist-tags"]?.[distTag] || "").trim() || null;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
registryError = error instanceof Error ? error.message : String(error);
|
|
165
|
+
if (!allowRegistryFailure) {
|
|
166
|
+
throw new CliError(`Unable to check npm for updates: ${registryError}`, {
|
|
167
|
+
code: "update_check_failed",
|
|
168
|
+
exitCode: EXIT.REMOTE,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const minimum = String(policy.minimum_supported_version || "").trim() || null;
|
|
173
|
+
return {
|
|
174
|
+
package: packageName,
|
|
175
|
+
channel: String(policy.channel || distTag),
|
|
176
|
+
dist_tag: distTag,
|
|
177
|
+
current_version: currentVersion,
|
|
178
|
+
latest_published_version: latestVersion,
|
|
179
|
+
reviewed_version: String(policy.reviewed_version || "").trim() || null,
|
|
180
|
+
minimum_supported_version: minimum,
|
|
181
|
+
update_available: Boolean(latestVersion && compareVersions(latestVersion, currentVersion) > 0),
|
|
182
|
+
supported: minimum ? compareVersions(currentVersion, minimum) >= 0 : true,
|
|
183
|
+
install_command: String(policy.install_command || `npm install --global ${packageName}@${distTag}`),
|
|
184
|
+
automatic_install: false,
|
|
185
|
+
...(registryError ? { registry_error: registryError } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
108
189
|
export class ApiClient {
|
|
109
|
-
constructor({ baseUrl, credential = null, userAgent, fetchImpl = globalThis.fetch }) {
|
|
190
|
+
constructor({ baseUrl, credential = null, workspaceId = null, userAgent, fetchImpl = globalThis.fetch }) {
|
|
110
191
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
111
192
|
this.credential = credential;
|
|
193
|
+
this.workspaceId = String(workspaceId || "").trim() || null;
|
|
112
194
|
this.userAgent = userAgent;
|
|
113
195
|
this.fetchImpl = fetchImpl;
|
|
114
196
|
}
|
|
@@ -129,6 +211,10 @@ export class ApiClient {
|
|
|
129
211
|
throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
|
|
130
212
|
}
|
|
131
213
|
requestHeaders.authorization = `Bearer ${this.credential}`;
|
|
214
|
+
if (this.workspaceId) {
|
|
215
|
+
requestHeaders["X-Corvio-Workspace-Id"] = this.workspaceId;
|
|
216
|
+
requestHeaders["X-Corvio-Workspace-Context-Contract"] = "required-v1";
|
|
217
|
+
}
|
|
132
218
|
}
|
|
133
219
|
let response;
|
|
134
220
|
try {
|
|
@@ -179,6 +265,10 @@ export class ApiClient {
|
|
|
179
265
|
throw new CliError("Corvio authentication is required.", { code: "auth_required", exitCode: EXIT.AUTH });
|
|
180
266
|
}
|
|
181
267
|
requestHeaders.authorization = `Bearer ${this.credential}`;
|
|
268
|
+
if (this.workspaceId) {
|
|
269
|
+
requestHeaders["X-Corvio-Workspace-Id"] = this.workspaceId;
|
|
270
|
+
requestHeaders["X-Corvio-Workspace-Context-Contract"] = "required-v1";
|
|
271
|
+
}
|
|
182
272
|
}
|
|
183
273
|
let response;
|
|
184
274
|
try {
|
|
@@ -208,6 +298,31 @@ export class ApiClient {
|
|
|
208
298
|
}
|
|
209
299
|
return bytes;
|
|
210
300
|
}
|
|
301
|
+
|
|
302
|
+
async putUrl(url, content, { headers = {} } = {}) {
|
|
303
|
+
let response;
|
|
304
|
+
try {
|
|
305
|
+
response = await this.fetchImpl(String(url), {
|
|
306
|
+
method: "PUT",
|
|
307
|
+
headers,
|
|
308
|
+
body: content,
|
|
309
|
+
});
|
|
310
|
+
} catch (cause) {
|
|
311
|
+
throw new CliError(`Unable to upload the file: ${cause instanceof Error ? cause.message : String(cause)}`, {
|
|
312
|
+
code: "upload_network_error",
|
|
313
|
+
exitCode: EXIT.REMOTE,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
const message = (await response.text()).slice(0, 500);
|
|
318
|
+
throw new CliError(message || `File upload failed (${response.status}).`, {
|
|
319
|
+
code: "upload_failed",
|
|
320
|
+
status: response.status,
|
|
321
|
+
exitCode: EXIT.REMOTE,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { ok: true, status: response.status };
|
|
325
|
+
}
|
|
211
326
|
}
|
|
212
327
|
|
|
213
328
|
function configRoot(env = process.env) {
|
|
@@ -220,6 +335,10 @@ export function workspaceCredentialPath(env = process.env) {
|
|
|
220
335
|
return join(configRoot(env), "credentials.json");
|
|
221
336
|
}
|
|
222
337
|
|
|
338
|
+
export function workspaceContextPath(env = process.env) {
|
|
339
|
+
return join(configRoot(env), "workspace-context.json");
|
|
340
|
+
}
|
|
341
|
+
|
|
223
342
|
export function adminCredentialPath(env = process.env) {
|
|
224
343
|
return join(configRoot(env), "admin-credentials.json");
|
|
225
344
|
}
|
|
@@ -227,8 +346,8 @@ export function adminCredentialPath(env = process.env) {
|
|
|
227
346
|
export async function readWorkspaceCredential(env = process.env) {
|
|
228
347
|
const environmentKey = String(env.CORVIO_API_KEY || "").trim();
|
|
229
348
|
if (environmentKey) {
|
|
230
|
-
if (!environmentKey.startsWith("cvk_")) {
|
|
231
|
-
throw new CliError("CORVIO_API_KEY must be a Workspace cvk_ key.", {
|
|
349
|
+
if (!environmentKey.startsWith("cvu_") && !environmentKey.startsWith("cvk_")) {
|
|
350
|
+
throw new CliError("CORVIO_API_KEY must be a user cvu_ key or legacy Workspace cvk_ key.", {
|
|
232
351
|
code: "invalid_workspace_key",
|
|
233
352
|
exitCode: EXIT.AUTH,
|
|
234
353
|
});
|
|
@@ -245,14 +364,15 @@ export async function readWorkspaceCredential(env = process.env) {
|
|
|
245
364
|
}
|
|
246
365
|
const profileName = String(parsed.active_profile || "default");
|
|
247
366
|
const profile = parsed.profiles?.[profileName];
|
|
248
|
-
const
|
|
249
|
-
|
|
367
|
+
const metadata = String(parsed.api_key || "").trim() ? parsed : profile;
|
|
368
|
+
const key = String(metadata?.api_key || "").trim();
|
|
369
|
+
if (!key.startsWith("cvu_") && !key.startsWith("cvk_")) {
|
|
250
370
|
throw new CliError("Stored Corvio credentials are invalid; run `corvio auth login` again.", {
|
|
251
371
|
code: "credential_store_invalid",
|
|
252
372
|
exitCode: EXIT.AUTH,
|
|
253
373
|
});
|
|
254
374
|
}
|
|
255
|
-
return { key, source: "device_link", profile: profileName, metadata
|
|
375
|
+
return { key, source: "device_link", profile: profileName, metadata };
|
|
256
376
|
}
|
|
257
377
|
|
|
258
378
|
export async function storeWorkspaceCredential(payload, env = process.env) {
|
|
@@ -260,28 +380,73 @@ export async function storeWorkspaceCredential(payload, env = process.env) {
|
|
|
260
380
|
const root = dirname(path);
|
|
261
381
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
262
382
|
await chmod(root, 0o700).catch(() => {});
|
|
263
|
-
const
|
|
383
|
+
const document = {
|
|
384
|
+
version: 2,
|
|
385
|
+
api_key: payload.secret,
|
|
386
|
+
key_id: payload.key_id,
|
|
387
|
+
audience: payload.audience || "user",
|
|
388
|
+
scopes: payload.scopes || [],
|
|
389
|
+
expires_at: payload.expires_at || null,
|
|
390
|
+
created_at: payload.created_at || new Date().toISOString(),
|
|
391
|
+
source: "device_link",
|
|
392
|
+
};
|
|
393
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
394
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
395
|
+
await chmod(temporary, 0o600);
|
|
396
|
+
await rename(temporary, path);
|
|
397
|
+
await chmod(path, 0o600);
|
|
398
|
+
return { path, profile: "default" };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function readWorkspaceContext(env = process.env) {
|
|
402
|
+
const environmentWorkspaceId = String(env.CORVIO_WORKSPACE_ID || "").trim();
|
|
403
|
+
if (environmentWorkspaceId) {
|
|
404
|
+
return { workspaceId: environmentWorkspaceId, workspaceName: null, source: "environment" };
|
|
405
|
+
}
|
|
406
|
+
const path = workspaceContextPath(env);
|
|
407
|
+
try {
|
|
408
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
409
|
+
const workspaceId = String(parsed.active_workspace_id || "").trim();
|
|
410
|
+
if (!workspaceId) return null;
|
|
411
|
+
return {
|
|
412
|
+
workspaceId,
|
|
413
|
+
workspaceName: String(parsed.active_workspace_name || "").trim() || null,
|
|
414
|
+
source: "stored",
|
|
415
|
+
};
|
|
416
|
+
} catch (cause) {
|
|
417
|
+
if (cause?.code === "ENOENT") return null;
|
|
418
|
+
throw new CliError("Stored Corvio Workspace selection is unreadable.", {
|
|
419
|
+
code: "workspace_context_store_invalid",
|
|
420
|
+
exitCode: EXIT.AUTH,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export async function storeWorkspaceContext({ workspaceId, workspaceName = null }, env = process.env) {
|
|
426
|
+
const normalizedId = String(workspaceId || "").trim();
|
|
427
|
+
if (!normalizedId) throw new CliError("Workspace ID is required.", { code: "workspace_selection_required" });
|
|
428
|
+
const path = workspaceContextPath(env);
|
|
429
|
+
const root = dirname(path);
|
|
430
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
431
|
+
await chmod(root, 0o700).catch(() => {});
|
|
264
432
|
const document = {
|
|
265
433
|
version: 1,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
api_key: payload.secret,
|
|
270
|
-
key_id: payload.key_id,
|
|
271
|
-
workspace_id: payload.workspace_id,
|
|
272
|
-
workspace_name: payload.workspace_name,
|
|
273
|
-
scopes: payload.scopes || [],
|
|
274
|
-
created_at: payload.created_at || new Date().toISOString(),
|
|
275
|
-
source: "device_link",
|
|
276
|
-
},
|
|
277
|
-
},
|
|
434
|
+
active_workspace_id: normalizedId,
|
|
435
|
+
active_workspace_name: String(workspaceName || "").trim() || null,
|
|
436
|
+
updated_at: new Date().toISOString(),
|
|
278
437
|
};
|
|
279
438
|
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
280
439
|
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 0o600, flag: "wx" });
|
|
281
440
|
await chmod(temporary, 0o600);
|
|
282
441
|
await rename(temporary, path);
|
|
283
442
|
await chmod(path, 0o600);
|
|
284
|
-
return { path,
|
|
443
|
+
return { path, workspaceId: normalizedId };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export async function clearWorkspaceContext(env = process.env) {
|
|
447
|
+
const path = workspaceContextPath(env);
|
|
448
|
+
await rm(path, { force: true });
|
|
449
|
+
return path;
|
|
285
450
|
}
|
|
286
451
|
|
|
287
452
|
export async function clearWorkspaceCredential(env = process.env) {
|
|
@@ -388,13 +553,24 @@ export async function openBrowser(url) {
|
|
|
388
553
|
const target = String(url);
|
|
389
554
|
const command = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open";
|
|
390
555
|
const args = platform() === "win32" ? ["/c", "start", "", target] : [target];
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
556
|
+
return new Promise((resolvePromise) => {
|
|
557
|
+
let settled = false;
|
|
558
|
+
const finish = (opened) => {
|
|
559
|
+
if (settled) return;
|
|
560
|
+
settled = true;
|
|
561
|
+
resolvePromise(opened);
|
|
562
|
+
};
|
|
563
|
+
try {
|
|
564
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
565
|
+
child.once("error", () => finish(false));
|
|
566
|
+
child.once("spawn", () => {
|
|
567
|
+
child.unref();
|
|
568
|
+
finish(true);
|
|
569
|
+
});
|
|
570
|
+
} catch {
|
|
571
|
+
finish(false);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
398
574
|
}
|
|
399
575
|
|
|
400
576
|
export async function sleep(milliseconds) {
|