@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.
Files changed (3) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/core.js +203 -27
  3. 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.3";
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 key = String(profile?.api_key || "").trim();
249
- if (!key.startsWith("cvk_")) {
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: profile };
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 profileName = String(payload.workspace_id || "default");
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
- active_profile: profileName,
267
- profiles: {
268
- [profileName]: {
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, profile: profileName };
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
- try {
392
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
393
- child.unref();
394
- return true;
395
- } catch {
396
- return false;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corvio/admin-cli",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "Official read-only Corvio platform administrator CLI.",
5
5
  "type": "module",
6
6
  "bin": {