@uru-intelligence/cli 0.3.56 → 0.3.57

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/README.md +53 -22
  2. package/dist/uru.mjs +1068 -560
  3. package/package.json +11 -1
package/dist/uru.mjs CHANGED
@@ -3,10 +3,70 @@
3
3
 
4
4
  // src/cli.ts
5
5
  import { spawn as spawn3 } from "node:child_process";
6
+ // package.json
7
+ var package_default = {
8
+ name: "@uru-intelligence/cli",
9
+ version: "0.3.57",
10
+ private: false,
11
+ description: "Uru full-platform command line interface",
12
+ repository: {
13
+ type: "git",
14
+ url: "git+https://github.com/Ignition-Forward/uru-workspace-platform.git",
15
+ directory: "apps/cli"
16
+ },
17
+ uru: {
18
+ mcpPackage: "uru-mcp@3.7.2"
19
+ },
20
+ type: "module",
21
+ bin: {
22
+ uru: "dist/uru.mjs"
23
+ },
24
+ exports: {
25
+ ".": "./dist/uru.mjs"
26
+ },
27
+ scripts: {
28
+ lint: "oxlint . --type-aware",
29
+ "lint:fix": "oxlint . --type-aware --fix",
30
+ "type-check": "tsc --noEmit",
31
+ test: "bun test",
32
+ "test:unit": "bun test",
33
+ format: "prettier --write . --ignore-path ../../.prettierignore",
34
+ "format:check": "prettier --check . --ignore-path ../../.prettierignore",
35
+ ci: "bun run lint && bun run type-check && bun run test:unit && bun run docs:check && bun run format:check && bun run build && bun run smoke:package && bun run smoke:binaries",
36
+ build: "node scripts/build.mjs",
37
+ prepack: "bun run build",
38
+ "docs:generate": "bun scripts/generate-docs.mjs",
39
+ "docs:check": "bun scripts/generate-docs.mjs --check",
40
+ "smoke:staging": "node scripts/staging-smoke.mjs",
41
+ "smoke:package": "node scripts/package-smoke.mjs",
42
+ "smoke:mcp-pin": "node scripts/mcp-pin-smoke.mjs",
43
+ "smoke:registry": "node scripts/registry-smoke.mjs",
44
+ "build:binaries": "node scripts/build-binaries.mjs",
45
+ "build:binaries:matrix": "node scripts/build-binaries.mjs --all",
46
+ "smoke:binaries": "node scripts/binary-smoke.mjs"
47
+ },
48
+ devDependencies: {
49
+ "@uru/platform-operations": "workspace:*",
50
+ "@types/node": "24.13.3",
51
+ "bun-types": "1.3.13",
52
+ typescript: "7.0.2"
53
+ },
54
+ packageManager: "bun@1.3.13",
55
+ engines: {
56
+ node: ">=24"
57
+ },
58
+ files: [
59
+ "dist",
60
+ "README.md"
61
+ ],
62
+ publishConfig: {
63
+ access: "public"
64
+ }
65
+ };
6
66
 
7
67
  // src/types.ts
8
68
  var DEFAULT_API_URL = "https://api.uruintelligence.com";
9
- var CLI_VERSION = "0.3.55";
69
+ var CLI_VERSION = package_default.version;
10
70
  var ExitCode = {
11
71
  Ok: 0,
12
72
  Failure: 1,
@@ -92,7 +152,7 @@ function parseArgs(argv) {
92
152
  }
93
153
  if (parsingFlags && arg.startsWith("--")) {
94
154
  const [flagName, inlineValue] = splitFlag(arg);
95
- if (command.length > 0 && flagName === "--output") {
155
+ if (command.length > 0 && (flagName === "--output" || flagName === "--version")) {
96
156
  command.push(arg);
97
157
  continue;
98
158
  }
@@ -193,8 +253,8 @@ function applyValueFlag(flags, flag, value) {
193
253
  }
194
254
  }
195
255
 
196
- // ../../packages/platform-operations/src/operation_groups.ts
197
- function buildOperationGroups(op) {
256
+ // ../../packages/platform-operations/src/operation_groups_gems.ts
257
+ function buildGemOperations(op) {
198
258
  const gemOperations = [
199
259
  op("gem.init", "gems", "Create a Gem project at a visible library/<name>.gem path.", "gem_control_plane", "init", "write"),
200
260
  op("gem.inspect", "gems", "Inspect Gem release readiness, bindings, secrets, and setup checks.", "gem_query", "inspect", "read"),
@@ -203,49 +263,70 @@ function buildOperationGroups(op) {
203
263
  op("gem.fs_write", "gems", "Write a file inside a Gem project directory.", "gem_control_plane", "fs_write", "write"),
204
264
  op("gem.fs_batch_write", "gems", "Write multiple Gem files atomically through the server source layer.", "gem_control_plane", "fs_batch_write", "write"),
205
265
  op("gem.fs_sync", "gems", "Synchronize Gem files and deletions atomically through the server source layer.", "gem_control_plane", "fs_sync", "write"),
206
- op("gem.fs_patch", "gems", "Apply a patch to Gem source files.", "gem_control_plane", "fs_patch", "write"),
266
+ op("gem.fs_patch", "gems", "Apply a complete Codex patch to Gem source files atomically.", "gem_control_plane", "fs_patch", "write"),
267
+ op("gem.fs_patch_preview", "gems", "Preview a complete Codex Gem source patch without mutation.", "gem_query", "fs_patch_preview", "read"),
207
268
  op("gem.fs_mkdir", "gems", "Create a directory inside a Gem project.", "gem_control_plane", "fs_mkdir", "write"),
208
269
  op("gem.fs_rm", "gems", "Remove a file or directory from a Gem project.", "gem_control_plane", "fs_rm", "destructive"),
209
270
  op("gem.fs_mv", "gems", "Move or rename a Gem project file.", "gem_control_plane", "fs_mv", "write"),
210
271
  op("gem.fs_cp", "gems", "Copy a Gem project file.", "gem_control_plane", "fs_cp", "write"),
211
272
  op("gem.fs_status", "gems", "Compare current Gem source against live, last build, or another version.", "gem_query", "fs_status", "read"),
212
273
  op("gem.fs_diff", "gems", "Read a git-like diff for Gem source changes.", "gem_query", "fs_diff", "read"),
213
- op("gem.validate", "gems", "Validate and build-check the current Gem source without publishing.", "gem_control_plane", "validate", "read"),
274
+ op("gem.validate", "gems", "Validate and build-check the current Gem source without creating a Version, Release, or changing any Link.", "gem_control_plane", "validate", "read"),
214
275
  op("gem.build", "gems", "Create or reuse a ready server-side Gem build.", "gem_control_plane", "build", "write"),
215
- op("gem.version_upload", "gems", "Create an immutable Gem version from a ready server-side build.", "gem_control_plane", "version_upload", "write", ["cli", "docs"]),
276
+ op("gem.edits_list", "gems", "List coherent Gem source edits.", "gem_query", "edits_list", "read"),
277
+ op("gem.edit_get", "gems", "Inspect one coherent Gem Edit.", "gem_query", "edit_get", "read"),
278
+ op("gem.versions_list", "gems", "List frozen Gem Versions.", "gem_query", "versions_list", "read"),
279
+ op("gem.version_get", "gems", "Inspect one frozen Gem Version.", "gem_query", "version_get", "read"),
280
+ op("gem.version_diff", "gems", "Compare two frozen Gem Versions.", "gem_query", "version_diff", "read"),
281
+ op("gem.version_save", "gems", "Save the current Draft as a frozen Version without changing Links.", "gem_control_plane", "version_save", "write"),
282
+ op("gem.version_restore", "gems", "Restore one Version into the Draft as a new Edit without changing Links.", "gem_control_plane", "version_restore", "write"),
283
+ op("gem.version_preview", "gems", "Open or provision an isolated preview for one Version without changing Links.", "gem_control_plane", "version_preview", "write"),
216
284
  op("gem.build_logs", "gems", "Read build diagnostics for a Gem build.", "gem_query", "build_logs", "read"),
217
285
  op("gem.build_inspect", "gems", "Inspect a Gem runtime contract and release overview.", "gem_query", "build_inspect", "read"),
218
286
  op("gem.upsert_binding", "gems", "Bind a declared Gem need to an approved platform resource.", "gem_control_plane", "upsert_binding", "write"),
219
287
  op("gem.resolve_safe_bindings", "gems", "Resolve deterministic safe bindings for a ready Gem build.", "gem_control_plane", "resolve_safe_bindings", "write"),
220
288
  op("gem.store_secret", "gems", "Store a user-supplied Gem secret value server-side.", "gem_control_plane", "store_secret", "write"),
221
- op("gem.deploy", "gems", "Deploy a Gem using the server-side build/deploy/publish lifecycle.", "gem_control_plane", "deploy", "write"),
222
- op("gem.version_deploy", "gems", "Deploy an immutable Gem version through the Gem control plane.", "gem_control_plane", "version_deploy", "write", ["cli", "docs"]),
289
+ op("gem.releases_list", "gems", "List logical hosted Releases.", "gem_query", "releases_list", "read"),
290
+ op("gem.release_get", "gems", "Inspect one Release and its attempts.", "gem_query", "release_get", "read"),
291
+ op("gem.release_create", "gems", "Create or reuse a hosted Release for one Version without changing Links.", "gem_control_plane", "release_create", "write"),
292
+ op("gem.release_retry", "gems", "Append a new attempt to a failed logical Release without changing Links.", "gem_control_plane", "release_retry", "write"),
223
293
  op("gem.status", "gems", "Read Gem deployment status, checks, runtime runs, and recent logs.", "gem_query", "deployment_status", "read"),
224
294
  op("gem.checks", "gems", "Inspect registered checks for a Gem deployment or immutable version.", "gem_query", "checks", "read", ["cli", "docs"]),
225
295
  op("gem.logs", "gems", "Read server-side Gem runtime logs with filters.", "gem_query", "logs", "read"),
226
296
  op("gem.browser_verify", "gems", "Run browser verification for a Gem deployment.", "gem_control_plane", "browser_verify", "write"),
227
297
  op("gem.security_scan", "gems", "Run a Gem security scan.", "gem_control_plane", "security_scan", "write"),
228
- op("gem.promote", "gems", "Promote a published Gem link to a deployment.", "gem_control_plane", "promote", "write"),
229
- op("gem.rollback", "gems", "Repoint a published Gem link to a previous immutable deployment.", "gem_control_plane", "rollback", "destructive"),
230
- op("gem.publish", "gems", "Publish or repoint a Gem link to a source version or deployment.", "gem_control_plane", "publish", "write"),
231
- op("gem.unpublish", "gems", "Remove the published primary link for a Gem.", "gem_control_plane", "unpublish", "destructive"),
232
298
  op("gem.links_list", "gems", "List published links for a Gem.", "gem_query", "links_list", "read"),
233
- op("gem.links_create", "gems", "Create an additional published Gem link.", "gem_control_plane", "links_create", "write"),
234
- op("gem.set_access", "gems", "Update access gates for a Gem link.", "gem_control_plane", "set_access", "write"),
299
+ op("gem.link_create", "gems", "Create a Link selecting one explicit Version.", "gem_control_plane", "link_create", "write"),
300
+ op("gem.link_set_version", "gems", "Move one explicit Link to one explicit Version.", "gem_control_plane", "link_set_version", "write"),
301
+ op("gem.link_set_access", "gems", "Update the access policy for one explicit Link.", "gem_control_plane", "link_set_access", "write"),
302
+ op("gem.link_set_notifications", "gems", "Update view notifications for one explicit Link.", "gem_control_plane", "link_set_notifications", "write"),
303
+ op("gem.link_rotate", "gems", "Rotate the URL token for one explicit Link.", "gem_control_plane", "link_rotate", "destructive"),
304
+ op("gem.link_revoke", "gems", "Revoke one explicit Link.", "gem_control_plane", "link_revoke", "destructive"),
305
+ op("gem.runs_list", "gems", "List bounded server Gem executions.", "gem_query", "runs_list", "read"),
306
+ op("gem.run_get", "gems", "Inspect one bounded Gem execution.", "gem_query", "run_get", "read"),
307
+ op("gem.run_cancel", "gems", "Cancel one running Gem execution when supported.", "gem_control_plane", "run_cancel", "destructive"),
308
+ op("gem.run_retry", "gems", "Retry one failed Gem execution when supported.", "gem_control_plane", "run_retry", "write"),
235
309
  op("gem.analytics", "gems", "Read Gem view analytics.", "gem_query", "get_gem_analytics", "read"),
236
310
  op("gem.domains_set", "gems", "Attach or update a custom domain for a Gem.", "gem_control_plane", "domains_set", "write"),
237
311
  op("gem.domains_list", "gems", "List custom domains for a Gem.", "gem_query", "domains_list", "read"),
238
312
  op("gem.components_list", "gems", "List verified Gem component templates.", "GEM_COMPONENTS_LIST", undefined, "read"),
239
313
  op("gem.components_get", "gems", "Fetch one verified Gem component definition.", "GEM_COMPONENTS_GET", undefined, "read")
240
314
  ];
315
+ return gemOperations;
316
+ }
317
+
318
+ // ../../packages/platform-operations/src/operation_groups.ts
319
+ function buildOperationGroups(op) {
320
+ const gemOperations = buildGemOperations(op);
241
321
  const libraryOperations = [
242
322
  op("library.ls", "library", "List visible Library folders and resources.", "library_query", "ls", "read"),
243
323
  op("library.stat", "library", "Read metadata for a visible Library path.", "library_query", "stat", "read"),
244
324
  op("library.read", "library", "Read a visible text-backed Library resource by path.", "library_query", "read", "read"),
245
325
  op("library.search", "library", "Search visible Library resources server-side.", "library_query", "search", "read"),
326
+ op("library.patch_preview", "library", "Preview a Codex patch against visible typed Library text without mutation.", "library_query", "patch_preview", "read"),
246
327
  op("library.mkdir", "library", "Create a Library folder.", "library_fs", "mkdir", "write"),
247
328
  op("library.write", "library", "Create or replace a text-backed Library resource by path.", "library_fs", "write", "write"),
248
- op("library.patch", "library", "Replace text content for a text-backed Library resource by path.", "library_fs", "patch", "write"),
329
+ op("library.patch", "library", "Apply Codex patch text to visible typed Library text resources; use write for full replacement.", "library_fs", "patch", "write"),
249
330
  op("library.cp", "library", "Copy a supported Library item when the backing resource supports copy semantics.", "library_fs", "cp", "write"),
250
331
  op("library.mv", "library", "Move or rename a Library item.", "library_fs", "mv", "write"),
251
332
  op("library.move_workspace", "library", "Move a Library item to another workspace.", "library_fs", "move_workspace", "destructive"),
@@ -773,14 +854,14 @@ function errorDetails(payload) {
773
854
  if (changedPaths.length > 0) {
774
855
  details.push(`Changed paths: ${changedPaths.join(", ")}`);
775
856
  }
776
- const currentSourceVersion = pickString(payload, [
777
- "current_source_version_id",
778
- "currentSourceVersionId",
779
- "source_version_id",
780
- "sourceVersionId"
857
+ const currentSourceRevision = pickString(payload, [
858
+ "current_source_revision_id",
859
+ "currentSourceRevisionId",
860
+ "source_revision_id",
861
+ "sourceRevisionId"
781
862
  ]);
782
- if (currentSourceVersion !== undefined) {
783
- details.push(`Current source version: ${currentSourceVersion}`);
863
+ if (currentSourceRevision !== undefined) {
864
+ details.push(`Current source version: ${currentSourceRevision}`);
784
865
  }
785
866
  const code = pickString(payload, ["code", "errorCode", "error_code"]);
786
867
  if (code !== undefined) {
@@ -999,19 +1080,18 @@ CORE
999
1080
  GEMS
1000
1081
  uru init <library/name.gem> Initialize a Gem project
1001
1082
  uru build [library/name.gem] --dry-run --outdir Build/validate and export build manifest
1002
- uru deploy [library/name.gem] [--prod] [--dry-run] Server-side build/deploy/publish
1003
- uru deploy [library/name.gem] --skip-promote Deploy without promoting the published link
1004
- uru promote <version-id> [library/name.gem] Promote a staged Gem version/deployment
1005
1083
  uru status [library/name.gem] Local source status versus source lock
1006
1084
  uru logs [library/name.gem] [--limit 100] [--follow]
1007
1085
  Runtime logs
1008
- uru open [library/name.gem] Print first published URL
1009
- uru rollback [<version-id>] [library/name.gem] --yes
1010
- Roll back a live Gem link
1086
+ uru open [library/name.gem] --link <id|name> Print one explicit Link URL
1011
1087
  uru gems inspect|validate|build|status [library/name.gem]
1012
- uru gems versions upload [library/name.gem] Create an immutable Gem version
1013
- uru gems versions deploy <version-id> [library/name.gem]
1014
- uru gems checks <version-id> [library/name.gem] [--deployment-id <id>]
1088
+ uru gems edits list|show [library/name.gem]
1089
+ uru gems versions save [library/name.gem] [--name "..."] [--message "..."]
1090
+ uru gems versions list|show|diff|restore|preview [library/name.gem]
1091
+ uru gems releases list|show|create|retry [library/name.gem]
1092
+ uru gems links list|create|set-version|set-access|set-notifications|rotate|revoke
1093
+ uru gems runs list|show|cancel|retry [library/name.gem]
1094
+ uru gems logs [library/name.gem] [--release <id>|--run <id>]
1015
1095
  uru gems read <library/name.gem> <file>
1016
1096
  uru gems write <library/name.gem> <file> --file ./local.html
1017
1097
 
@@ -1163,23 +1243,7 @@ Initialize a Gem project in Library and link the current directory.
1163
1243
  build: `Usage: uru build [library/name.gem] [--dry-run] [--outdir <dir>] [--json]
1164
1244
 
1165
1245
  Validate/build the current Gem through the server-side build pipeline. Use
1166
- --dry-run --outdir to inspect the build manifest without deploying.
1167
- `,
1168
- deploy: `Usage: uru deploy [library/name.gem] [--prod] [--skip-promote] [--no-wait] [--dry-run] [--json]
1169
-
1170
- Build and deploy a Gem through Uru's server-side lifecycle. The backend chooses
1171
- Cloudflare/Fly/fallback from the Gem's compiled runtime contract; the CLI does
1172
- not let users hand-pick providers. Deploy publishes/promotes by default unless
1173
- --skip-promote is supplied. Use --prod --skip-promote for a staged production
1174
- deployment that can be promoted after checks; --prod cannot be combined with
1175
- --no-publish.
1176
- `,
1177
- promote: `Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1178
- uru promote --deployment-id <deployment-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1179
-
1180
- Promote an already-staged immutable Gem version or ready deployment. With
1181
- --link-id, this is a published-link pointer flip after checks pass. If --link-id
1182
- is omitted, Uru promotes through the default clean Gem link.
1246
+ --dry-run --outdir to inspect the build manifest without creating a Version.
1183
1247
  `,
1184
1248
  pull: `Usage: uru pull [library/name.gem] [--force]
1185
1249
 
@@ -1200,43 +1264,63 @@ Upload local Gem source with source-version CAS. Without a matching lock, push
1200
1264
  fails instead of silently overwriting remote edits. Generated directories,
1201
1265
  node_modules, VCS metadata, and local env files are excluded.
1202
1266
  `,
1203
- logs: `Usage: uru logs [library/name.gem] [--limit 100] [--follow]
1267
+ logs: `Usage: uru logs [library/name.gem] [--release <id>|--run <id>] [--limit 100] [--follow]
1204
1268
 
1205
- Print runtime/build logs for the Gem through the control plane. With --follow,
1269
+ Print bounded runtime logs for the Gem, one logical Release, or one Run. With --follow,
1206
1270
  the CLI polls for a bounded 5 minutes; text status goes to stderr and log lines
1207
1271
  go to stdout. Use global --output stream-json or --output-format stream-json
1208
1272
  before the command to emit NDJSON events.
1209
1273
  `,
1210
- open: `Usage: uru open [library/name.gem]
1274
+ open: `Usage: uru open [library/name.gem] [--link <link-id-or-name>]
1211
1275
 
1212
- Print the first available hosted/published Gem URL.
1276
+ Print one explicit active Link URL. When a Gem has more than one active Link,
1277
+ use --link to choose the Link rather than guessing which URL you mean.
1213
1278
  `,
1214
- rollback: `Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes
1215
-
1216
- Rollback retargets a live link to an already-materialized older Gem version or
1217
- deployment after server-side rollback preflight passes. Requires --yes because it
1218
- mutates live traffic. If --link-id is omitted, Uru rolls back through the active
1219
- Primary Gem link.
1220
- `,
1221
- gems: `Usage: uru gems <inspect|validate|build|status|read|write|versions|checks> [...]
1279
+ gems: `Usage: uru gems <inspect|validate|build|status|read|write|edits|versions|releases|links|runs|logs> [...]
1222
1280
 
1223
1281
  Dedicated Gem lifecycle and filesystem commands. These are adapters over the
1224
1282
  same platform operations used by MCP and the web UI.
1225
1283
  `,
1226
- "gems versions": `Usage: uru gems versions upload [library/name.gem]
1227
- uru gems versions deploy <version-id> [library/name.gem] [--pct 100]
1284
+ "gems versions": `Usage: uru gems versions save [library/name.gem] [--name "..."] [--message "..."]
1285
+ uru gems versions list [library/name.gem]
1286
+ uru gems versions show [library/name.gem] <version>
1287
+ uru gems versions diff [library/name.gem] <from-version> <to-version>
1288
+ uru gems versions restore [library/name.gem] <version>
1289
+ uru gems versions preview [library/name.gem] <version>
1290
+
1291
+ Save and inspect frozen Gem checkpoints. Name and message are optional. Saving,
1292
+ restoring, previewing, and comparing Versions do not change any Link.
1293
+ `,
1294
+ "gems edits": `Usage: uru gems edits list [library/name.gem]
1295
+ uru gems edits show [library/name.gem] <edit>
1228
1296
 
1229
- Create immutable Gem versions and deploy them through the Gem control plane.
1230
- Upload accepts --message, --tag/--tags, and --preview-alias. Non-100
1231
- percentages are accepted only with --skip-promote to stage rollout intent; live
1232
- promote/rollback requires 100 until weighted link routing is enabled.
1297
+ Inspect coherent changes in Edit history. Internal source snapshots are available
1298
+ only through diagnostics and are not a normal lifecycle command.
1233
1299
  `,
1234
- "gems checks": `Usage: uru gems checks <version-id> [library/name.gem] [--deployment-id <id>]
1235
- uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]
1300
+ "gems releases": `Usage: uru gems releases list [library/name.gem] [--cursor <cursor>] [--limit <n>]
1301
+ uru gems releases show [library/name.gem] <release>
1302
+ uru gems releases create [library/name.gem] <version>
1303
+ uru gems releases retry [library/name.gem] <release>
1236
1304
 
1237
- Inspect registered publish/deployment checks for an immutable Gem version, or run
1238
- a browser/security check through the Gem control plane. Pass --blocking to make
1239
- the check a release gate; security also accepts --deep.
1305
+ Create and inspect hosted Releases for explicit Versions. A Release never changes
1306
+ a Link. Embedded Gems are link-ready without a Release.
1307
+ `,
1308
+ "gems links": `Usage: uru gems links list [library/name.gem]
1309
+ uru gems links create [library/name.gem] --name <name> --version <version>
1310
+ uru gems links set-version [library/name.gem] <link> <version>
1311
+ uru gems links set-access [library/name.gem] <link> --gate <gate>
1312
+ uru gems links set-notifications [library/name.gem] <link> <mode>
1313
+ uru gems links rotate|revoke [library/name.gem] <link>
1314
+
1315
+ Each Link independently selects one frozen Version. Changing one Link does not
1316
+ move any other Link.
1317
+ `,
1318
+ "gems runs": `Usage: uru gems runs list [library/name.gem] [--cursor <cursor>] [--limit <n>] [--status <status>]
1319
+ uru gems runs show [library/name.gem] <run>
1320
+ uru gems runs cancel|retry [library/name.gem] <run>
1321
+
1322
+ Inspect and control bounded Gem executions. Builds and Release attempts are not
1323
+ Runs. Gems without a bounded execution capability do not have Runs.
1240
1324
  `,
1241
1325
  library: `Usage: uru library ls [path]
1242
1326
  uru library search <query>
@@ -1537,43 +1621,6 @@ function stringField(record, keys) {
1537
1621
  }
1538
1622
  return;
1539
1623
  }
1540
- function firstUrl(value) {
1541
- if (typeof value === "string" && /^https?:\/\//.test(value)) {
1542
- return value;
1543
- }
1544
- if (Array.isArray(value)) {
1545
- for (const item of value) {
1546
- const found = firstUrl(item);
1547
- if (found !== undefined) {
1548
- return found;
1549
- }
1550
- }
1551
- return;
1552
- }
1553
- if (!isJsonObject2(value)) {
1554
- return;
1555
- }
1556
- for (const key of [
1557
- "published_url",
1558
- "publishedUrl",
1559
- "preview_url",
1560
- "previewUrl",
1561
- "url",
1562
- "href"
1563
- ]) {
1564
- const candidate = value[key];
1565
- if (typeof candidate === "string" && /^https?:\/\//.test(candidate)) {
1566
- return candidate;
1567
- }
1568
- }
1569
- for (const candidate of Object.values(value)) {
1570
- const found = firstUrl(candidate);
1571
- if (found !== undefined) {
1572
- return found;
1573
- }
1574
- }
1575
- return;
1576
- }
1577
1624
  function isJsonObject2(value) {
1578
1625
  return value !== null && typeof value === "object" && !Array.isArray(value);
1579
1626
  }
@@ -1934,7 +1981,6 @@ function windowsCredentialNativePowerShell() {
1934
1981
  Add-Type -TypeDefinition @"
1935
1982
  using System;
1936
1983
  using System.Runtime.InteropServices;
1937
- using System.Runtime.InteropServices.ComTypes;
1938
1984
 
1939
1985
  public static class UruCredentialNative
1940
1986
  {
@@ -1945,7 +1991,7 @@ public static class UruCredentialNative
1945
1991
  public UInt32 Type;
1946
1992
  public string TargetName;
1947
1993
  public string Comment;
1948
- public FILETIME LastWritten;
1994
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
1949
1995
  public UInt32 CredentialBlobSize;
1950
1996
  public IntPtr CredentialBlob;
1951
1997
  public UInt32 Persist;
@@ -2059,6 +2105,12 @@ var ERROR_TIPS = {
2059
2105
  oauth_request_failed: "Check the API URL and network access, then retry `uru login`; use `uru login --device` from SSH/headless environments.",
2060
2106
  oauth_device_failed: "Restart `uru login --device` and complete authorization in the browser before the code expires.",
2061
2107
  oauth_device_timeout: "Run `uru login --device` again and complete browser authorization before the timeout.",
2108
+ oauth_refresh_invalid: "Run `uru login` again; the stored renewable login has expired or was revoked.",
2109
+ oauth_refresh_token_missing: "Run `uru login` again; the OAuth server did not issue the renewable credential required by the CLI.",
2110
+ oauth_resource_mismatch: "Check the configured API URL, then restart `uru login`; the OAuth server returned a different protected resource.",
2111
+ oauth_invalid_token_type: "Restart `uru login`; the OAuth server returned an unsupported access-token type.",
2112
+ oauth_logout_revocation_failed: "Local credentials were cleared. Check network access before logging in again if remote revocation must be confirmed.",
2113
+ stored_oauth_credential_invalid: "Run `uru logout`, then `uru login` to replace the unreadable stored OAuth credential.",
2062
2114
  cancelled: "The command was cancelled; rerun it only if you still want the operation to continue."
2063
2115
  };
2064
2116
  function cliErrorJson(error) {
@@ -2130,8 +2182,8 @@ async function readSourceLock(cwd) {
2130
2182
  return {};
2131
2183
  }
2132
2184
  const lock = {};
2133
- if (typeof parsed["sourceVersionId"] === "string") {
2134
- lock.sourceVersionId = parsed["sourceVersionId"];
2185
+ if (typeof parsed["sourceRevisionId"] === "string") {
2186
+ lock.sourceRevisionId = parsed["sourceRevisionId"];
2135
2187
  }
2136
2188
  if (isRecord(parsed["files"])) {
2137
2189
  lock.files = Object.fromEntries(Object.entries(parsed["files"]).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
@@ -2150,7 +2202,7 @@ async function readSourceLock(cwd) {
2150
2202
  async function writeSourceLock(cwd, lock) {
2151
2203
  const path = sourceLockPath(cwd);
2152
2204
  const sanitized = {
2153
- ...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId },
2205
+ ...lock.sourceRevisionId === undefined ? {} : { sourceRevisionId: lock.sourceRevisionId },
2154
2206
  files: stableStringRecord(lock.files ?? {}),
2155
2207
  ...lock.checkedOutAt === undefined ? {} : { checkedOutAt: lock.checkedOutAt }
2156
2208
  };
@@ -2437,11 +2489,9 @@ var BOOLEAN_LOCAL_FLAGS = new Set([
2437
2489
  "--force-build",
2438
2490
  "--hard-delete",
2439
2491
  "--no-create",
2440
- "--no-publish",
2441
2492
  "--no-wait",
2442
2493
  "--permanent",
2443
2494
  "--prod",
2444
- "--skip-promote",
2445
2495
  "--stdin",
2446
2496
  "--value-stdin",
2447
2497
  "--yes"
@@ -2713,6 +2763,19 @@ async function registerOAuthClient(args) {
2713
2763
  scope: optionalStringField(response, "scope") ?? "mcp"
2714
2764
  };
2715
2765
  }
2766
+ async function discoverOAuthResource(args) {
2767
+ const response = await fetchOAuthGet({
2768
+ apiUrl: args.apiUrl,
2769
+ path: "/.well-known/oauth-protected-resource",
2770
+ ...args.fetch === undefined ? {} : { fetch: args.fetch }
2771
+ });
2772
+ if (!isRecord(response)) {
2773
+ throw new CliError("Unexpected OAuth protected-resource metadata response.");
2774
+ }
2775
+ const resource = stringField2(response, "resource");
2776
+ assertHttpUrl(resource, "OAuth protected resource");
2777
+ return resource;
2778
+ }
2716
2779
  async function exchangeAuthorizationCode(args) {
2717
2780
  const body = new URLSearchParams;
2718
2781
  body.set("grant_type", "authorization_code");
@@ -2731,6 +2794,33 @@ async function exchangeAuthorizationCode(args) {
2731
2794
  });
2732
2795
  return normalizeOAuthTokenResponse(response);
2733
2796
  }
2797
+ async function refreshOAuthToken(args) {
2798
+ const body = new URLSearchParams;
2799
+ body.set("grant_type", "refresh_token");
2800
+ body.set("client_id", args.clientId);
2801
+ body.set("refresh_token", args.refreshToken);
2802
+ body.set("resource", args.resource);
2803
+ const response = await fetchOAuthForm({
2804
+ apiUrl: args.apiUrl,
2805
+ path: "/oauth/token",
2806
+ body,
2807
+ rejectedGrantErrorCode: "oauth_refresh_invalid",
2808
+ ...args.fetch === undefined ? {} : { fetch: args.fetch }
2809
+ });
2810
+ return normalizeOAuthTokenResponse(response);
2811
+ }
2812
+ async function revokeOAuthToken(args) {
2813
+ const body = new URLSearchParams;
2814
+ body.set("token", args.token);
2815
+ body.set("token_type_hint", "refresh_token");
2816
+ body.set("client_id", args.clientId);
2817
+ await fetchOAuthForm({
2818
+ apiUrl: args.apiUrl,
2819
+ path: "/oauth/revoke",
2820
+ body,
2821
+ ...args.fetch === undefined ? {} : { fetch: args.fetch }
2822
+ });
2823
+ }
2734
2824
  async function exchangeOAuthForCliSession(args) {
2735
2825
  const body = {};
2736
2826
  if (args.workspaceId !== undefined && args.workspaceId.trim() !== "") {
@@ -2755,6 +2845,7 @@ async function exchangeOAuthForCliSession(args) {
2755
2845
  accessToken: stringField2(response, "access_token"),
2756
2846
  tokenType,
2757
2847
  expiresIn: numberField(response, "expires_in"),
2848
+ oauthResource: stringField2(response, "oauth_resource"),
2758
2849
  ...workspaceId === undefined ? {} : { workspaceId }
2759
2850
  };
2760
2851
  }
@@ -2832,6 +2923,18 @@ async function fetchOAuthJson(args) {
2832
2923
  }
2833
2924
  return payload;
2834
2925
  }
2926
+ async function fetchOAuthGet(args) {
2927
+ const fetchImpl = args.fetch ?? fetch;
2928
+ const response = await fetchImpl(`${normalizeApiUrl(args.apiUrl)}${args.path}`, {
2929
+ method: "GET",
2930
+ headers: { Accept: "application/json" }
2931
+ });
2932
+ const payload = parseJsonResponse(await response.text());
2933
+ if (!response.ok) {
2934
+ throw oauthHttpError(response.status, payload);
2935
+ }
2936
+ return payload;
2937
+ }
2835
2938
  function tokenUrlSafe(byteLength) {
2836
2939
  return randomBytes(byteLength).toString("base64url");
2837
2940
  }
@@ -2850,10 +2953,7 @@ async function fetchOAuthForm(args) {
2850
2953
  });
2851
2954
  const payload = parseJsonResponse(await response.text());
2852
2955
  if (!response.ok && !(args.allowOAuthError === true && isOAuthError(payload))) {
2853
- throw new CliError(errorMessage2(payload) ?? `OAuth request failed: HTTP ${response.status}`, {
2854
- code: response.status === 401 || response.status === 403 ? ExitCode.AuthRequired : ExitCode.Failure,
2855
- errorCode: "oauth_request_failed"
2856
- });
2956
+ throw oauthHttpError(response.status, payload, args.rejectedGrantErrorCode);
2857
2957
  }
2858
2958
  return payload;
2859
2959
  }
@@ -2908,14 +3008,47 @@ function normalizeOAuthTokenResponse(value) {
2908
3008
  const refreshToken = optionalStringField(value, "refresh_token");
2909
3009
  const scope = optionalStringField(value, "scope");
2910
3010
  const expiresInRaw = value["expires_in"];
3011
+ const tokenType = stringField2(value, "token_type");
3012
+ if (tokenType !== "Bearer") {
3013
+ throw new CliError("OAuth token response used an unsupported token type.", {
3014
+ code: ExitCode.AuthRequired,
3015
+ errorCode: "oauth_invalid_token_type"
3016
+ });
3017
+ }
2911
3018
  return {
2912
3019
  accessToken: stringField2(value, "access_token"),
2913
- tokenType: stringField2(value, "token_type"),
3020
+ tokenType,
2914
3021
  ...typeof expiresInRaw === "number" ? { expiresIn: expiresInRaw } : {},
2915
3022
  ...refreshToken === undefined ? {} : { refreshToken },
2916
3023
  ...scope === undefined ? {} : { scope }
2917
3024
  };
2918
3025
  }
3026
+ function oauthHttpError(status, payload, rejectedGrantErrorCode) {
3027
+ const oauthError = isRecord(payload) && typeof payload["error"] === "string" ? payload["error"] : null;
3028
+ const authRequired = status === 400 || status === 401 || status === 403 || status === 409;
3029
+ const grantRejected = rejectedGrantErrorCode !== undefined && (oauthError === "invalid_grant" || oauthError === "invalid_target" || oauthError === "invalid_client");
3030
+ return new CliError(errorMessage2(payload) ?? `OAuth request failed: HTTP ${status}`, {
3031
+ code: authRequired ? ExitCode.AuthRequired : ExitCode.Failure,
3032
+ errorCode: grantRejected ? rejectedGrantErrorCode : "oauth_request_failed"
3033
+ });
3034
+ }
3035
+ function assertHttpUrl(value, label) {
3036
+ let url;
3037
+ try {
3038
+ url = new URL(value);
3039
+ } catch (error) {
3040
+ throw new CliError(`${label} is not a valid URL.`, {
3041
+ errorCode: "oauth_invalid_response",
3042
+ cause: error
3043
+ });
3044
+ }
3045
+ const loopbackHostname = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
3046
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopbackHostname)) {
3047
+ throw new CliError(`${label} must use HTTPS except on loopback.`, {
3048
+ errorCode: "oauth_invalid_response"
3049
+ });
3050
+ }
3051
+ }
2919
3052
  function stringField2(value, key) {
2920
3053
  const field = value[key];
2921
3054
  if (typeof field !== "string" || field.trim() === "") {
@@ -2952,12 +3085,203 @@ function defaultSleep(milliseconds) {
2952
3085
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
2953
3086
  }
2954
3087
 
3088
+ // src/oauth-credential.ts
3089
+ var OAUTH_CREDENTIAL_PREFIX = "uru_cli_oauth_v1:";
3090
+ var SESSION_REFRESH_LEEWAY_MS = 5 * 60 * 1000;
3091
+ function createStoredCliOAuthCredential(args) {
3092
+ const nowMs = args.nowMs ?? Date.now();
3093
+ if (!Number.isFinite(args.sessionExpiresIn) || args.sessionExpiresIn <= 0) {
3094
+ throw invalidStoredCredential("CLI session lifetime is invalid.");
3095
+ }
3096
+ return {
3097
+ version: 1,
3098
+ sessionAccessToken: requiredSecret(args.sessionAccessToken, "CLI session access token"),
3099
+ sessionExpiresAt: nowMs + args.sessionExpiresIn * 1000,
3100
+ oauthRefreshToken: requiredSecret(args.oauthRefreshToken, "OAuth refresh token"),
3101
+ oauthClientId: requiredValue(args.oauthClientId, "OAuth client id"),
3102
+ oauthResource: requiredHttpsResource(args.oauthResource),
3103
+ ...args.workspaceId === undefined || args.workspaceId.trim() === "" ? {} : { workspaceId: args.workspaceId.trim() }
3104
+ };
3105
+ }
3106
+ function serializeStoredCliOAuthCredential(credential) {
3107
+ return `${OAUTH_CREDENTIAL_PREFIX}${Buffer.from(JSON.stringify(credential), "utf8").toString("base64url")}`;
3108
+ }
3109
+ function parseStoredCliOAuthCredential(storedValue) {
3110
+ if (!storedValue.startsWith(OAUTH_CREDENTIAL_PREFIX)) {
3111
+ return null;
3112
+ }
3113
+ let parsed;
3114
+ try {
3115
+ const encoded = storedValue.slice(OAUTH_CREDENTIAL_PREFIX.length);
3116
+ parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
3117
+ } catch (error) {
3118
+ throw invalidStoredCredential("Stored CLI OAuth credential is unreadable.", error);
3119
+ }
3120
+ if (!isRecord(parsed) || parsed["version"] !== 1) {
3121
+ throw invalidStoredCredential("Stored CLI OAuth credential version is invalid.");
3122
+ }
3123
+ const sessionAccessToken = recordString(parsed, "sessionAccessToken");
3124
+ const oauthRefreshToken = recordString(parsed, "oauthRefreshToken");
3125
+ const oauthClientId = recordString(parsed, "oauthClientId");
3126
+ const oauthResource = recordString(parsed, "oauthResource");
3127
+ const sessionExpiresAt = parsed["sessionExpiresAt"];
3128
+ const workspaceId = optionalRecordString(parsed, "workspaceId");
3129
+ if (sessionAccessToken === null || oauthRefreshToken === null || oauthClientId === null || oauthResource === null || typeof sessionExpiresAt !== "number" || !Number.isFinite(sessionExpiresAt) || sessionExpiresAt <= 0) {
3130
+ throw invalidStoredCredential("Stored CLI OAuth credential is incomplete.");
3131
+ }
3132
+ return {
3133
+ version: 1,
3134
+ sessionAccessToken,
3135
+ sessionExpiresAt,
3136
+ oauthRefreshToken,
3137
+ oauthClientId,
3138
+ oauthResource: requiredHttpsResource(oauthResource),
3139
+ ...workspaceId === null ? {} : { workspaceId }
3140
+ };
3141
+ }
3142
+ function cliOAuthCredentialNeedsRefresh(credential, workspaceId, nowMs = Date.now()) {
3143
+ const requestedWorkspace = workspaceId?.trim();
3144
+ if (requestedWorkspace !== undefined && requestedWorkspace !== "" && requestedWorkspace !== credential.workspaceId) {
3145
+ return true;
3146
+ }
3147
+ return credential.sessionExpiresAt <= nowMs + SESSION_REFRESH_LEEWAY_MS;
3148
+ }
3149
+ function recordString(value, key) {
3150
+ const item = value[key];
3151
+ return typeof item === "string" && item.trim() !== "" ? item : null;
3152
+ }
3153
+ function optionalRecordString(value, key) {
3154
+ const item = value[key];
3155
+ return typeof item === "string" && item.trim() !== "" ? item.trim() : null;
3156
+ }
3157
+ function requiredSecret(value, label) {
3158
+ return requiredValue(value, label);
3159
+ }
3160
+ function requiredValue(value, label) {
3161
+ const normalized = value.trim();
3162
+ if (normalized === "") {
3163
+ throw invalidStoredCredential(`${label} is missing.`);
3164
+ }
3165
+ return normalized;
3166
+ }
3167
+ function requiredHttpsResource(value) {
3168
+ const normalized = requiredValue(value, "OAuth resource");
3169
+ let resource;
3170
+ try {
3171
+ resource = new URL(normalized);
3172
+ } catch (error) {
3173
+ throw invalidStoredCredential("OAuth resource is not a valid URL.", error);
3174
+ }
3175
+ const loopbackHostname = resource.hostname === "localhost" || resource.hostname === "127.0.0.1" || resource.hostname === "[::1]";
3176
+ if (resource.protocol !== "https:" && !(resource.protocol === "http:" && loopbackHostname)) {
3177
+ throw invalidStoredCredential("OAuth resource must use HTTPS.");
3178
+ }
3179
+ return normalized;
3180
+ }
3181
+ function invalidStoredCredential(message, cause) {
3182
+ return new CliError(message, {
3183
+ code: ExitCode.AuthRequired,
3184
+ errorCode: "stored_oauth_credential_invalid",
3185
+ ...cause === undefined ? {} : { cause }
3186
+ });
3187
+ }
3188
+
2955
3189
  // src/auth-commands.ts
2956
3190
  var OAUTH_CALLBACK_PATH = "/oauth/callback";
2957
3191
  var OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
2958
3192
  var DEVICE_MAX_POLLS = 120;
2959
- async function resolveStoredToken(credentialStore, apiUrl, config) {
2960
- return await credentialStore.readToken(apiUrl) ?? (config.insecureStorage === true ? config.token : undefined);
3193
+ async function resolveStoredToken(options) {
3194
+ const keychainValue = await options.credentialStore.readToken(options.apiUrl);
3195
+ const source = keychainValue === undefined ? "config" : "keychain";
3196
+ const storedValue = keychainValue ?? (options.config.insecureStorage === true ? options.config.token : undefined);
3197
+ if (storedValue === undefined) {
3198
+ return;
3199
+ }
3200
+ let credential;
3201
+ try {
3202
+ credential = parseStoredCliOAuthCredential(storedValue);
3203
+ } catch (error) {
3204
+ if (options.recoverInvalidCredential === true) {
3205
+ return;
3206
+ }
3207
+ throw error;
3208
+ }
3209
+ if (credential === null) {
3210
+ return storedValue;
3211
+ }
3212
+ if (!cliOAuthCredentialNeedsRefresh(credential, options.workspaceId)) {
3213
+ return credential.sessionAccessToken;
3214
+ }
3215
+ try {
3216
+ const refreshed = await refreshOAuthToken({
3217
+ apiUrl: options.apiUrl,
3218
+ clientId: credential.oauthClientId,
3219
+ refreshToken: credential.oauthRefreshToken,
3220
+ resource: credential.oauthResource,
3221
+ ...options.fetch === undefined ? {} : { fetch: options.fetch }
3222
+ });
3223
+ const rotatedRefreshToken = requireRefreshToken(refreshed);
3224
+ await persistResolvedCredential(options, source, serializeStoredCliOAuthCredential({
3225
+ ...credential,
3226
+ oauthRefreshToken: rotatedRefreshToken
3227
+ }));
3228
+ const session = await exchangeOAuthForCliSession({
3229
+ apiUrl: options.apiUrl,
3230
+ oauthAccessToken: refreshed.accessToken,
3231
+ ...options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId },
3232
+ ...options.fetch === undefined ? {} : { fetch: options.fetch }
3233
+ });
3234
+ if (session.oauthResource !== credential.oauthResource) {
3235
+ throw new CliError("Refreshed CLI session OAuth resource did not match the stored credential.", {
3236
+ code: ExitCode.AuthRequired,
3237
+ errorCode: "oauth_resource_mismatch"
3238
+ });
3239
+ }
3240
+ const nextCredential = createStoredCliOAuthCredential({
3241
+ sessionAccessToken: session.accessToken,
3242
+ sessionExpiresIn: session.expiresIn,
3243
+ oauthRefreshToken: rotatedRefreshToken,
3244
+ oauthClientId: credential.oauthClientId,
3245
+ oauthResource: session.oauthResource,
3246
+ ...session.workspaceId === undefined ? {} : { workspaceId: session.workspaceId }
3247
+ });
3248
+ await persistResolvedCredential(options, source, serializeStoredCliOAuthCredential(nextCredential));
3249
+ return nextCredential.sessionAccessToken;
3250
+ } catch (error) {
3251
+ if (!isStoredRefreshRejected(error)) {
3252
+ throw error;
3253
+ }
3254
+ await clearResolvedCredential(options, source);
3255
+ return;
3256
+ }
3257
+ }
3258
+ async function revokeStoredOAuthLogin(args) {
3259
+ const storedValue = await args.credentialStore.readToken(args.apiUrl) ?? (args.config.insecureStorage === true ? args.config.token : undefined);
3260
+ if (storedValue === undefined) {
3261
+ return;
3262
+ }
3263
+ const credential = parseStoredCliOAuthCredential(storedValue);
3264
+ if (credential === null) {
3265
+ return;
3266
+ }
3267
+ const sessionClient = new UruApiClient({
3268
+ apiUrl: args.apiUrl,
3269
+ token: credential.sessionAccessToken,
3270
+ ...args.fetch === undefined ? {} : { fetch: args.fetch }
3271
+ });
3272
+ const revocations = await Promise.allSettled([
3273
+ sessionClient.api("/api/auth/logout", { method: "POST" }),
3274
+ revokeOAuthToken({
3275
+ apiUrl: args.apiUrl,
3276
+ clientId: credential.oauthClientId,
3277
+ token: credential.oauthRefreshToken,
3278
+ ...args.fetch === undefined ? {} : { fetch: args.fetch }
3279
+ })
3280
+ ]);
3281
+ const rejected = revocations.find((result) => result.status === "rejected");
3282
+ if (rejected !== undefined) {
3283
+ throw rejected.reason;
3284
+ }
2961
3285
  }
2962
3286
  async function resolveFlagToken(flags, stdin) {
2963
3287
  if (flags.token !== undefined) {
@@ -2991,20 +3315,26 @@ async function resolveFlagToken(flags, stdin) {
2991
3315
  }
2992
3316
  async function login(ctx, options = {}) {
2993
3317
  const existingToken = ctx.token === undefined || ctx.token.trim() === "" ? undefined : ctx.token;
2994
- let token = existingToken ?? await obtainLoginSessionToken(ctx);
3318
+ const existingStoredValue = existingToken !== undefined && !ctx.tokenIsExplicit ? await readStoredLoginValue(ctx) : undefined;
3319
+ let usedExistingToken = existingToken !== undefined;
3320
+ let loginToken = existingToken === undefined ? await obtainLoginSessionToken(ctx) : {
3321
+ accessToken: existingToken,
3322
+ storedValue: existingStoredValue ?? existingToken
3323
+ };
2995
3324
  let result;
2996
3325
  try {
2997
- result = await validateSessionToken(ctx, token);
3326
+ result = await validateSessionToken(ctx, loginToken.accessToken);
2998
3327
  } catch (error) {
2999
3328
  const storedTokenIsStale = existingToken !== undefined && !ctx.tokenIsExplicit && isAuthRequiredError(error);
3000
3329
  if (!storedTokenIsStale) {
3001
3330
  throw error;
3002
3331
  }
3003
- token = await obtainLoginSessionToken(ctx);
3004
- result = await validateSessionToken(ctx, token);
3332
+ loginToken = await obtainLoginSessionToken(ctx);
3333
+ usedExistingToken = false;
3334
+ result = await validateSessionToken(ctx, loginToken.accessToken);
3005
3335
  }
3006
3336
  const selectedWorkspace = selectWorkspace(result, ctx.workspace);
3007
- await persistLoginToken(ctx, token, selectedWorkspace);
3337
+ await persistLoginToken(ctx, loginToken.storedValue, selectedWorkspace);
3008
3338
  if (options.silentSuccess === true) {
3009
3339
  return;
3010
3340
  }
@@ -3017,12 +3347,15 @@ async function login(ctx, options = {}) {
3017
3347
  workspaceId: selectedWorkspace,
3018
3348
  workspaceSelectionRequired: result.workspaceSelectionRequired,
3019
3349
  storage: ctx.insecureStorage ? "insecure_file" : "keychain",
3020
- auth: ctx.token === token ? "token" : ctx.device ? "device_oauth" : "browser_oauth"
3350
+ auth: usedExistingToken ? "token" : ctx.device ? "device_oauth" : "browser_oauth"
3021
3351
  });
3022
3352
  } else {
3023
3353
  writeText(ctx.io, ctx.insecureStorage ? "Logged in (token stored in local config)" : "Logged in");
3024
3354
  }
3025
3355
  }
3356
+ async function readStoredLoginValue(ctx) {
3357
+ return await ctx.credentialStore.readToken(ctx.apiUrl) ?? (ctx.config.insecureStorage === true ? ctx.config.token : undefined);
3358
+ }
3026
3359
  async function validateSessionToken(ctx, token) {
3027
3360
  const loginClient = new UruApiClient({
3028
3361
  apiUrl: ctx.apiUrl,
@@ -3035,6 +3368,9 @@ async function validateSessionToken(ctx, token) {
3035
3368
  function isAuthRequiredError(error) {
3036
3369
  return error instanceof CliError && error.exitCode === ExitCode.AuthRequired;
3037
3370
  }
3371
+ function isStoredRefreshRejected(error) {
3372
+ return error instanceof CliError && error.errorCode === "oauth_refresh_invalid";
3373
+ }
3038
3374
  async function readAllStdin(input) {
3039
3375
  const chunks = [];
3040
3376
  for await (const chunk of input) {
@@ -3052,6 +3388,10 @@ async function obtainLoginSessionToken(ctx) {
3052
3388
  return loginWithBrowser(ctx);
3053
3389
  }
3054
3390
  async function loginWithDeviceCode(ctx) {
3391
+ const resource = await discoverOAuthResource({
3392
+ apiUrl: ctx.apiUrl,
3393
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
3394
+ });
3055
3395
  const clientRegistration = await registerOAuthClient({
3056
3396
  apiUrl: ctx.apiUrl,
3057
3397
  redirectUri: buildLoopbackRedirectUri({
@@ -3070,6 +3410,7 @@ async function loginWithDeviceCode(ctx) {
3070
3410
  apiUrl: ctx.apiUrl,
3071
3411
  clientId: clientRegistration.clientId,
3072
3412
  scope: clientRegistration.scope,
3413
+ resource,
3073
3414
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3074
3415
  });
3075
3416
  const complete = authorization.verificationUriComplete ?? authorization.verificationUri;
@@ -3086,7 +3427,7 @@ async function loginWithDeviceCode(ctx) {
3086
3427
  maxPolls: Math.min(DEVICE_MAX_POLLS, Math.ceil(authorization.expiresIn / Math.max(1, authorization.interval))),
3087
3428
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3088
3429
  });
3089
- return exchangeOAuthTokenForSession(ctx, token);
3430
+ return exchangeOAuthTokenForSession(ctx, token, clientRegistration.clientId, resource);
3090
3431
  }
3091
3432
  async function loginWithBrowser(ctx) {
3092
3433
  const callbackHost = ctx.callbackHost;
@@ -3095,6 +3436,10 @@ async function loginWithBrowser(ctx) {
3095
3436
  ...ctx.callbackPort === undefined ? {} : { port: ctx.callbackPort }
3096
3437
  });
3097
3438
  try {
3439
+ const resource = await discoverOAuthResource({
3440
+ apiUrl: ctx.apiUrl,
3441
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
3442
+ });
3098
3443
  const redirectUri = buildLoopbackRedirectUri({
3099
3444
  host: callbackHost,
3100
3445
  port: callback.port,
@@ -3113,7 +3458,8 @@ async function loginWithBrowser(ctx) {
3113
3458
  redirectUri,
3114
3459
  scope: clientRegistration.scope,
3115
3460
  state,
3116
- codeChallenge: pkce.challenge
3461
+ codeChallenge: pkce.challenge,
3462
+ resource
3117
3463
  });
3118
3464
  await maybeCopyLoginUrl(ctx, authorizationUrl);
3119
3465
  if (ctx.noOpen) {
@@ -3132,21 +3478,40 @@ async function loginWithBrowser(ctx) {
3132
3478
  code: authorization.code,
3133
3479
  redirectUri,
3134
3480
  codeVerifier: pkce.verifier,
3481
+ resource,
3135
3482
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3136
3483
  });
3137
- return exchangeOAuthTokenForSession(ctx, token);
3484
+ return exchangeOAuthTokenForSession(ctx, token, clientRegistration.clientId, resource);
3138
3485
  } finally {
3139
3486
  callback.close();
3140
3487
  }
3141
3488
  }
3142
- async function exchangeOAuthTokenForSession(ctx, token) {
3489
+ async function exchangeOAuthTokenForSession(ctx, token, clientId, resource) {
3490
+ const refreshToken = requireRefreshToken(token);
3143
3491
  const session = await exchangeOAuthForCliSession({
3144
3492
  apiUrl: ctx.apiUrl,
3145
3493
  oauthAccessToken: token.accessToken,
3146
3494
  ...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
3147
3495
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3148
3496
  });
3149
- return session.accessToken;
3497
+ if (session.oauthResource !== resource) {
3498
+ throw new CliError("CLI session OAuth resource did not match discovery.", {
3499
+ code: ExitCode.AuthRequired,
3500
+ errorCode: "oauth_resource_mismatch"
3501
+ });
3502
+ }
3503
+ const credential = createStoredCliOAuthCredential({
3504
+ sessionAccessToken: session.accessToken,
3505
+ sessionExpiresIn: session.expiresIn,
3506
+ oauthRefreshToken: refreshToken,
3507
+ oauthClientId: clientId,
3508
+ oauthResource: session.oauthResource,
3509
+ ...session.workspaceId === undefined ? {} : { workspaceId: session.workspaceId }
3510
+ });
3511
+ return {
3512
+ accessToken: session.accessToken,
3513
+ storedValue: serializeStoredCliOAuthCredential(credential)
3514
+ };
3150
3515
  }
3151
3516
  async function persistLoginToken(ctx, token, selectedWorkspace) {
3152
3517
  if (ctx.insecureStorage) {
@@ -3173,6 +3538,40 @@ async function persistLoginToken(ctx, token, selectedWorkspace) {
3173
3538
  };
3174
3539
  await ctx.store.write(nextConfig);
3175
3540
  }
3541
+ function requireRefreshToken(token) {
3542
+ const refreshToken = token.refreshToken?.trim();
3543
+ if (refreshToken === undefined || refreshToken === "") {
3544
+ throw new CliError("OAuth server did not issue a refresh token for the CLI.", {
3545
+ code: ExitCode.AuthRequired,
3546
+ errorCode: "oauth_refresh_token_missing"
3547
+ });
3548
+ }
3549
+ return refreshToken;
3550
+ }
3551
+ async function persistResolvedCredential(options, source, storedValue) {
3552
+ if (source === "keychain") {
3553
+ await options.credentialStore.writeToken(options.apiUrl, storedValue);
3554
+ return;
3555
+ }
3556
+ await options.store.write({
3557
+ ...options.config,
3558
+ apiUrl: options.apiUrl,
3559
+ token: storedValue,
3560
+ insecureStorage: true
3561
+ });
3562
+ }
3563
+ async function clearResolvedCredential(options, source) {
3564
+ if (source === "keychain") {
3565
+ await options.credentialStore.clearToken(options.apiUrl);
3566
+ return;
3567
+ }
3568
+ const {
3569
+ token: _token,
3570
+ insecureStorage: _insecureStorage,
3571
+ ...safeConfig
3572
+ } = options.config;
3573
+ await options.store.write(safeConfig);
3574
+ }
3176
3575
  async function maybeCopyLoginUrl(ctx, url) {
3177
3576
  if (!ctx.clipboard) {
3178
3577
  return;
@@ -3276,6 +3675,7 @@ async function switchWorkspace(ctx, workspaceId) {
3276
3675
  // src/local-commands.ts
3277
3676
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3278
3677
  import { dirname as dirname3, resolve as resolve2 } from "node:path";
3678
+ var DEFAULT_MCP_PACKAGE = package_default.uru.mcpPackage;
3279
3679
  var GENERIC_FAMILY_COMMANDS = {
3280
3680
  workspace: "workspace",
3281
3681
  sharing: "sharing",
@@ -3304,15 +3704,12 @@ var TOP_LEVEL_COMMANDS = [
3304
3704
  "tools",
3305
3705
  "init",
3306
3706
  "build",
3307
- "deploy",
3308
- "promote",
3309
3707
  "pull",
3310
3708
  "status",
3311
3709
  "diff",
3312
3710
  "push",
3313
3711
  "logs",
3314
3712
  "open",
3315
- "rollback",
3316
3713
  "gems",
3317
3714
  "automations",
3318
3715
  "library",
@@ -3405,7 +3802,7 @@ async function dispatchMcp(ctx, sub, args) {
3405
3802
  const config = mcpServerConfig({
3406
3803
  apiKey: flags.values["--api-key"] ?? ctx.token,
3407
3804
  serverName: flags.values["--server-name"] ?? "uru",
3408
- packageName: flags.values["--package"] ?? "uru-mcp@latest"
3805
+ packageName: flags.values["--package"] ?? DEFAULT_MCP_PACKAGE
3409
3806
  });
3410
3807
  if (sub === "config") {
3411
3808
  writeJson(ctx.io, config);
@@ -3769,7 +4166,7 @@ function mcpServerConfig(args) {
3769
4166
  mcpServers: {
3770
4167
  [args.serverName]: {
3771
4168
  command: "npx",
3772
- args: [args.packageName],
4169
+ args: ["-y", args.packageName],
3773
4170
  env: { URU_API_KEY: apiKey }
3774
4171
  }
3775
4172
  }
@@ -3818,129 +4215,414 @@ function levenshtein(left, right) {
3818
4215
  import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3819
4216
  import { setTimeout as sleep } from "node:timers/promises";
3820
4217
  import { join as join3, resolve as resolve3 } from "node:path";
3821
- async function dispatchGems(ctx, sub, args, linkedProject) {
3822
- const flags = parseLocalFlags(args);
3823
- const gemPath = () => gemPathFromArgs(flags, linkedProject);
3824
- if (sub === "inspect") {
3825
- await emitGemTool(ctx, "gem.inspect", { path: gemPath() });
3826
- return;
4218
+
4219
+ // src/gem-lifecycle-resource-commands.ts
4220
+ async function dispatchGemLifecycleResourceCommand(ctx, sub, args, linkedProject) {
4221
+ if (sub === "versions") {
4222
+ await dispatchGemVersions(ctx, args, linkedProject);
4223
+ return true;
3827
4224
  }
3828
- if (sub === "validate") {
3829
- await emitGemTool(ctx, "gem.validate", { path: gemPath() });
3830
- return;
4225
+ if (sub === "edits") {
4226
+ await dispatchGemEdits(ctx, args, linkedProject);
4227
+ return true;
3831
4228
  }
3832
- if (sub === "build") {
3833
- await gemBuild(ctx, args, linkedProject);
3834
- return;
4229
+ if (sub === "releases") {
4230
+ await dispatchGemReleases(ctx, args, linkedProject);
4231
+ return true;
3835
4232
  }
3836
- if (sub === "versions") {
3837
- await dispatchGemVersions(ctx, args, linkedProject);
3838
- return;
4233
+ if (sub === "links") {
4234
+ await dispatchGemLinks(ctx, args, linkedProject);
4235
+ return true;
3839
4236
  }
3840
- if (sub === "checks") {
3841
- await gemChecks(ctx, args, linkedProject);
4237
+ if (sub === "runs") {
4238
+ await dispatchGemRuns(ctx, args, linkedProject);
4239
+ return true;
4240
+ }
4241
+ return false;
4242
+ }
4243
+ async function dispatchGemVersions(ctx, args, linkedProject) {
4244
+ const [verb, ...rest] = args;
4245
+ if (verb === "save") {
4246
+ await gemVersionSave(ctx, rest, linkedProject);
3842
4247
  return;
3843
4248
  }
3844
- if (sub === "status") {
3845
- await emitGemTool(ctx, "gem.status", { path: gemPath() });
4249
+ if (verb === "list") {
4250
+ const flags = parseLocalFlags(rest);
4251
+ const params = {
4252
+ path: gemPathFromArgs(flags, linkedProject)
4253
+ };
4254
+ copyStringFlag(flags, params, "--cursor", "cursor");
4255
+ copyOptionalIntegerFlag(flags, params, "--limit", "limit");
4256
+ await emitGemTool(ctx, "gem.versions_list", params);
3846
4257
  return;
3847
4258
  }
3848
- if (sub === "read") {
3849
- const [path, file] = args;
3850
- await emitGemTool(ctx, "gem.fs_read", {
3851
- path: gemFilePath(requireCommandValue(path, "Usage: uru gems read <gem-path> <file>"), requireCommandValue(file, "Usage: uru gems read <gem-path> <file>"))
3852
- });
4259
+ if (verb === "show" || verb === "restore" || verb === "preview") {
4260
+ const flags = parseLocalFlags(rest);
4261
+ const version = lifecycleResourceArg(flags, `Usage: uru gems versions ${verb} <gem> <version>`);
4262
+ const path = gemPathFromArgs(flags, linkedProject);
4263
+ await emitGemTool(ctx, verb === "show" ? "gem.version_get" : verb === "restore" ? "gem.version_restore" : "gem.version_preview", { path, version_id: version }, verb === "restore" ? { textExtractor: extractVersionRestoreSummary } : undefined);
3853
4264
  return;
3854
4265
  }
3855
- if (sub === "write") {
3856
- await gemWrite(ctx, args);
4266
+ if (verb === "diff") {
4267
+ const flags = parseLocalFlags(rest);
4268
+ const [fromVersion, toVersion] = lifecycleResourceArgs(flags, 2, "Usage: uru gems versions diff <gem> <from-version> <to-version>");
4269
+ await emitGemTool(ctx, "gem.version_diff", {
4270
+ path: gemPathFromArgs(flags, linkedProject),
4271
+ from_version_id: requireCommandValue(fromVersion, "Missing from-version"),
4272
+ to_version_id: requireCommandValue(toVersion, "Missing to-version")
4273
+ });
3857
4274
  return;
3858
4275
  }
3859
- throw new CliError("Usage: uru gems inspect|validate|build|versions|checks|status|read|write");
4276
+ throw new CliError("Usage: uru gems versions list <gem> | save <gem> [--name ...] [--message ...] | show <gem> <version> | diff <gem> <from> <to> | restore <gem> <version> | preview <gem> <version>");
3860
4277
  }
3861
- async function gemInit(ctx, args) {
4278
+ async function gemVersionSave(ctx, args, linkedProject) {
3862
4279
  const flags = parseLocalFlags(args);
3863
- const path = requireCommandValue(flags.positionals[0], "Usage: uru init <library/name.gem> [--entrypoint main.html] [--file ./main.html]");
3864
- const params = { path };
4280
+ assertAllowedLocalFlags(flags, ["--gem", "--idempotency-key", "--message", "--name", "--path"], "uru gems versions save");
4281
+ const gemPath = gemPathFromArgs(flags, linkedProject);
4282
+ const params = { path: gemPath };
3865
4283
  copyStringFlag(flags, params, "--name", "name");
3866
- copyStringFlag(flags, params, "--description", "description");
3867
- copyStringFlag(flags, params, "--entrypoint", "entrypoint");
3868
- copyStringFlag(flags, params, "--runtime", "runtime");
3869
- copyStringFlag(flags, params, "--visibility", "visibility");
3870
- if (flags.values["--content"] !== undefined) {
3871
- params["content"] = flags.values["--content"];
4284
+ copyStringFlag(flags, params, "--message", "message");
4285
+ copyStringFlag(flags, params, "--idempotency-key", "idempotency_key");
4286
+ await emitGemTool(ctx, "gem.version_save", params, {
4287
+ streamStart: { operation: "gem.version_save", path: gemPath },
4288
+ textExtractor: extractVersionSaveSummary
4289
+ });
4290
+ }
4291
+ async function dispatchGemEdits(ctx, args, linkedProject) {
4292
+ const [verb, ...rest] = args;
4293
+ const flags = parseLocalFlags(rest);
4294
+ if (verb === "list") {
4295
+ const params = {
4296
+ path: gemPathFromArgs(flags, linkedProject)
4297
+ };
4298
+ copyStringFlag(flags, params, "--cursor", "cursor");
4299
+ copyOptionalIntegerFlag(flags, params, "--limit", "limit");
4300
+ await emitGemTool(ctx, "gem.edits_list", params);
4301
+ return;
3872
4302
  }
3873
- if (flags.values["--file"] !== undefined) {
3874
- params["content"] = await readFile5(flags.values["--file"], "utf8");
4303
+ if (verb === "show") {
4304
+ const editId = requireCommandValue(lifecycleResourceArg(flags, "Usage: uru gems edits show <gem> <edit>"), "Usage: uru gems edits show <gem> <edit>");
4305
+ await emitGemTool(ctx, "gem.edit_get", {
4306
+ path: gemPathFromArgs(flags, linkedProject),
4307
+ edit_id: editId
4308
+ });
4309
+ return;
3875
4310
  }
3876
- await emitGemTool(ctx, "gem.init", params);
4311
+ throw new CliError("Usage: uru gems edits list <gem> | show <gem> <edit>");
3877
4312
  }
3878
- async function gemDeploy(ctx, args, linkedProject) {
3879
- const flags = parseLocalFlags(args);
3880
- rejectUnsupportedLocalFlags(flags, ["--runtime-provider", "--provider-deployment-id"], "Provider routing is chosen by the Gem runtime contract; the CLI does not accept provider-internal deploy flags.");
3881
- assertAllowedLocalFlags(flags, [
3882
- "--build-id",
3883
- "--dry-run",
3884
- "--force-build",
3885
- "--gem",
3886
- "--link-id",
3887
- "--no-publish",
3888
- "--no-wait",
3889
- "--path",
3890
- "--prod",
3891
- "--skip-promote",
3892
- "--source-version-id"
3893
- ], "uru deploy");
3894
- if (flags.booleans.has("--prod") && flags.booleans.has("--no-publish")) {
3895
- throw new CliError("uru deploy --prod cannot be combined with --no-publish. Use --skip-promote for a staged production deployment.");
3896
- }
3897
- if (flags.booleans.has("--dry-run")) {
3898
- const buildArgs = [...flags.positionals, "--dry-run"];
3899
- for (const flag of ["--path", "--gem"]) {
3900
- const value = flags.values[flag];
3901
- if (value !== undefined) {
3902
- buildArgs.push(flag, value);
4313
+ async function dispatchGemReleases(ctx, args, linkedProject) {
4314
+ const [verb, ...rest] = args;
4315
+ const flags = parseLocalFlags(rest);
4316
+ if (verb === "list") {
4317
+ const params = {
4318
+ path: gemPathFromArgs(flags, linkedProject)
4319
+ };
4320
+ copyStringFlag(flags, params, "--cursor", "cursor");
4321
+ copyOptionalIntegerFlag(flags, params, "--limit", "limit");
4322
+ await emitGemTool(ctx, "gem.releases_list", params);
4323
+ return;
4324
+ }
4325
+ if (verb === "create") {
4326
+ const versionId = lifecycleResourceArg(flags, "Usage: uru gems releases create <gem> <version>");
4327
+ const path = gemPathFromArgs(flags, linkedProject);
4328
+ await emitGemTool(ctx, "gem.release_create", {
4329
+ path,
4330
+ version_id: versionId
4331
+ }, {
4332
+ streamStart: { operation: "gem.release_create", path },
4333
+ textExtractor: extractReleaseSummary
4334
+ });
4335
+ return;
4336
+ }
4337
+ if (verb === "show" || verb === "retry") {
4338
+ const releaseId = lifecycleResourceArg(flags, `Usage: uru gems releases ${verb} <gem> <release>`);
4339
+ const path = gemPathFromArgs(flags, linkedProject);
4340
+ const params = { path, release_id: releaseId };
4341
+ await emitGemTool(ctx, verb === "show" ? "gem.release_get" : "gem.release_retry", params, verb === "retry" ? { textExtractor: extractReleaseSummary } : undefined);
4342
+ return;
4343
+ }
4344
+ throw new CliError("Usage: uru gems releases list|show|create|retry");
4345
+ }
4346
+ async function dispatchGemLinks(ctx, args, linkedProject) {
4347
+ const [verb, ...rest] = args;
4348
+ const flags = parseLocalFlags(rest);
4349
+ if (verb === "list") {
4350
+ await emitGemTool(ctx, "gem.links_list", {
4351
+ path: gemPathFromArgs(flags, linkedProject)
4352
+ });
4353
+ return;
4354
+ }
4355
+ if (verb === "create") {
4356
+ const path = gemPathFromArgs(flags, linkedProject);
4357
+ await emitGemTool(ctx, "gem.link_create", {
4358
+ path,
4359
+ name: requireCommandValue(flags.values["--name"], "links create requires --name"),
4360
+ version_id: requireCommandValue(flags.values["--version"], "links create requires --version")
4361
+ }, { textExtractor: extractLinkCreateSummary });
4362
+ return;
4363
+ }
4364
+ if (verb === "set-version") {
4365
+ const [linkId, versionId] = lifecycleResourceArgs(flags, 2, "Usage: uru gems links set-version <gem> <link> <version>");
4366
+ await emitGemTool(ctx, "gem.link_set_version", {
4367
+ path: gemPathFromArgs(flags, linkedProject),
4368
+ link_id: requireCommandValue(linkId, "Missing Link"),
4369
+ version_id: requireCommandValue(versionId, "Missing Version")
4370
+ }, {
4371
+ textExtractor: extractLinkVersionSummary
4372
+ });
4373
+ return;
4374
+ }
4375
+ if (verb === "set-access") {
4376
+ const linkId = lifecycleResourceArg(flags, "Usage: uru gems links set-access <gem> <link> --gate <public|password|email>");
4377
+ const params = {
4378
+ path: gemPathFromArgs(flags, linkedProject),
4379
+ link_id: linkId,
4380
+ gate_type: requireCommandValue(flags.values["--gate"], "links set-access requires --gate")
4381
+ };
4382
+ copyStringFlag(flags, params, "--passcode", "passcode");
4383
+ const emailAllowlist = flags.values["--email-allowlist"];
4384
+ if (emailAllowlist !== undefined) {
4385
+ params["email_allowlist"] = emailAllowlist.split(",").map((email) => email.trim()).filter((email) => email.length > 0);
4386
+ }
4387
+ copyStringFlag(flags, params, "--expires-at", "expires_at");
4388
+ await emitGemTool(ctx, "gem.link_set_access", params);
4389
+ return;
4390
+ }
4391
+ if (verb === "set-notifications") {
4392
+ const [linkId, mode] = lifecycleResourceArgs(flags, 2, "Usage: uru gems links set-notifications <gem> <link> <mode>");
4393
+ await emitGemTool(ctx, "gem.link_set_notifications", {
4394
+ path: gemPathFromArgs(flags, linkedProject),
4395
+ link_id: requireCommandValue(linkId, "Missing Link"),
4396
+ view_notification_mode: requireCommandValue(mode, "Missing notification mode")
4397
+ });
4398
+ return;
4399
+ }
4400
+ if (verb === "rotate" || verb === "revoke") {
4401
+ const linkId = lifecycleResourceArg(flags, `Usage: uru gems links ${verb} <gem> <link>`);
4402
+ await emitGemTool(ctx, verb === "rotate" ? "gem.link_rotate" : "gem.link_revoke", {
4403
+ path: gemPathFromArgs(flags, linkedProject),
4404
+ link_id: linkId
4405
+ });
4406
+ return;
4407
+ }
4408
+ throw new CliError("Usage: uru gems links list|create|set-version|set-access|set-notifications|rotate|revoke");
4409
+ }
4410
+ async function dispatchGemRuns(ctx, args, linkedProject) {
4411
+ const [verb, ...rest] = args;
4412
+ const flags = parseLocalFlags(rest);
4413
+ if (verb === "list") {
4414
+ const params = {
4415
+ path: gemPathFromArgs(flags, linkedProject)
4416
+ };
4417
+ copyStringFlag(flags, params, "--cursor", "cursor");
4418
+ copyStringFlag(flags, params, "--status", "status");
4419
+ copyOptionalIntegerFlag(flags, params, "--limit", "limit");
4420
+ await emitGemTool(ctx, "gem.runs_list", params);
4421
+ return;
4422
+ }
4423
+ if (verb === "show" || verb === "cancel" || verb === "retry") {
4424
+ const runId = lifecycleResourceArg(flags, `Usage: uru gems runs ${verb} <gem> <run>`);
4425
+ await emitGemTool(ctx, verb === "show" ? "gem.run_get" : verb === "cancel" ? "gem.run_cancel" : "gem.run_retry", {
4426
+ path: gemPathFromArgs(flags, linkedProject),
4427
+ run_id: runId
4428
+ });
4429
+ return;
4430
+ }
4431
+ throw new CliError("Usage: uru gems runs list <gem> | show|cancel|retry <gem> <run>");
4432
+ }
4433
+ function extractVersionSaveSummary(value) {
4434
+ const objects = collectJsonObjects(value);
4435
+ const version = versionLabel(objects);
4436
+ return `Saved Version${version === null ? "" : ` ${version}`}. No Links changed.`;
4437
+ }
4438
+ function extractVersionRestoreSummary(value) {
4439
+ const version = versionLabel(collectJsonObjects(value));
4440
+ return `Restored Version${version === null ? "" : ` ${version}`} to Draft. No Links changed.`;
4441
+ }
4442
+ function extractReleaseSummary(value) {
4443
+ const objects = collectJsonObjects(value);
4444
+ const version = versionLabel(objects);
4445
+ const state = stringFieldFromJsonObjects(objects, [
4446
+ "state",
4447
+ "status",
4448
+ "release_state",
4449
+ "release_status"
4450
+ ]);
4451
+ const stateLabel = state === null || state === "ready" || state === "succeeded" ? "ready" : state === "queued" || state === "building" || state === "preparing" ? "preparing" : state.replaceAll("_", " ").toLowerCase();
4452
+ return `Release ${stateLabel} for Version${version === null ? "" : ` ${version}`}. No Links changed.`;
4453
+ }
4454
+ function extractLinkCreateSummary(value) {
4455
+ const objects = collectJsonObjects(value);
4456
+ const name = stringFieldFromJsonObjects(objects, ["name", "link_name"]);
4457
+ const version = versionLabel(objects);
4458
+ return `Created ${name ?? "Link"}. It opens Version${version === null ? "" : ` ${version}`}. Other Links did not change.`;
4459
+ }
4460
+ function extractLinkVersionSummary(value) {
4461
+ const objects = collectJsonObjects(value);
4462
+ const name = stringFieldFromJsonObjects(objects, ["name", "link_name"]);
4463
+ const version = versionLabel(objects);
4464
+ return `${name ?? "Link"} now opens Version${version === null ? "" : ` ${version}`}. Other Links did not change.`;
4465
+ }
4466
+ function versionLabel(objects) {
4467
+ for (const object of objects) {
4468
+ for (const key of [
4469
+ "version_number",
4470
+ "versionNumber",
4471
+ "gem_version_number",
4472
+ "gemVersionNumber"
4473
+ ]) {
4474
+ const value = object[key];
4475
+ if (typeof value === "number") {
4476
+ if (Number.isSafeInteger(value) && value > 0) {
4477
+ return String(value);
4478
+ }
4479
+ throw new CliError(`Invalid unsafe Version number: ${value}`);
4480
+ }
4481
+ if (typeof value === "string") {
4482
+ const normalized = value.trim();
4483
+ if (/^[1-9]\d*$/u.test(normalized) && BigInt(normalized) <= BigInt(Number.MAX_SAFE_INTEGER)) {
4484
+ return normalized;
4485
+ }
4486
+ throw new CliError(`Invalid unsafe Version number: ${value}`);
3903
4487
  }
3904
4488
  }
3905
- if (flags.booleans.has("--force-build")) {
3906
- buildArgs.push("--force");
4489
+ }
4490
+ return null;
4491
+ }
4492
+ function collectJsonObjects(value) {
4493
+ const objects = [];
4494
+ collectJsonObjectsInto(value, objects);
4495
+ return objects;
4496
+ }
4497
+ function collectJsonObjectsInto(value, objects) {
4498
+ if (Array.isArray(value)) {
4499
+ for (const item of value) {
4500
+ collectJsonObjectsInto(item, objects);
3907
4501
  }
3908
- const sourceVersionId = flags.values["--source-version-id"];
3909
- if (sourceVersionId !== undefined) {
3910
- buildArgs.push("--source-version-id", sourceVersionId);
4502
+ return;
4503
+ }
4504
+ const object = jsonObjectOrNull(value);
4505
+ if (object === null) {
4506
+ return;
4507
+ }
4508
+ objects.push(object);
4509
+ for (const child of Object.values(object)) {
4510
+ collectJsonObjectsInto(child, objects);
4511
+ }
4512
+ }
4513
+ function lifecycleResourceArg(flags, usage) {
4514
+ return lifecycleResourceArgs(flags, 1, usage)[0];
4515
+ }
4516
+ function lifecycleResourceArgs(flags, count, usage) {
4517
+ const resources = flags.positionals.filter((positional) => !positional.startsWith("library/"));
4518
+ if (resources.length !== count) {
4519
+ throw new CliError(usage);
4520
+ }
4521
+ return resources;
4522
+ }
4523
+ function copyOptionalIntegerFlag(flags, params, flag, key) {
4524
+ if (flags.values[flag] === undefined) {
4525
+ return;
4526
+ }
4527
+ const value = integerFlag(flags, flag, 0);
4528
+ if (value <= 0) {
4529
+ throw new CliError(`${flag} must be greater than zero`);
4530
+ }
4531
+ params[key] = value;
4532
+ }
4533
+ function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
4534
+ const allowed = new Set(allowedFlags);
4535
+ const unexpected = Object.keys(flags.values).filter((flag) => flags.values[flag] !== undefined && !allowed.has(flag));
4536
+ if (unexpected.length > 0) {
4537
+ throw new CliError(`${commandName} does not support ${unexpected.join(", ")}.`);
4538
+ }
4539
+ }
4540
+ function stringFieldFromJsonObjects(objects, fields) {
4541
+ for (const object of objects) {
4542
+ for (const field of fields) {
4543
+ const value = object[field];
4544
+ if (typeof value === "string" && value.trim().length > 0) {
4545
+ return value.trim();
4546
+ }
3911
4547
  }
3912
- await gemBuild(ctx, buildArgs, linkedProject);
4548
+ }
4549
+ return null;
4550
+ }
4551
+ function jsonObjectOrNull(value) {
4552
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
4553
+ return null;
4554
+ }
4555
+ return value;
4556
+ }
4557
+
4558
+ // src/gem-lifecycle-commands.ts
4559
+ async function dispatchGems(ctx, sub, args, linkedProject) {
4560
+ const flags = parseLocalFlags(args);
4561
+ const gemPath = () => gemPathFromArgs(flags, linkedProject);
4562
+ if (sub === "inspect") {
4563
+ await emitGemTool(ctx, "gem.inspect", { path: gemPath() });
3913
4564
  return;
3914
4565
  }
3915
- const gemPath = gemPathFromArgs(flags, linkedProject);
3916
- const publish = !flags.booleans.has("--no-publish") && !flags.booleans.has("--skip-promote");
3917
- const params = {
3918
- path: gemPath,
3919
- publish
3920
- };
3921
- if (flags.booleans.has("--prod")) {
3922
- params["metadata"] = {
3923
- deployment_intent: "production",
3924
- promotion_strategy: publish ? "immediate" : "staged"
3925
- };
4566
+ if (sub === "validate") {
4567
+ await emitGemTool(ctx, "gem.validate", { path: gemPath() });
4568
+ return;
3926
4569
  }
3927
- copyStringFlag(flags, params, "--build-id", "build_id");
3928
- copyStringFlag(flags, params, "--link-id", "link_id");
3929
- copyStringFlag(flags, params, "--source-version-id", "source_version_id");
3930
- if (flags.booleans.has("--force-build")) {
3931
- params["force_build"] = true;
4570
+ if (sub === "build") {
4571
+ await gemBuild(ctx, args, linkedProject);
4572
+ return;
3932
4573
  }
3933
- if (flags.booleans.has("--no-wait")) {
3934
- params["wait"] = false;
4574
+ if (await dispatchGemLifecycleResourceCommand(ctx, sub, args, linkedProject)) {
4575
+ return;
3935
4576
  }
3936
- await emitGemTool(ctx, "gem.deploy", params, {
3937
- streamStart: { operation: "gem.deploy", path: gemPath },
3938
- textExtractor: extractDeploymentUrl
3939
- });
4577
+ if (sub === "checks") {
4578
+ const versionId = requireCommandValue(flags.positionals[0], "Usage: uru gems checks <version> [gem]");
4579
+ await emitGemTool(ctx, "gem.checks", {
4580
+ path: gemPathFromArgs(shiftPositionals(flags, 1), linkedProject),
4581
+ version_id: versionId
4582
+ });
4583
+ return;
4584
+ }
4585
+ if (sub === "logs") {
4586
+ await gemLogs(ctx, args, linkedProject);
4587
+ return;
4588
+ }
4589
+ if (sub === "status") {
4590
+ await emitGemTool(ctx, "gem.status", { path: gemPath() });
4591
+ return;
4592
+ }
4593
+ if (sub === "read") {
4594
+ const [path, file] = args;
4595
+ await emitGemTool(ctx, "gem.fs_read", {
4596
+ path: gemFilePath(requireCommandValue(path, "Usage: uru gems read <gem-path> <file>"), requireCommandValue(file, "Usage: uru gems read <gem-path> <file>"))
4597
+ });
4598
+ return;
4599
+ }
4600
+ if (sub === "write") {
4601
+ await gemWrite(ctx, args);
4602
+ return;
4603
+ }
4604
+ throw new CliError("Usage: uru gems inspect|validate|build|edits|versions|releases|links|runs|checks|status|read|write");
4605
+ }
4606
+ async function gemInit(ctx, args) {
4607
+ const flags = parseLocalFlags(args);
4608
+ const path = requireCommandValue(flags.positionals[0], "Usage: uru init <library/name.gem> [--entrypoint main.html] [--file ./main.html]");
4609
+ const params = { path };
4610
+ copyStringFlag(flags, params, "--name", "name");
4611
+ copyStringFlag(flags, params, "--description", "description");
4612
+ copyStringFlag(flags, params, "--entrypoint", "entrypoint");
4613
+ copyStringFlag(flags, params, "--runtime", "runtime");
4614
+ copyStringFlag(flags, params, "--visibility", "visibility");
4615
+ if (flags.values["--content"] !== undefined) {
4616
+ params["content"] = flags.values["--content"];
4617
+ }
4618
+ if (flags.values["--file"] !== undefined) {
4619
+ params["content"] = await readFile5(flags.values["--file"], "utf8");
4620
+ }
4621
+ await emitGemTool(ctx, "gem.init", params);
3940
4622
  }
3941
4623
  async function gemBuild(ctx, args, linkedProject) {
3942
4624
  const flags = parseLocalFlags(args);
3943
- assertAllowedLocalFlags(flags, ["--dry-run", "--force", "--gem", "--outdir", "--path", "--source-version-id"], "uru build");
4625
+ assertAllowedLocalFlags2(flags, ["--dry-run", "--force", "--gem", "--outdir", "--path", "--source-revision-id"], "uru build");
3944
4626
  const gemPath = gemPathFromArgs(flags, linkedProject);
3945
4627
  const params = {
3946
4628
  path: gemPath
@@ -3951,7 +4633,7 @@ async function gemBuild(ctx, args, linkedProject) {
3951
4633
  if (flags.booleans.has("--dry-run")) {
3952
4634
  params["dry_run"] = true;
3953
4635
  }
3954
- copyStringFlag(flags, params, "--source-version-id", "source_version_id");
4636
+ copyStringFlag(flags, params, "--source-revision-id", "source_revision_id");
3955
4637
  if (ctx.outputFormat === "stream-json") {
3956
4638
  writeStreamJson(ctx.io, "started", { operation: "gem.build", path: gemPath });
3957
4639
  writeStreamJson(ctx.io, "phase", {
@@ -3974,63 +4656,9 @@ async function gemBuild(ctx, args, linkedProject) {
3974
4656
  writeText(ctx.io, JSON.stringify(unwrapped, null, 2));
3975
4657
  }
3976
4658
  }
3977
- async function gemPromote(ctx, args, linkedProject) {
3978
- const flags = parseLocalFlags(args);
3979
- assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
3980
- await deployVersionToPublishedLink(ctx, flags, linkedProject, {
3981
- operation: "promote",
3982
- usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] | uru promote --deployment-id <deployment-id> [library/name.gem]"
3983
- });
3984
- }
3985
- async function gemChecks(ctx, args, linkedProject) {
3986
- const flags = parseLocalFlags(args);
3987
- if (flags.positionals[0] === "run") {
3988
- await gemRunCheck(ctx, flags, linkedProject);
3989
- return;
3990
- }
3991
- assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--path"], "uru gems checks");
3992
- const explicitDeploymentId = flags.values["--deployment-id"];
3993
- const gemPathFlags = explicitDeploymentId === undefined ? shiftPositionals(flags, 1) : flags;
3994
- const params = {
3995
- path: gemPathFromArgs(gemPathFlags, linkedProject)
3996
- };
3997
- if (explicitDeploymentId !== undefined) {
3998
- params["deployment_id"] = explicitDeploymentId;
3999
- } else if (flags.positionals[0] !== undefined) {
4000
- params["version_id"] = flags.positionals[0];
4001
- }
4002
- await emitGemTool(ctx, "gem.checks", params);
4003
- }
4004
- async function gemRunCheck(ctx, flags, linkedProject) {
4005
- assertAllowedLocalFlags(flags, ["--blocking", "--deep", "--deployment-id", "--gem", "--path"], "uru gems checks run");
4006
- const kind = requireCommandValue(flags.positionals[1], "Usage: uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]");
4007
- const params = {
4008
- path: gemPathFromArgs(shiftPositionals(flags, 2), linkedProject)
4009
- };
4010
- copyStringFlag(flags, params, "--deployment-id", "deployment_id");
4011
- if (flags.booleans.has("--blocking")) {
4012
- params["blocking"] = true;
4013
- }
4014
- if (kind === "browser" || kind === "browser_verify") {
4015
- if (flags.booleans.has("--deep")) {
4016
- throw new CliError("--deep is only valid for `uru gems checks run security`.");
4017
- }
4018
- await emitGemTool(ctx, "gem.browser_verify", params);
4019
- return;
4020
- }
4021
- if (kind === "security" || kind === "security_scan") {
4022
- if (flags.booleans.has("--deep")) {
4023
- params["deep"] = true;
4024
- }
4025
- await emitGemTool(ctx, "gem.security_scan", params);
4026
- return;
4027
- }
4028
- throw new CliError("Unknown Gem check runner. Use `uru gems checks run browser` or `uru gems checks run security`.");
4029
- }
4030
4659
  async function gemLogs(ctx, args, linkedProject) {
4031
4660
  const flags = parseLocalFlags(args);
4032
- assertAllowedLocalFlags(flags, [
4033
- "--deployment-id",
4661
+ assertAllowedLocalFlags2(flags, [
4034
4662
  "--follow",
4035
4663
  "--follow-duration-ms",
4036
4664
  "--gem",
@@ -4038,15 +4666,16 @@ async function gemLogs(ctx, args, linkedProject) {
4038
4666
  "--limit",
4039
4667
  "--path",
4040
4668
  "--poll-interval-ms",
4041
- "--request-id"
4669
+ "--release",
4670
+ "--run"
4042
4671
  ], "uru logs");
4043
4672
  const params = {
4044
4673
  path: gemPathFromArgs(flags, linkedProject),
4045
4674
  limit: integerFlag(flags, "--limit", 100)
4046
4675
  };
4047
4676
  copyStringFlag(flags, params, "--level", "level");
4048
- copyStringFlag(flags, params, "--deployment-id", "deployment_id");
4049
- copyStringFlag(flags, params, "--request-id", "request_id");
4677
+ copyStringFlag(flags, params, "--release", "release_id");
4678
+ copyStringFlag(flags, params, "--run", "run_id");
4050
4679
  if (flags.booleans.has("--follow")) {
4051
4680
  await followGemLogs(ctx, params, {
4052
4681
  durationMs: boundedIntegerFlag(flags, "--follow-duration-ms", DEFAULT_LOG_FOLLOW_DURATION_MS, MAX_LOG_FOLLOW_DURATION_MS),
@@ -4139,13 +4768,13 @@ function emitFollowLogEntry(ctx, entry) {
4139
4768
  writeText(ctx.io, formatLogEntry(entry));
4140
4769
  }
4141
4770
  function normalizeLogEntries(value) {
4142
- const root = jsonObjectOrNull(value);
4771
+ const root = jsonObjectOrNull2(value);
4143
4772
  const candidate = root === null ? value : root["logs"];
4144
4773
  if (!Array.isArray(candidate)) {
4145
4774
  return [];
4146
4775
  }
4147
4776
  return candidate.flatMap((entry, index) => {
4148
- const object = jsonObjectOrNull(entry);
4777
+ const object = jsonObjectOrNull2(entry);
4149
4778
  if (object === null) {
4150
4779
  return [];
4151
4780
  }
@@ -4188,36 +4817,6 @@ function boundedIntegerFlag(flags, name, fallback, maximum) {
4188
4817
  }
4189
4818
  return Math.min(value, maximum);
4190
4819
  }
4191
- async function gemOpen(ctx, args, linkedProject) {
4192
- const flags = parseLocalFlags(args);
4193
- const result = await executeOperation(ctx, "gem.links_list", {
4194
- path: gemPathFromArgs(flags, linkedProject)
4195
- });
4196
- const url = firstUrl(unwrapToolResult(result));
4197
- if (url === undefined) {
4198
- throw new CliError("No published URL found for this Gem. Run `uru deploy` first.");
4199
- }
4200
- ctx.json ? writeJson(ctx.io, { url }) : writeText(ctx.io, url);
4201
- }
4202
- async function gemRollback(ctx, args, linkedProject) {
4203
- const flags = parseLocalFlags(args);
4204
- assertAllowedLocalFlags(flags, [
4205
- "--deployment-id",
4206
- "--gem",
4207
- "--link-id",
4208
- "--no-wait",
4209
- "--path",
4210
- "--pct",
4211
- "--yes"
4212
- ], "uru rollback");
4213
- if (!ctx.yes && !flags.booleans.has("--yes")) {
4214
- throw new CliError("Rollback requires --yes to confirm the link pointer change.");
4215
- }
4216
- await deployVersionToPublishedLink(ctx, flags, linkedProject, {
4217
- operation: "rollback",
4218
- usage: "Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes"
4219
- });
4220
- }
4221
4820
  async function gemWrite(ctx, args) {
4222
4821
  const flags = parseLocalFlags(args);
4223
4822
  const gemPath = requireCommandValue(flags.positionals[0], "Usage: uru gems write <gem-path> <file> --content <text>|--file <local-file>");
@@ -4237,142 +4836,13 @@ function gemFilePath(gemPath, filePath) {
4237
4836
  const normalizedFilePath = trimLeadingSlashes(filePath);
4238
4837
  return `${normalizedGemPath}/${normalizedFilePath}`;
4239
4838
  }
4240
- async function dispatchGemVersions(ctx, args, linkedProject) {
4241
- const [verb, ...rest] = args;
4242
- if (verb === "upload") {
4243
- await gemVersionUpload(ctx, rest, linkedProject);
4244
- return;
4245
- }
4246
- if (verb === "deploy") {
4247
- const flags = parseLocalFlags(rest);
4248
- rejectUnsupportedLocalFlags(flags, ["--runtime-provider", "--provider-deployment-id"], "Provider routing is chosen by the Gem runtime contract; version deploy does not accept provider-internal flags.");
4249
- assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct", "--skip-promote"], "uru gems versions deploy");
4250
- const version = requireCommandValue(flags.positionals[0], "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]");
4251
- const [versionId, trafficPercent] = version.split("@", 2);
4252
- const gemPath = gemPathFromArgs(shiftPositionals(flags, 1), linkedProject);
4253
- const pct = flags.values["--pct"] ?? trafficPercent;
4254
- const trafficPercentValue = pct === undefined || pct === "" ? 100 : parseTrafficPercent(pct);
4255
- const publish = !flags.booleans.has("--skip-promote");
4256
- if (publish && trafficPercentValue !== 100) {
4257
- throw new CliError("Gradual Gem traffic shifts are not enabled yet. Use --skip-promote to create the deployment without changing live traffic, or deploy with --pct 100.");
4258
- }
4259
- const params = {
4260
- path: gemPath,
4261
- version_id: requireCommandValue(versionId, "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]"),
4262
- traffic_percent: trafficPercentValue,
4263
- publish
4264
- };
4265
- copyStringFlag(flags, params, "--link-id", "link_id");
4266
- if (flags.booleans.has("--no-wait")) {
4267
- params["wait"] = false;
4268
- }
4269
- await emitGemTool(ctx, "gem.version_deploy", params, {
4270
- streamStart: { operation: "gem.version_deploy", path: gemPath },
4271
- textExtractor: extractDeploymentUrl
4272
- });
4273
- return;
4274
- }
4275
- throw new CliError("Usage: uru gems versions upload [library/name.gem] | deploy <version-id> [library/name.gem]");
4276
- }
4277
- async function gemVersionUpload(ctx, args, linkedProject) {
4278
- const flags = parseLocalFlags(args);
4279
- assertAllowedLocalFlags(flags, [
4280
- "--force",
4281
- "--gem",
4282
- "--message",
4283
- "--path",
4284
- "--preview-alias",
4285
- "--source-version-id",
4286
- "--tag",
4287
- "--tags"
4288
- ], "uru gems versions upload");
4289
- const gemPath = gemPathFromArgs(flags, linkedProject);
4290
- const params = {
4291
- path: gemPath
4292
- };
4293
- if (flags.booleans.has("--force")) {
4294
- params["force"] = true;
4295
- }
4296
- copyStringFlag(flags, params, "--source-version-id", "source_version_id");
4297
- copyStringFlag(flags, params, "--message", "message");
4298
- const previewAlias = flags.values["--preview-alias"];
4299
- if (previewAlias !== undefined) {
4300
- params["preview_alias"] = validatePreviewAlias(previewAlias);
4301
- }
4302
- const tags = tagsFromFlags(flags);
4303
- if (tags.length > 0) {
4304
- params["tags"] = tags;
4305
- }
4306
- await emitGemTool(ctx, "gem.version_upload", params, {
4307
- streamStart: { operation: "gem.version_upload", path: gemPath },
4308
- textExtractor: extractVersionUploadSummary
4309
- });
4310
- }
4311
- function parseTrafficPercent(raw) {
4312
- const value = Number(raw);
4313
- if (!Number.isFinite(value) || value < 0 || value > 100) {
4314
- throw new CliError("--pct or @pct must be a number from 0 through 100");
4315
- }
4316
- return value;
4317
- }
4318
- async function deployVersionToPublishedLink(ctx, flags, linkedProject, options) {
4319
- const deploymentId = flags.values["--deployment-id"];
4320
- const firstPositional = flags.positionals[0];
4321
- const versionId = deploymentId === undefined && firstPositional !== undefined ? firstPositional.startsWith("library/") ? undefined : firstPositional : undefined;
4322
- if (deploymentId === undefined && versionId === undefined) {
4323
- throw new CliError(options.usage);
4324
- }
4325
- const gemPath = gemPathFromArgs(versionId === undefined ? flags : shiftPositionals(flags, 1), linkedProject);
4326
- const trafficPercent = parseTrafficPercent(flags.values["--pct"] ?? "100");
4327
- if (trafficPercent !== 100) {
4328
- throw new CliError(`${options.operation} does not support partial traffic shifts yet. Deploy the version with --skip-promote for validation, or ${options.operation} with --pct 100.`);
4329
- }
4330
- const params = {
4331
- path: gemPath
4332
- };
4333
- if (deploymentId === undefined) {
4334
- params["version_id"] = requireCommandValue(versionId, options.usage);
4335
- } else {
4336
- params["app_deployment_id"] = deploymentId;
4337
- }
4338
- copyStringFlag(flags, params, "--link-id", "link_id");
4339
- if (flags.booleans.has("--no-wait")) {
4340
- params["wait"] = false;
4341
- }
4342
- const operation = `gem.${options.operation}`;
4343
- await emitGemTool(ctx, operation, params, {
4344
- streamStart: { operation, path: gemPath },
4345
- textExtractor: extractDeploymentUrl
4346
- });
4347
- }
4348
- function tagsFromFlags(flags) {
4349
- const raw = flags.values["--tags"] ?? flags.values["--tag"];
4350
- if (raw === undefined) {
4351
- return [];
4352
- }
4353
- return raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
4354
- }
4355
- function validatePreviewAlias(rawAlias) {
4356
- const alias = rawAlias.trim();
4357
- if (alias.length === 0 || alias.length > 63 || !/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(alias)) {
4358
- throw new CliError("--preview-alias must be a DNS-safe label: 1-63 characters, start with a lowercase letter, contain only lowercase letters, numbers, and dashes, and not end with a dash.");
4359
- }
4360
- return alias;
4361
- }
4362
- function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
4839
+ function assertAllowedLocalFlags2(flags, allowedFlags, commandName) {
4363
4840
  const allowed = new Set(allowedFlags);
4364
4841
  const unexpected = localFlagNames(flags).filter((flag) => !allowed.has(flag));
4365
4842
  if (unexpected.length > 0) {
4366
4843
  throw new CliError(`${commandName} does not support ${unexpected.join(", ")}.`);
4367
4844
  }
4368
4845
  }
4369
- function rejectUnsupportedLocalFlags(flags, unsupportedFlags, message) {
4370
- const unsupported = new Set(unsupportedFlags);
4371
- const found = localFlagNames(flags).filter((flag) => unsupported.has(flag));
4372
- if (found.length > 0) {
4373
- throw new CliError(`${message} Unsupported flag(s): ${found.join(", ")}.`);
4374
- }
4375
- }
4376
4846
  function localFlagNames(flags) {
4377
4847
  return [...new Set([...Object.keys(flags.values), ...flags.booleans])];
4378
4848
  }
@@ -4387,10 +4857,10 @@ async function writeBuildManifest(ctx, outdir, buildResult) {
4387
4857
  `, { mode: 384 });
4388
4858
  }
4389
4859
  function gemBuildManifestFromBuildResult(buildResult) {
4390
- const root = jsonObjectOrNull(buildResult);
4391
- const build = jsonObjectOrNull(root?.["build"]);
4392
- const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
4393
- const buildMetadata = jsonObjectOrNull(buildResultDetails?.["metadata"]);
4860
+ const root = jsonObjectOrNull2(buildResult);
4861
+ const build = jsonObjectOrNull2(root?.["build"]);
4862
+ const buildResultDetails = jsonObjectOrNull2(build?.["build_result"]);
4863
+ const buildMetadata = jsonObjectOrNull2(buildResultDetails?.["metadata"]);
4394
4864
  const candidates = [
4395
4865
  root?.["gem_build_manifest"],
4396
4866
  root?.["gemBuildManifest"],
@@ -4410,7 +4880,7 @@ function gemBuildManifestFromBuildResult(buildResult) {
4410
4880
  throw new CliError("Build result did not include a GemBuildManifestV1. Re-run after the backend build manifest projection is deployed, or use --force to create a fresh ready build.");
4411
4881
  }
4412
4882
  function normalizeGemBuildManifest(value) {
4413
- const manifest = jsonObjectOrNull(value);
4883
+ const manifest = jsonObjectOrNull2(value);
4414
4884
  if (manifest === null) {
4415
4885
  return null;
4416
4886
  }
@@ -4437,10 +4907,10 @@ function normalizeGemBuildManifest(value) {
4437
4907
  };
4438
4908
  }
4439
4909
  function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4440
- const root = jsonObjectOrNull(buildResult);
4441
- const build = jsonObjectOrNull(root?.["build"]);
4442
- const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
4443
- const buildMetadata = jsonObjectOrNull(buildResultDetails?.["metadata"]);
4910
+ const root = jsonObjectOrNull2(buildResult);
4911
+ const build = jsonObjectOrNull2(root?.["build"]);
4912
+ const buildResultDetails = jsonObjectOrNull2(build?.["build_result"]);
4913
+ const buildMetadata = jsonObjectOrNull2(buildResultDetails?.["metadata"]);
4444
4914
  const runtimeProfile = firstJsonObject([
4445
4915
  root?.["runtime_profile"],
4446
4916
  root?.["runtimeProfile"],
@@ -4477,8 +4947,8 @@ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4477
4947
  schema: "uru.gem.provider_decision.v1",
4478
4948
  schema_version: 1,
4479
4949
  manifest_kind: "gem_provider_decision",
4480
- provider: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["provider", "runtime_provider", "runtimeProvider", "selected_provider"]),
4481
- runtime_class: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["runtime_class", "runtimeClass", "class", "serving_mode"]),
4950
+ provider: stringFieldFromJsonObjects2([providerDecision, runtimeProfile, manifest], ["provider", "runtime_provider", "runtimeProvider", "selected_provider"]),
4951
+ runtime_class: stringFieldFromJsonObjects2([providerDecision, runtimeProfile, manifest], ["runtime_class", "runtimeClass", "class", "serving_mode"]),
4482
4952
  runtime_profile: runtimeProfile,
4483
4953
  provider_decision: providerDecision,
4484
4954
  runtime_contract: runtimeContract,
@@ -4492,14 +4962,14 @@ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4492
4962
  }
4493
4963
  function firstJsonObject(values) {
4494
4964
  for (const value of values) {
4495
- const object = jsonObjectOrNull(value);
4965
+ const object = jsonObjectOrNull2(value);
4496
4966
  if (object !== null) {
4497
4967
  return object;
4498
4968
  }
4499
4969
  }
4500
4970
  return null;
4501
4971
  }
4502
- function stringFieldFromJsonObjects(objects, keys) {
4972
+ function stringFieldFromJsonObjects2(objects, keys) {
4503
4973
  for (const object of objects) {
4504
4974
  for (const key of keys) {
4505
4975
  const value = object[key];
@@ -4510,57 +4980,12 @@ function stringFieldFromJsonObjects(objects, keys) {
4510
4980
  }
4511
4981
  return null;
4512
4982
  }
4513
- function jsonObjectOrNull(value) {
4983
+ function jsonObjectOrNull2(value) {
4514
4984
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
4515
4985
  return null;
4516
4986
  }
4517
4987
  return value;
4518
4988
  }
4519
- function extractDeploymentUrl(value) {
4520
- return firstUrl(value);
4521
- }
4522
- function extractVersionUploadSummary(value) {
4523
- const objects = collectJsonObjects(value);
4524
- const versionId = stringFieldFromJsonObjects(objects, [
4525
- "version_id",
4526
- "versionId",
4527
- "id"
4528
- ]);
4529
- const previewUrl = stringFieldFromJsonObjects(objects, [
4530
- "preview_url",
4531
- "previewUrl",
4532
- "preview_alias_url",
4533
- "previewAliasUrl",
4534
- "url"
4535
- ]) ?? firstUrl(value);
4536
- const lines = [
4537
- versionId === null ? null : `Version: ${versionId}`,
4538
- previewUrl === undefined || previewUrl === null ? null : `Preview: ${previewUrl}`
4539
- ].filter((line) => line !== null);
4540
- return lines.length === 0 ? undefined : lines.join(`
4541
- `);
4542
- }
4543
- function collectJsonObjects(value) {
4544
- const objects = [];
4545
- collectJsonObjectsInto(value, objects);
4546
- return objects;
4547
- }
4548
- function collectJsonObjectsInto(value, objects) {
4549
- if (Array.isArray(value)) {
4550
- for (const item of value) {
4551
- collectJsonObjectsInto(item, objects);
4552
- }
4553
- return;
4554
- }
4555
- const object = jsonObjectOrNull(value);
4556
- if (object === null) {
4557
- return;
4558
- }
4559
- objects.push(object);
4560
- for (const child of Object.values(object)) {
4561
- collectJsonObjectsInto(child, objects);
4562
- }
4563
- }
4564
4989
  function trimTrailingSlashes(value) {
4565
4990
  let end = value.length;
4566
4991
  while (end > 0 && value.charCodeAt(end - 1) === 47) {
@@ -4576,6 +5001,62 @@ function trimLeadingSlashes(value) {
4576
5001
  return value.slice(start);
4577
5002
  }
4578
5003
 
5004
+ // src/gem-open-command.ts
5005
+ async function gemOpen(ctx, args, linkedProject) {
5006
+ const flags = parseLocalFlags(args);
5007
+ const result = await executeOperation(ctx, "gem.links_list", {
5008
+ path: gemPathFromArgs(flags, linkedProject)
5009
+ });
5010
+ const selector = flags.values["--link"]?.trim();
5011
+ const links = publicLinks(unwrapToolResult(result));
5012
+ const selected = selector === undefined || selector === "" ? links.length === 1 ? links[0] : null : links.find((link) => link.id === selector || link.name === selector) ?? null;
5013
+ if (selected === null || selected === undefined) {
5014
+ if (links.length === 0) {
5015
+ throw new CliError("No active Link found. Create a Version, then create a Link for the Version.");
5016
+ }
5017
+ if (selector === undefined || selector === "") {
5018
+ throw new CliError("This Gem has multiple active Links. Run `uru gems links list`, then use `uru open --link <link-id-or-name>`.");
5019
+ }
5020
+ throw new CliError(`No active Link matches '${selector}'. Run \`uru gems links list\`.`);
5021
+ }
5022
+ if (ctx.json) {
5023
+ writeJson(ctx.io, {
5024
+ linkId: selected.id,
5025
+ name: selected.name,
5026
+ url: selected.url
5027
+ });
5028
+ return;
5029
+ }
5030
+ writeText(ctx.io, selected.url);
5031
+ }
5032
+ function publicLinks(value) {
5033
+ const root = jsonObjectOrNull3(value);
5034
+ const candidates = root === null ? [] : root["links"];
5035
+ if (!Array.isArray(candidates)) {
5036
+ return [];
5037
+ }
5038
+ return candidates.flatMap((candidate) => {
5039
+ const link = jsonObjectOrNull3(candidate);
5040
+ const id = link === null ? undefined : stringField3(link, ["id", "link_id"]);
5041
+ const name = link === null ? undefined : stringField3(link, ["name"]);
5042
+ const url = link === null ? undefined : stringField3(link, ["publish_url", "publishUrl", "url"]);
5043
+ const status = link === null ? undefined : stringField3(link, ["status"]);
5044
+ return id !== undefined && name !== undefined && url !== undefined && status === "active" ? [{ id, name, url }] : [];
5045
+ });
5046
+ }
5047
+ function jsonObjectOrNull3(value) {
5048
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
5049
+ }
5050
+ function stringField3(entry, keys) {
5051
+ for (const key of keys) {
5052
+ const value = entry[key];
5053
+ if (typeof value === "string" && value.trim() !== "") {
5054
+ return value.trim();
5055
+ }
5056
+ }
5057
+ return;
5058
+ }
5059
+
4579
5060
  // src/gem-source-commands.ts
4580
5061
  import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
4581
5062
  import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
@@ -4599,14 +5080,14 @@ async function gemSourcePull(ctx, args, linkedProject) {
4599
5080
  if (filePaths.length === 0) {
4600
5081
  throw new CliError(`No source files found for ${gemPath}.`);
4601
5082
  }
4602
- let sourceVersionId = findSourceVersionId(tree);
5083
+ let sourceRevisionId = findSourceRevisionId(tree);
4603
5084
  const files = {};
4604
5085
  const pulledFiles = [];
4605
5086
  for (const filePath of filePaths) {
4606
5087
  const readResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_read", {
4607
5088
  path: gemFilePath(gemPath, filePath)
4608
5089
  }));
4609
- sourceVersionId = sourceVersionId ?? findSourceVersionId(readResult);
5090
+ sourceRevisionId = sourceRevisionId ?? findSourceRevisionId(readResult);
4610
5091
  const content = extractRemoteFileContent(readResult, filePath);
4611
5092
  pulledFiles.push({ path: filePath, content });
4612
5093
  files[filePath] = sha256Hex(content);
@@ -4616,7 +5097,7 @@ async function gemSourcePull(ctx, args, linkedProject) {
4616
5097
  }
4617
5098
  const pruned = await pruneRemoteDeletedSourceFiles(ctx.cwd, previousLock, localFilesBeforePull, new Set(filePaths));
4618
5099
  const lock = {
4619
- ...sourceVersionId === undefined ? {} : { sourceVersionId },
5100
+ ...sourceRevisionId === undefined ? {} : { sourceRevisionId },
4620
5101
  files,
4621
5102
  checkedOutAt: new Date().toISOString()
4622
5103
  };
@@ -4627,7 +5108,7 @@ async function gemSourcePull(ctx, args, linkedProject) {
4627
5108
  files: filePaths,
4628
5109
  prunedFiles: pruned.deleted,
4629
5110
  preservedFiles: pruned.preserved,
4630
- ...sourceVersionId === undefined ? {} : { sourceVersionId }
5111
+ ...sourceRevisionId === undefined ? {} : { sourceRevisionId }
4631
5112
  }, formatPullSummary(gemPath, filePaths.length, pruned));
4632
5113
  }
4633
5114
  async function gemSourceStatus(ctx, args, linkedProject) {
@@ -4638,7 +5119,7 @@ async function gemSourceStatus(ctx, args, linkedProject) {
4638
5119
  const localStatus = compareSourceLock(lock, localFiles);
4639
5120
  const remote = await remoteSourceOperation(ctx, "gem.fs_status", {
4640
5121
  path: gemPath,
4641
- ...lock.sourceVersionId === undefined ? {} : { base: lock.sourceVersionId }
5122
+ ...lock.sourceRevisionId === undefined ? {} : { base: lock.sourceRevisionId }
4642
5123
  });
4643
5124
  const report = sourceStatusReport(gemPath, lock, localStatus, remote);
4644
5125
  if (ctx.outputFormat === "stream-json") {
@@ -4659,7 +5140,7 @@ async function gemSourceDiff(ctx, args, linkedProject) {
4659
5140
  const remote = await remoteSourceOperation(ctx, "gem.fs_diff", {
4660
5141
  path: gemPath,
4661
5142
  mode,
4662
- ...lock.sourceVersionId === undefined ? {} : { base: lock.sourceVersionId }
5143
+ ...lock.sourceRevisionId === undefined ? {} : { base: lock.sourceRevisionId }
4663
5144
  });
4664
5145
  const report = sourceStatusReport(gemPath, lock, localStatus, remote);
4665
5146
  if (ctx.outputFormat === "stream-json") {
@@ -4681,11 +5162,11 @@ async function gemSourcePush(ctx, args, linkedProject) {
4681
5162
  if (sortedFiles.length === 0 && localStatus.deleted.length === 0) {
4682
5163
  throw new CliError("No local Gem source files to push.");
4683
5164
  }
4684
- if (lock.sourceVersionId === undefined && !force) {
4685
- throw new CliError("No .uru/source-lock.json sourceVersionId found. Run `uru pull` first, or re-run `uru push --force` to explicitly overwrite the remote Gem source.", { code: ExitCode.Conflict });
5165
+ if (lock.sourceRevisionId === undefined && !force) {
5166
+ throw new CliError("No .uru/source-lock.json sourceRevisionId found. Run `uru pull` first, or re-run `uru push --force` to explicitly overwrite the remote Gem source.", { code: ExitCode.Conflict });
4686
5167
  }
4687
5168
  let sourceCas = {
4688
- ...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId }
5169
+ ...lock.sourceRevisionId === undefined ? {} : { sourceRevisionId: lock.sourceRevisionId }
4689
5170
  };
4690
5171
  try {
4691
5172
  const syncResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_sync", {
@@ -4707,9 +5188,9 @@ async function gemSourcePush(ctx, args, linkedProject) {
4707
5188
  }
4708
5189
  throw error;
4709
5190
  }
4710
- const nextSourceVersionId = sourceCas.sourceVersionId;
5191
+ const nextSourceRevisionId = sourceCas.sourceRevisionId;
4711
5192
  await writeSourceLock(ctx.cwd, {
4712
- ...nextSourceVersionId === undefined ? {} : { sourceVersionId: nextSourceVersionId },
5193
+ ...nextSourceRevisionId === undefined ? {} : { sourceRevisionId: nextSourceRevisionId },
4713
5194
  files: sourceHashesFromLocalFiles(localFiles),
4714
5195
  checkedOutAt: new Date().toISOString()
4715
5196
  });
@@ -4718,7 +5199,7 @@ async function gemSourcePush(ctx, args, linkedProject) {
4718
5199
  path: gemPath,
4719
5200
  files: sortedFiles.map((file) => file.path),
4720
5201
  deletedFiles: [...localStatus.deleted],
4721
- ...nextSourceVersionId === undefined ? {} : { sourceVersionId: nextSourceVersionId }
5202
+ ...nextSourceRevisionId === undefined ? {} : { sourceRevisionId: nextSourceRevisionId }
4722
5203
  }, formatPushSummary(gemPath, sortedFiles.length, localStatus.deleted.length));
4723
5204
  }
4724
5205
  function localFileBatchWritePayload(file) {
@@ -4766,17 +5247,17 @@ function formatPullSummary(gemPath, writtenCount, pruned) {
4766
5247
  return parts.join("; ");
4767
5248
  }
4768
5249
  function sourceMutationCasParams(cas, force) {
4769
- if (force || cas.sourceVersionId === undefined) {
5250
+ if (force || cas.sourceRevisionId === undefined) {
4770
5251
  return { force: true };
4771
5252
  }
4772
5253
  return {
4773
- base_source_version_id: cas.sourceVersionId
5254
+ base_source_revision_id: cas.sourceRevisionId
4774
5255
  };
4775
5256
  }
4776
5257
  function nextSourceMutationCas(current, result) {
4777
- const sourceVersionId = findSourceVersionId(result) ?? current.sourceVersionId;
5258
+ const sourceRevisionId = findSourceRevisionId(result) ?? current.sourceRevisionId;
4778
5259
  return {
4779
- ...sourceVersionId === undefined ? {} : { sourceVersionId }
5260
+ ...sourceRevisionId === undefined ? {} : { sourceRevisionId }
4780
5261
  };
4781
5262
  }
4782
5263
  function formatPushSummary(gemPath, writtenCount, deletedCount) {
@@ -4792,7 +5273,7 @@ function formatPushSummary(gemPath, writtenCount, deletedCount) {
4792
5273
  function sourceStatusReport(path, lock, local, remote) {
4793
5274
  return {
4794
5275
  path,
4795
- sourceVersionId: lock.sourceVersionId ?? null,
5276
+ sourceRevisionId: lock.sourceRevisionId ?? null,
4796
5277
  local,
4797
5278
  remote
4798
5279
  };
@@ -4800,7 +5281,7 @@ function sourceStatusReport(path, lock, local, remote) {
4800
5281
  function sourceStatusReportJson(report) {
4801
5282
  return {
4802
5283
  path: report.path,
4803
- sourceVersionId: report.sourceVersionId,
5284
+ sourceRevisionId: report.sourceRevisionId,
4804
5285
  local: localSourceStatusJson(report.local),
4805
5286
  remote: remoteSourceOperationResultJson(report.remote)
4806
5287
  };
@@ -4828,7 +5309,7 @@ function remoteSourceOperationResultJson(remote) {
4828
5309
  function formatSourceStatus(report) {
4829
5310
  const lines = [
4830
5311
  `Gem: ${report.path}`,
4831
- `Source version: ${report.sourceVersionId ?? "unknown"}`,
5312
+ `Source version: ${report.sourceRevisionId ?? "unknown"}`,
4832
5313
  `Local source: ${report.local.clean ? "clean" : "dirty"}`,
4833
5314
  ...formatLocalStatusEntries(report.local),
4834
5315
  `Remote source: ${formatRemoteSummary(report.remote)}`
@@ -4840,7 +5321,7 @@ function formatSourceDiff(report) {
4840
5321
  const localLines = formatLocalStatusEntries(report.local);
4841
5322
  const lines = [
4842
5323
  `Gem: ${report.path}`,
4843
- `Source version: ${report.sourceVersionId ?? "unknown"}`,
5324
+ `Source version: ${report.sourceRevisionId ?? "unknown"}`,
4844
5325
  "Local changes since .uru/source-lock.json:",
4845
5326
  ...localLines.length === 0 ? [" clean"] : localLines.map((line) => ` ${line}`),
4846
5327
  "Remote diff:",
@@ -4977,17 +5458,17 @@ function safeProjectPath(path) {
4977
5458
  }
4978
5459
  return parts.join("/");
4979
5460
  }
4980
- function findSourceVersionId(value) {
5461
+ function findSourceRevisionId(value) {
4981
5462
  let found;
4982
5463
  visitJson(value, (candidate) => {
4983
5464
  if (!isJsonObject2(candidate)) {
4984
5465
  return;
4985
5466
  }
4986
5467
  const valueAtKey = stringField(candidate, [
4987
- "currentSourceVersionId",
4988
- "current_source_version_id",
4989
- "sourceVersionId",
4990
- "source_version_id"
5468
+ "currentSourceRevisionId",
5469
+ "current_source_revision_id",
5470
+ "sourceRevisionId",
5471
+ "source_revision_id"
4991
5472
  ]);
4992
5473
  if (valueAtKey !== undefined) {
4993
5474
  found = valueAtKey;
@@ -5012,7 +5493,7 @@ function visitJson(value, visitor) {
5012
5493
  }
5013
5494
  function isConflictLikeError(error) {
5014
5495
  const message = error instanceof Error ? error.message.toLowerCase() : "";
5015
- return message.includes("409") || message.includes("conflict") || message.includes("stale") || message.includes("base_source_version") || message.includes("current_source_version") || message.includes("version mismatch");
5496
+ return message.includes("409") || message.includes("conflict") || message.includes("stale") || message.includes("base_source_revision") || message.includes("current_source_revision") || message.includes("version mismatch");
5016
5497
  }
5017
5498
 
5018
5499
  // src/token-env-secret-commands.ts
@@ -5478,11 +5959,21 @@ async function runCli(options) {
5478
5959
  const envGemId = nonEmptyString(options.env?.["URU_GEM_ID"]);
5479
5960
  const explicitGemRef = parsed.flags.gem ?? envGemId;
5480
5961
  const apiUrl = parsed.flags.apiUrl ?? envApiUrl ?? config.apiUrl ?? DEFAULT_API_URL;
5481
- const token = flagToken ?? envToken ?? await resolveStoredToken(credentialStore, apiUrl, config);
5482
5962
  const cwd = parsed.flags.cwd ?? process.cwd();
5483
5963
  const linkedProjectFromDisk = await readLinkedProject(cwd);
5484
5964
  const linkedProject = explicitGemRef === undefined ? linkedProjectFromDisk : linkedProjectWithExplicitGemRef(linkedProjectFromDisk, explicitGemRef);
5485
5965
  const workspace = parsed.flags.workspace ?? envWorkspace ?? linkedProject.workspaceId ?? config.workspace;
5966
+ const storedToken = flagToken === undefined && envToken === undefined && shouldResolveStoredToken(parsed.command) ? await resolveStoredToken({
5967
+ credentialStore,
5968
+ store,
5969
+ apiUrl,
5970
+ config,
5971
+ ...workspace === undefined ? {} : { workspaceId: workspace },
5972
+ ...options.fetch === undefined ? {} : { fetch: options.fetch },
5973
+ ...parsed.command[0] === "login" ? { recoverInvalidCredential: true } : {}
5974
+ }) : undefined;
5975
+ const token = flagToken ?? envToken ?? storedToken;
5976
+ const runtimeConfig = await store.read();
5486
5977
  const outputFormat = parsed.flags.outputFormat ?? (parsed.flags.json ? "json" : "text");
5487
5978
  const callbackHost = parsed.flags.callbackHost ?? "127.0.0.1";
5488
5979
  const callbackPort = parseCallbackPort(parsed.flags.callbackPort);
@@ -5496,7 +5987,7 @@ async function runCli(options) {
5496
5987
  const ctx = {
5497
5988
  io,
5498
5989
  store,
5499
- config,
5990
+ config: runtimeConfig,
5500
5991
  credentialStore,
5501
5992
  apiUrl,
5502
5993
  ...token !== undefined ? { token } : {},
@@ -5602,11 +6093,18 @@ async function dispatchWithImplicitLogin(ctx, command, linkedProject) {
5602
6093
  }
5603
6094
  await login(ctx, { silentSuccess: true });
5604
6095
  const nextConfig = await ctx.store.read();
5605
- const nextToken = await resolveStoredToken(ctx.credentialStore, ctx.apiUrl, nextConfig);
6096
+ const selectedWorkspace = ctx.workspace ?? nextConfig.workspace;
6097
+ const nextToken = await resolveStoredToken({
6098
+ credentialStore: ctx.credentialStore,
6099
+ store: ctx.store,
6100
+ apiUrl: ctx.apiUrl,
6101
+ config: nextConfig,
6102
+ ...selectedWorkspace === undefined ? {} : { workspaceId: selectedWorkspace },
6103
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
6104
+ });
5606
6105
  if (nextToken === undefined) {
5607
6106
  throw new CliError("Login completed but no CLI session token was stored. Re-run `uru login` or pass --token.", { code: ExitCode.AuthRequired });
5608
6107
  }
5609
- const selectedWorkspace = ctx.workspace ?? nextConfig.workspace;
5610
6108
  const nextCtx = {
5611
6109
  ...ctx,
5612
6110
  config: nextConfig,
@@ -5616,7 +6114,14 @@ async function dispatchWithImplicitLogin(ctx, command, linkedProject) {
5616
6114
  await dispatch(nextCtx, command, linkedProject);
5617
6115
  }
5618
6116
  function shouldStartImplicitLogin(ctx, error) {
5619
- return error.exitCode === ExitCode.AuthRequired && ctx.token === undefined && !ctx.noInput && !ctx.terminal.ci && ctx.terminal.stdinIsTty && ctx.terminal.stderrIsTty;
6117
+ return error.exitCode === ExitCode.AuthRequired && !ctx.tokenIsExplicit && !ctx.noInput && !ctx.terminal.ci && ctx.terminal.stdinIsTty && ctx.terminal.stderrIsTty;
6118
+ }
6119
+ function shouldResolveStoredToken(command) {
6120
+ const [head, sub] = command;
6121
+ if (head === "operations" && (sub === "ls" || sub === "list")) {
6122
+ return false;
6123
+ }
6124
+ return !["logout", "link", "unlink", "docs", "help", "complete", "update"].includes(head ?? "");
5620
6125
  }
5621
6126
  function terminalStdin(stdin) {
5622
6127
  return stdin === undefined ? process.stdin : stdin;
@@ -5628,8 +6133,26 @@ async function dispatch(ctx, command, linkedProject) {
5628
6133
  return;
5629
6134
  }
5630
6135
  if (head === "logout") {
5631
- await ctx.credentialStore.clearToken(ctx.apiUrl);
5632
- await ctx.store.clear();
6136
+ let revocationFailure;
6137
+ try {
6138
+ await revokeStoredOAuthLogin({
6139
+ credentialStore: ctx.credentialStore,
6140
+ apiUrl: ctx.apiUrl,
6141
+ config: ctx.config,
6142
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
6143
+ });
6144
+ } catch (error) {
6145
+ revocationFailure = error;
6146
+ } finally {
6147
+ await ctx.credentialStore.clearToken(ctx.apiUrl);
6148
+ await ctx.store.clear();
6149
+ }
6150
+ if (revocationFailure !== undefined) {
6151
+ throw new CliError("Logged out locally, but server-side credential revocation could not be confirmed.", {
6152
+ errorCode: "oauth_logout_revocation_failed",
6153
+ cause: revocationFailure
6154
+ });
6155
+ }
5633
6156
  emit(ctx, { ok: true }, "Logged out");
5634
6157
  return;
5635
6158
  }
@@ -5698,16 +6221,6 @@ async function dispatch(ctx, command, linkedProject) {
5698
6221
  await gemBuild(ctx, command.slice(1), linkedProject);
5699
6222
  return;
5700
6223
  }
5701
- if (head === "deploy") {
5702
- await withWorkspace(ctx);
5703
- await gemDeploy(ctx, command.slice(1), linkedProject);
5704
- return;
5705
- }
5706
- if (head === "promote") {
5707
- await withWorkspace(ctx);
5708
- await gemPromote(ctx, command.slice(1), linkedProject);
5709
- return;
5710
- }
5711
6224
  if (head === "pull") {
5712
6225
  await withWorkspace(ctx);
5713
6226
  await gemSourcePull(ctx, command.slice(1), linkedProject);
@@ -5738,11 +6251,6 @@ async function dispatch(ctx, command, linkedProject) {
5738
6251
  await gemOpen(ctx, command.slice(1), linkedProject);
5739
6252
  return;
5740
6253
  }
5741
- if (head === "rollback") {
5742
- await withWorkspace(ctx);
5743
- await gemRollback(ctx, command.slice(1), linkedProject);
5744
- return;
5745
- }
5746
6254
  if (head === "gems") {
5747
6255
  await withWorkspace(ctx);
5748
6256
  await dispatchGems(ctx, sub, command.slice(2), linkedProject);