@uru-intelligence/cli 0.3.55 → 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 +1416 -806
  3. package/package.json +14 -4
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) {
@@ -906,171 +987,6 @@ function normalizeTool(value) {
906
987
  return [tool];
907
988
  }
908
989
 
909
- // src/cli-helpers.ts
910
- function parseApiArgs(args) {
911
- let method = "GET";
912
- let path;
913
- let body;
914
- const fields = {};
915
- for (let index = 0;index < args.length; index += 1) {
916
- const arg = args[index];
917
- if (arg === undefined) {
918
- continue;
919
- }
920
- if (arg === "-X" || arg === "--method") {
921
- method = requireCommandValue(args[index + 1], `${arg} requires a method`).toUpperCase();
922
- index += 1;
923
- continue;
924
- }
925
- if (arg.startsWith("-X") && arg.length > 2) {
926
- method = arg.slice(2).toUpperCase();
927
- continue;
928
- }
929
- if (arg === "--body-json" || arg === "--data-json") {
930
- body = parseParamsJson(requireCommandValue(args[index + 1], `${arg} requires a JSON object`), arg);
931
- index += 1;
932
- continue;
933
- }
934
- if (arg === "-F" || arg === "--field") {
935
- addApiField(fields, requireCommandValue(args[index + 1], `${arg} requires key=value`));
936
- index += 1;
937
- continue;
938
- }
939
- if (arg.startsWith("-F") && arg.length > 2) {
940
- addApiField(fields, arg.slice(2));
941
- continue;
942
- }
943
- if (path === undefined) {
944
- path = normalizeApiPath(arg);
945
- continue;
946
- }
947
- throw new CliError(`Unexpected api argument: ${arg}`);
948
- }
949
- if (Object.keys(fields).length > 0) {
950
- body = { ...body ?? {}, ...fields };
951
- if (method === "GET") {
952
- method = "POST";
953
- }
954
- }
955
- return {
956
- path: requireCommandValue(path, "Usage: uru api <path> [-X POST] [-F k=v] [--body-json {...}]"),
957
- method,
958
- ...body === undefined ? {} : { body }
959
- };
960
- }
961
- function normalizeApiPath(value) {
962
- const trimmed = value.trim();
963
- if (trimmed.startsWith("/")) {
964
- return trimmed;
965
- }
966
- return `/api/${trimmed.replace(/^api\//, "")}`;
967
- }
968
- function addApiField(target, raw) {
969
- const equals = raw.indexOf("=");
970
- if (equals <= 0) {
971
- throw new CliError("-F/--field requires key=value");
972
- }
973
- target[raw.slice(0, equals)] = raw.slice(equals + 1);
974
- }
975
- function paramsJson(args) {
976
- for (let index = 0;index < args.length; index += 1) {
977
- const arg = args[index];
978
- if (arg === "--params-json" || arg === "--args-json") {
979
- const value = args[index + 1];
980
- if (value === undefined) {
981
- throw new CliError(`${arg} requires a JSON object`);
982
- }
983
- return parseParamsJson(value, arg);
984
- }
985
- if (arg?.startsWith("--params-json=")) {
986
- return parseParamsJson(arg.slice("--params-json=".length), "--params-json");
987
- }
988
- if (arg?.startsWith("--args-json=")) {
989
- return parseParamsJson(arg.slice("--args-json=".length), "--args-json");
990
- }
991
- }
992
- return {};
993
- }
994
- function parseParamsJson(value, label) {
995
- try {
996
- return parseJsonObject(value, label);
997
- } catch (error) {
998
- throw new CliError(error instanceof Error ? error.message : `Invalid ${label}`, {
999
- cause: error
1000
- });
1001
- }
1002
- }
1003
- function parseLocalFlags(args) {
1004
- const values = {};
1005
- const booleans = new Set;
1006
- const positionals = [];
1007
- for (let index = 0;index < args.length; index += 1) {
1008
- const arg = args[index];
1009
- if (arg === undefined) {
1010
- continue;
1011
- }
1012
- if (!arg.startsWith("--")) {
1013
- positionals.push(arg);
1014
- continue;
1015
- }
1016
- const equals = arg.indexOf("=");
1017
- if (equals > -1) {
1018
- values[arg.slice(0, equals)] = arg.slice(equals + 1);
1019
- continue;
1020
- }
1021
- const next = args[index + 1];
1022
- if (next !== undefined && !next.startsWith("--")) {
1023
- values[arg] = next;
1024
- index += 1;
1025
- } else {
1026
- booleans.add(arg);
1027
- }
1028
- }
1029
- return { values, booleans, positionals };
1030
- }
1031
- function integerFlag(flags, name, fallback) {
1032
- const raw = flags.values[name];
1033
- if (raw === undefined) {
1034
- return fallback;
1035
- }
1036
- const value = Number.parseInt(raw, 10);
1037
- if (!Number.isFinite(value) || value < 0) {
1038
- throw new CliError(`${name} must be a non-negative integer`);
1039
- }
1040
- return value;
1041
- }
1042
- function copyStringFlag(flags, target, flagName, paramName) {
1043
- const value = flags.values[flagName];
1044
- if (value !== undefined) {
1045
- target[paramName] = value;
1046
- }
1047
- }
1048
- function isJsonRequested(argv) {
1049
- for (let index = 0;index < argv.length; index += 1) {
1050
- const arg = argv[index];
1051
- if (arg === "--json") {
1052
- return true;
1053
- }
1054
- if (arg === "--output-format=json" || arg === "--output-format=stream-json" || arg === "--output=json" || arg === "--output=stream-json") {
1055
- return true;
1056
- }
1057
- if (arg === "--output-format" || arg === "--output") {
1058
- const value = argv[index + 1];
1059
- if (value === "json" || value === "stream-json") {
1060
- return true;
1061
- }
1062
- }
1063
- }
1064
- return false;
1065
- }
1066
- function nonEmptyString(value) {
1067
- if (value === undefined) {
1068
- return;
1069
- }
1070
- const trimmed = value.trim();
1071
- return trimmed === "" ? undefined : trimmed;
1072
- }
1073
-
1074
990
  // src/output.ts
1075
991
  function writeJson(io, value) {
1076
992
  io.stdout.write(`${JSON.stringify(value, null, 2)}
@@ -1164,19 +1080,18 @@ CORE
1164
1080
  GEMS
1165
1081
  uru init <library/name.gem> Initialize a Gem project
1166
1082
  uru build [library/name.gem] --dry-run --outdir Build/validate and export build manifest
1167
- uru deploy [library/name.gem] [--prod] [--dry-run] Server-side build/deploy/publish
1168
- uru deploy [library/name.gem] --skip-promote Deploy without promoting the published link
1169
- uru promote <version-id> [library/name.gem] Promote a staged Gem version/deployment
1170
1083
  uru status [library/name.gem] Local source status versus source lock
1171
1084
  uru logs [library/name.gem] [--limit 100] [--follow]
1172
1085
  Runtime logs
1173
- uru open [library/name.gem] Print first published URL
1174
- uru rollback [<version-id>] [library/name.gem] --yes
1175
- Roll back a live Gem link
1086
+ uru open [library/name.gem] --link <id|name> Print one explicit Link URL
1176
1087
  uru gems inspect|validate|build|status [library/name.gem]
1177
- uru gems versions upload [library/name.gem] Create an immutable Gem version
1178
- uru gems versions deploy <version-id> [library/name.gem]
1179
- 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>]
1180
1095
  uru gems read <library/name.gem> <file>
1181
1096
  uru gems write <library/name.gem> <file> --file ./local.html
1182
1097
 
@@ -1328,23 +1243,7 @@ Initialize a Gem project in Library and link the current directory.
1328
1243
  build: `Usage: uru build [library/name.gem] [--dry-run] [--outdir <dir>] [--json]
1329
1244
 
1330
1245
  Validate/build the current Gem through the server-side build pipeline. Use
1331
- --dry-run --outdir to inspect the build manifest without deploying.
1332
- `,
1333
- deploy: `Usage: uru deploy [library/name.gem] [--prod] [--skip-promote] [--no-wait] [--dry-run] [--json]
1334
-
1335
- Build and deploy a Gem through Uru's server-side lifecycle. The backend chooses
1336
- Cloudflare/Fly/fallback from the Gem's compiled runtime contract; the CLI does
1337
- not let users hand-pick providers. Deploy publishes/promotes by default unless
1338
- --skip-promote is supplied. Use --prod --skip-promote for a staged production
1339
- deployment that can be promoted after checks; --prod cannot be combined with
1340
- --no-publish.
1341
- `,
1342
- promote: `Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1343
- uru promote --deployment-id <deployment-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
1344
-
1345
- Promote an already-staged immutable Gem version or ready deployment. With
1346
- --link-id, this is a published-link pointer flip after checks pass. If --link-id
1347
- is omitted, Uru promotes through the default clean Gem link.
1246
+ --dry-run --outdir to inspect the build manifest without creating a Version.
1348
1247
  `,
1349
1248
  pull: `Usage: uru pull [library/name.gem] [--force]
1350
1249
 
@@ -1365,43 +1264,63 @@ Upload local Gem source with source-version CAS. Without a matching lock, push
1365
1264
  fails instead of silently overwriting remote edits. Generated directories,
1366
1265
  node_modules, VCS metadata, and local env files are excluded.
1367
1266
  `,
1368
- 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]
1369
1268
 
1370
- 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,
1371
1270
  the CLI polls for a bounded 5 minutes; text status goes to stderr and log lines
1372
1271
  go to stdout. Use global --output stream-json or --output-format stream-json
1373
1272
  before the command to emit NDJSON events.
1374
1273
  `,
1375
- open: `Usage: uru open [library/name.gem]
1376
-
1377
- Print the first available hosted/published Gem URL.
1378
- `,
1379
- rollback: `Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes
1274
+ open: `Usage: uru open [library/name.gem] [--link <link-id-or-name>]
1380
1275
 
1381
- Rollback retargets a live link to an already-materialized older Gem version or
1382
- deployment after server-side rollback preflight passes. Requires --yes because it
1383
- mutates live traffic. If --link-id is omitted, Uru rolls back through the active
1384
- Primary Gem link.
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.
1385
1278
  `,
1386
- 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> [...]
1387
1280
 
1388
1281
  Dedicated Gem lifecycle and filesystem commands. These are adapters over the
1389
1282
  same platform operations used by MCP and the web UI.
1390
1283
  `,
1391
- "gems versions": `Usage: uru gems versions upload [library/name.gem]
1392
- 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>
1296
+
1297
+ Inspect coherent changes in Edit history. Internal source snapshots are available
1298
+ only through diagnostics and are not a normal lifecycle command.
1299
+ `,
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>
1304
+
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>
1393
1314
 
1394
- Create immutable Gem versions and deploy them through the Gem control plane.
1395
- Upload accepts --message, --tag/--tags, and --preview-alias. Non-100
1396
- percentages are accepted only with --skip-promote to stage rollout intent; live
1397
- promote/rollback requires 100 until weighted link routing is enabled.
1315
+ Each Link independently selects one frozen Version. Changing one Link does not
1316
+ move any other Link.
1398
1317
  `,
1399
- "gems checks": `Usage: uru gems checks <version-id> [library/name.gem] [--deployment-id <id>]
1400
- uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]
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>
1401
1321
 
1402
- Inspect registered publish/deployment checks for an immutable Gem version, or run
1403
- a browser/security check through the Gem control plane. Pass --blocking to make
1404
- the check a release gate; security also accepts --deep.
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.
1405
1324
  `,
1406
1325
  library: `Usage: uru library ls [path]
1407
1326
  uru library search <query>
@@ -1581,7 +1500,14 @@ async function executeOperationStream(ctx, operationId, params, onEvent) {
1581
1500
  if (operation.backendOp !== undefined) {
1582
1501
  toolParams["op"] = operation.backendOp;
1583
1502
  }
1584
- return client(ctx).streamTool(operation.backendToolName, toolParams, onEvent);
1503
+ const response = await client(ctx).streamTool(operation.backendToolName, toolParams, (event) => {
1504
+ assertToolResultSuccess(event);
1505
+ onEvent(event);
1506
+ });
1507
+ if (response.payload !== undefined) {
1508
+ assertToolResultSuccess(response.payload);
1509
+ }
1510
+ return response;
1585
1511
  }
1586
1512
  async function executeOperation(ctx, operationId, params) {
1587
1513
  const operation = getPlatformOperation(operationId);
@@ -1592,7 +1518,9 @@ async function executeOperation(ctx, operationId, params) {
1592
1518
  if (operation.backendOp !== undefined) {
1593
1519
  toolParams["op"] = operation.backendOp;
1594
1520
  }
1595
- return client(ctx).runTool(operation.backendToolName, toolParams);
1521
+ const result = await client(ctx).runTool(operation.backendToolName, toolParams);
1522
+ assertToolResultSuccess(result);
1523
+ return result;
1596
1524
  }
1597
1525
  function emit(ctx, value, text) {
1598
1526
  if (ctx.outputFormat === "stream-json") {
@@ -1604,19 +1532,51 @@ function emit(ctx, value, text) {
1604
1532
  }
1605
1533
  }
1606
1534
  function unwrapToolResult(value) {
1607
- if (isJsonObject2(value) && isJsonObject2(value["result"])) {
1608
- return value["result"];
1535
+ assertToolResultSuccess(value);
1536
+ if (isJsonObject2(value) && Object.hasOwn(value, "result") && (isToolResultEnvelope(value) || isJsonObject2(value["result"]))) {
1537
+ const result = value["result"];
1538
+ if (result !== undefined) {
1539
+ return result;
1540
+ }
1609
1541
  }
1610
1542
  return value;
1611
1543
  }
1544
+ function assertToolResultSuccess(value) {
1545
+ if (!isJsonObject2(value) || !isToolResultEnvelope(value)) {
1546
+ return;
1547
+ }
1548
+ const httpStatus = toolResultHttpStatus(value);
1549
+ const failed = value["success"] === false || Object.hasOwn(value, "error") || Object.hasOwn(value, "errorMessage") || Object.hasOwn(value, "error_message") || Object.hasOwn(value, "errorCode") || Object.hasOwn(value, "error_code") || Object.hasOwn(value, "code") || httpStatus !== undefined && httpStatus >= 400;
1550
+ if (!failed) {
1551
+ return;
1552
+ }
1553
+ const errorCode = stringField(value, ["errorCode", "error_code", "code"]);
1554
+ const toolId = stringField(value, ["toolId", "tool_id"]);
1555
+ const message = stringField(value, ["errorMessage", "error_message", "error", "message"]) ?? `${toolId === undefined ? "Tool operation" : `Tool ${toolId}`} failed`;
1556
+ const details = [
1557
+ errorCode === undefined ? undefined : `[${errorCode}]`,
1558
+ httpStatus === undefined ? undefined : `(HTTP ${httpStatus})`
1559
+ ].filter((detail) => detail !== undefined);
1560
+ const tip = stringField(value, ["tip"]);
1561
+ throw new CliError(`${message}${details.length === 0 ? "" : ` ${details.join(" ")}`}${tip === undefined ? "" : ` ${tip}`}`, {
1562
+ code: httpStatus === 409 ? ExitCode.Conflict : httpStatus === 401 || httpStatus === 403 ? ExitCode.AuthRequired : ExitCode.Failure,
1563
+ ...errorCode === undefined ? {} : { errorCode }
1564
+ });
1565
+ }
1566
+ function isToolResultEnvelope(value) {
1567
+ return typeof value["success"] === "boolean" && (typeof value["toolId"] === "string" || typeof value["tool_id"] === "string" || typeof value["executionTimeMs"] === "number" || typeof value["execution_time_ms"] === "number");
1568
+ }
1569
+ function toolResultHttpStatus(value) {
1570
+ const status = value["httpStatus"] ?? value["http_status"];
1571
+ return typeof status === "number" && Number.isFinite(status) ? status : undefined;
1572
+ }
1612
1573
  function delegatedPayload(value) {
1613
1574
  if (isJsonObject2(value) && isJsonObject2(value["payload"])) {
1614
1575
  return value["payload"];
1615
1576
  }
1616
1577
  return value;
1617
1578
  }
1618
- function gemPathFromArgs(args, linkedProject) {
1619
- const flags = parseLocalFlags(args);
1579
+ function gemPathFromArgs(flags, linkedProject) {
1620
1580
  return flags.values["--path"] ?? flags.values["--gem"] ?? flags.positionals.find((value) => value.startsWith("library/")) ?? linkedProject.libraryPath ?? linkedProject.gemId ?? (() => {
1621
1581
  throw new CliError("Gem path required. Pass library/<name>.gem or run `uru link --path library/<name>.gem`.");
1622
1582
  })();
@@ -1661,43 +1621,6 @@ function stringField(record, keys) {
1661
1621
  }
1662
1622
  return;
1663
1623
  }
1664
- function firstUrl(value) {
1665
- if (typeof value === "string" && /^https?:\/\//.test(value)) {
1666
- return value;
1667
- }
1668
- if (Array.isArray(value)) {
1669
- for (const item of value) {
1670
- const found = firstUrl(item);
1671
- if (found !== undefined) {
1672
- return found;
1673
- }
1674
- }
1675
- return;
1676
- }
1677
- if (!isJsonObject2(value)) {
1678
- return;
1679
- }
1680
- for (const key of [
1681
- "published_url",
1682
- "publishedUrl",
1683
- "preview_url",
1684
- "previewUrl",
1685
- "url",
1686
- "href"
1687
- ]) {
1688
- const candidate = value[key];
1689
- if (typeof candidate === "string" && /^https?:\/\//.test(candidate)) {
1690
- return candidate;
1691
- }
1692
- }
1693
- for (const candidate of Object.values(value)) {
1694
- const found = firstUrl(candidate);
1695
- if (found !== undefined) {
1696
- return found;
1697
- }
1698
- }
1699
- return;
1700
- }
1701
1624
  function isJsonObject2(value) {
1702
1625
  return value !== null && typeof value === "object" && !Array.isArray(value);
1703
1626
  }
@@ -1840,16 +1763,9 @@ function createMacosKeychainCredentialStore(runCommand) {
1840
1763
  },
1841
1764
  async writeToken(apiUrl, token) {
1842
1765
  try {
1843
- await runCommand("security", [
1844
- "add-generic-password",
1845
- "-a",
1846
- accountForApiUrl(apiUrl),
1847
- "-s",
1848
- KEYCHAIN_SERVICE,
1849
- "-w",
1850
- token,
1851
- "-U"
1852
- ]);
1766
+ await runCommand("security", ["-i"], {
1767
+ input: macosKeychainWriteCommand(apiUrl, token)
1768
+ });
1853
1769
  } catch (error) {
1854
1770
  throw credentialStoreError("macOS Keychain", error);
1855
1771
  }
@@ -1871,6 +1787,11 @@ function createMacosKeychainCredentialStore(runCommand) {
1871
1787
  }
1872
1788
  };
1873
1789
  }
1790
+ function macosKeychainWriteCommand(apiUrl, token) {
1791
+ const tokenHex = Buffer.from(token, "utf8").toString("hex");
1792
+ return `add-generic-password -a "${accountForApiUrl(apiUrl)}" -s "${KEYCHAIN_SERVICE}" -X ${tokenHex} -U
1793
+ `;
1794
+ }
1874
1795
  function createLinuxSecretToolCredentialStore(runCommand) {
1875
1796
  return {
1876
1797
  async readToken(apiUrl) {
@@ -2060,7 +1981,6 @@ function windowsCredentialNativePowerShell() {
2060
1981
  Add-Type -TypeDefinition @"
2061
1982
  using System;
2062
1983
  using System.Runtime.InteropServices;
2063
- using System.Runtime.InteropServices.ComTypes;
2064
1984
 
2065
1985
  public static class UruCredentialNative
2066
1986
  {
@@ -2071,7 +1991,7 @@ public static class UruCredentialNative
2071
1991
  public UInt32 Type;
2072
1992
  public string TargetName;
2073
1993
  public string Comment;
2074
- public FILETIME LastWritten;
1994
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
2075
1995
  public UInt32 CredentialBlobSize;
2076
1996
  public IntPtr CredentialBlob;
2077
1997
  public UInt32 Persist;
@@ -2185,6 +2105,12 @@ var ERROR_TIPS = {
2185
2105
  oauth_request_failed: "Check the API URL and network access, then retry `uru login`; use `uru login --device` from SSH/headless environments.",
2186
2106
  oauth_device_failed: "Restart `uru login --device` and complete authorization in the browser before the code expires.",
2187
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.",
2188
2114
  cancelled: "The command was cancelled; rerun it only if you still want the operation to continue."
2189
2115
  };
2190
2116
  function cliErrorJson(error) {
@@ -2256,8 +2182,8 @@ async function readSourceLock(cwd) {
2256
2182
  return {};
2257
2183
  }
2258
2184
  const lock = {};
2259
- if (typeof parsed["sourceVersionId"] === "string") {
2260
- lock.sourceVersionId = parsed["sourceVersionId"];
2185
+ if (typeof parsed["sourceRevisionId"] === "string") {
2186
+ lock.sourceRevisionId = parsed["sourceRevisionId"];
2261
2187
  }
2262
2188
  if (isRecord(parsed["files"])) {
2263
2189
  lock.files = Object.fromEntries(Object.entries(parsed["files"]).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
@@ -2276,7 +2202,7 @@ async function readSourceLock(cwd) {
2276
2202
  async function writeSourceLock(cwd, lock) {
2277
2203
  const path = sourceLockPath(cwd);
2278
2204
  const sanitized = {
2279
- ...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId },
2205
+ ...lock.sourceRevisionId === undefined ? {} : { sourceRevisionId: lock.sourceRevisionId },
2280
2206
  files: stableStringRecord(lock.files ?? {}),
2281
2207
  ...lock.checkedOutAt === undefined ? {} : { checkedOutAt: lock.checkedOutAt }
2282
2208
  };
@@ -2369,7 +2295,8 @@ var SKIPPED_SOURCE_PATH_SEGMENTS = new Set([
2369
2295
  "build",
2370
2296
  "coverage",
2371
2297
  "tmp",
2372
- "temp"
2298
+ "temp",
2299
+ "__MACOSX"
2373
2300
  ]);
2374
2301
  var SKIPPED_SOURCE_FILE_NAMES = new Set([
2375
2302
  ".DS_Store",
@@ -2382,7 +2309,8 @@ var SKIPPED_SOURCE_FILE_NAMES = new Set([
2382
2309
  ]);
2383
2310
  function shouldSkipProjectPath(path) {
2384
2311
  const segments = path.split("/");
2385
- return segments.some((segment) => SKIPPED_SOURCE_PATH_SEGMENTS.has(segment)) || SKIPPED_SOURCE_FILE_NAMES.has(segments.at(-1) ?? "");
2312
+ const name = segments.at(-1) ?? "";
2313
+ return segments.some((segment) => SKIPPED_SOURCE_PATH_SEGMENTS.has(segment)) || SKIPPED_SOURCE_FILE_NAMES.has(name) || name.startsWith("._");
2386
2314
  }
2387
2315
  function stableStringRecord(record) {
2388
2316
  return Object.fromEntries(Object.entries(record).filter(([, value]) => typeof value === "string" && value.trim() !== "").sort(([left], [right]) => compareStrings(left, right)));
@@ -2437,11 +2365,12 @@ async function maybeNotifyCliUpdate(ctx, options = {}) {
2437
2365
  if (!shouldCheckForUpdate(ctx, options.env ?? process.env)) {
2438
2366
  return;
2439
2367
  }
2368
+ const currentConfig = await readConfigBestEffort(ctx);
2440
2369
  const now = options.now?.() ?? Date.now();
2441
- if (!isUpdateCheckDue(ctx.config, now)) {
2370
+ if (!isUpdateCheckDue(currentConfig, now)) {
2442
2371
  return;
2443
2372
  }
2444
- const checkedConfig = withUpdateCheckTimestamp(ctx.config, now);
2373
+ const checkedConfig = withUpdateCheckTimestamp(currentConfig, now);
2445
2374
  await writeConfigBestEffort(ctx, checkedConfig);
2446
2375
  const latestVersion = await fetchLatestVersion({
2447
2376
  fetchImpl: ctx.fetch ?? fetch,
@@ -2463,6 +2392,13 @@ async function maybeNotifyCliUpdate(ctx, options = {}) {
2463
2392
  ctx.io.stderr.write(`A newer Uru CLI is available: ${CLI_VERSION} → ${latestVersion}. ` + `Run npm install -g ${PACKAGE_NAME}@latest
2464
2393
  `);
2465
2394
  }
2395
+ async function readConfigBestEffort(ctx) {
2396
+ try {
2397
+ return await ctx.store.read();
2398
+ } catch {
2399
+ return ctx.config;
2400
+ }
2401
+ }
2466
2402
  function shouldCheckForUpdate(ctx, env) {
2467
2403
  return ctx.outputFormat === "text" && ctx.terminal.stderrIsTty && !ctx.terminal.ci && env["NO_UPDATE_NOTIFIER"] === undefined && env["URU_NO_UPDATE_NOTIFIER"] === undefined;
2468
2404
  }
@@ -2542,33 +2478,219 @@ function isRecord2(value) {
2542
2478
  return value !== null && typeof value === "object" && !Array.isArray(value);
2543
2479
  }
2544
2480
 
2545
- // src/auth-commands.ts
2546
- import { readFile as readFile3 } from "node:fs/promises";
2547
- import { createServer } from "node:http";
2548
-
2549
- // src/oauth.ts
2550
- import { createHash as createHash2, randomBytes } from "node:crypto";
2551
- var DEFAULT_PKCE_VERIFIER_BYTES = 32;
2552
- var DEFAULT_STATE_BYTES = 24;
2553
- var OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2554
- var DEVICE_POLL_SLOW_DOWN_SECONDS = 5;
2555
- function generatePkcePair() {
2556
- const verifier = tokenUrlSafe(DEFAULT_PKCE_VERIFIER_BYTES);
2557
- return {
2558
- verifier,
2559
- challenge: pkceChallengeS256(verifier),
2560
- method: "S256"
2561
- };
2562
- }
2563
- function generateOAuthState() {
2564
- return tokenUrlSafe(DEFAULT_STATE_BYTES);
2565
- }
2566
- function pkceChallengeS256(verifier) {
2567
- if (!isValidPkceVerifier(verifier)) {
2568
- throw new CliError("PKCE code verifier must be 43-128 RFC7636 unreserved characters.");
2569
- }
2570
- return createHash2("sha256").update(verifier).digest("base64url");
2571
- }
2481
+ // src/cli-helpers.ts
2482
+ var BOOLEAN_LOCAL_FLAGS = new Set([
2483
+ "--blocking",
2484
+ "--create",
2485
+ "--deep",
2486
+ "--dry-run",
2487
+ "--follow",
2488
+ "--force",
2489
+ "--force-build",
2490
+ "--hard-delete",
2491
+ "--no-create",
2492
+ "--no-wait",
2493
+ "--permanent",
2494
+ "--prod",
2495
+ "--stdin",
2496
+ "--value-stdin",
2497
+ "--yes"
2498
+ ]);
2499
+ function parseApiArgs(args) {
2500
+ let method = "GET";
2501
+ let path;
2502
+ let body;
2503
+ const fields = {};
2504
+ for (let index = 0;index < args.length; index += 1) {
2505
+ const arg = args[index];
2506
+ if (arg === undefined) {
2507
+ continue;
2508
+ }
2509
+ if (arg === "-X" || arg === "--method") {
2510
+ method = requireCommandValue(args[index + 1], `${arg} requires a method`).toUpperCase();
2511
+ index += 1;
2512
+ continue;
2513
+ }
2514
+ if (arg.startsWith("-X") && arg.length > 2) {
2515
+ method = arg.slice(2).toUpperCase();
2516
+ continue;
2517
+ }
2518
+ if (arg === "--body-json" || arg === "--data-json") {
2519
+ body = parseParamsJson(requireCommandValue(args[index + 1], `${arg} requires a JSON object`), arg);
2520
+ index += 1;
2521
+ continue;
2522
+ }
2523
+ if (arg === "-F" || arg === "--field") {
2524
+ addApiField(fields, requireCommandValue(args[index + 1], `${arg} requires key=value`));
2525
+ index += 1;
2526
+ continue;
2527
+ }
2528
+ if (arg.startsWith("-F") && arg.length > 2) {
2529
+ addApiField(fields, arg.slice(2));
2530
+ continue;
2531
+ }
2532
+ if (path === undefined) {
2533
+ path = normalizeApiPath(arg);
2534
+ continue;
2535
+ }
2536
+ throw new CliError(`Unexpected api argument: ${arg}`);
2537
+ }
2538
+ if (Object.keys(fields).length > 0) {
2539
+ body = { ...body ?? {}, ...fields };
2540
+ if (method === "GET") {
2541
+ method = "POST";
2542
+ }
2543
+ }
2544
+ return {
2545
+ path: requireCommandValue(path, "Usage: uru api <path> [-X POST] [-F k=v] [--body-json {...}]"),
2546
+ method,
2547
+ ...body === undefined ? {} : { body }
2548
+ };
2549
+ }
2550
+ function normalizeApiPath(value) {
2551
+ const trimmed = value.trim();
2552
+ if (trimmed.startsWith("/")) {
2553
+ return trimmed;
2554
+ }
2555
+ return `/api/${trimmed.replace(/^api\//, "")}`;
2556
+ }
2557
+ function addApiField(target, raw) {
2558
+ const equals = raw.indexOf("=");
2559
+ if (equals <= 0) {
2560
+ throw new CliError("-F/--field requires key=value");
2561
+ }
2562
+ target[raw.slice(0, equals)] = raw.slice(equals + 1);
2563
+ }
2564
+ function paramsJson(args) {
2565
+ for (let index = 0;index < args.length; index += 1) {
2566
+ const arg = args[index];
2567
+ if (arg === "--params-json" || arg === "--args-json") {
2568
+ const value = args[index + 1];
2569
+ if (value === undefined) {
2570
+ throw new CliError(`${arg} requires a JSON object`);
2571
+ }
2572
+ return parseParamsJson(value, arg);
2573
+ }
2574
+ if (arg?.startsWith("--params-json=")) {
2575
+ return parseParamsJson(arg.slice("--params-json=".length), "--params-json");
2576
+ }
2577
+ if (arg?.startsWith("--args-json=")) {
2578
+ return parseParamsJson(arg.slice("--args-json=".length), "--args-json");
2579
+ }
2580
+ }
2581
+ return {};
2582
+ }
2583
+ function parseParamsJson(value, label) {
2584
+ try {
2585
+ return parseJsonObject(value, label);
2586
+ } catch (error) {
2587
+ throw new CliError(error instanceof Error ? error.message : `Invalid ${label}`, {
2588
+ cause: error
2589
+ });
2590
+ }
2591
+ }
2592
+ function parseLocalFlags(args) {
2593
+ const values = {};
2594
+ const booleans = new Set;
2595
+ const positionals = [];
2596
+ for (let index = 0;index < args.length; index += 1) {
2597
+ const arg = args[index];
2598
+ if (arg === undefined) {
2599
+ continue;
2600
+ }
2601
+ if (!arg.startsWith("--")) {
2602
+ positionals.push(arg);
2603
+ continue;
2604
+ }
2605
+ const equals = arg.indexOf("=");
2606
+ if (equals > -1) {
2607
+ values[arg.slice(0, equals)] = arg.slice(equals + 1);
2608
+ continue;
2609
+ }
2610
+ if (BOOLEAN_LOCAL_FLAGS.has(arg)) {
2611
+ booleans.add(arg);
2612
+ continue;
2613
+ }
2614
+ const next = args[index + 1];
2615
+ if (next !== undefined && !next.startsWith("--")) {
2616
+ values[arg] = next;
2617
+ index += 1;
2618
+ } else {
2619
+ booleans.add(arg);
2620
+ }
2621
+ }
2622
+ return { values, booleans, positionals };
2623
+ }
2624
+ function integerFlag(flags, name, fallback) {
2625
+ const raw = flags.values[name];
2626
+ if (raw === undefined) {
2627
+ return fallback;
2628
+ }
2629
+ const value = Number.parseInt(raw, 10);
2630
+ if (!Number.isFinite(value) || value < 0) {
2631
+ throw new CliError(`${name} must be a non-negative integer`);
2632
+ }
2633
+ return value;
2634
+ }
2635
+ function copyStringFlag(flags, target, flagName, paramName) {
2636
+ const value = flags.values[flagName];
2637
+ if (value !== undefined) {
2638
+ target[paramName] = value;
2639
+ }
2640
+ }
2641
+ function isJsonRequested(argv) {
2642
+ for (let index = 0;index < argv.length; index += 1) {
2643
+ const arg = argv[index];
2644
+ if (arg === "--json") {
2645
+ return true;
2646
+ }
2647
+ if (arg === "--output-format=json" || arg === "--output-format=stream-json" || arg === "--output=json" || arg === "--output=stream-json") {
2648
+ return true;
2649
+ }
2650
+ if (arg === "--output-format" || arg === "--output") {
2651
+ const value = argv[index + 1];
2652
+ if (value === "json" || value === "stream-json") {
2653
+ return true;
2654
+ }
2655
+ }
2656
+ }
2657
+ return false;
2658
+ }
2659
+ function nonEmptyString(value) {
2660
+ if (value === undefined) {
2661
+ return;
2662
+ }
2663
+ const trimmed = value.trim();
2664
+ return trimmed === "" ? undefined : trimmed;
2665
+ }
2666
+
2667
+ // src/auth-commands.ts
2668
+ import { readFile as readFile3 } from "node:fs/promises";
2669
+ import { createServer } from "node:http";
2670
+
2671
+ // src/oauth.ts
2672
+ import { createHash as createHash2, randomBytes } from "node:crypto";
2673
+ var DEFAULT_PKCE_VERIFIER_BYTES = 32;
2674
+ var DEFAULT_STATE_BYTES = 24;
2675
+ var OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
2676
+ var DEVICE_POLL_SLOW_DOWN_SECONDS = 5;
2677
+ function generatePkcePair() {
2678
+ const verifier = tokenUrlSafe(DEFAULT_PKCE_VERIFIER_BYTES);
2679
+ return {
2680
+ verifier,
2681
+ challenge: pkceChallengeS256(verifier),
2682
+ method: "S256"
2683
+ };
2684
+ }
2685
+ function generateOAuthState() {
2686
+ return tokenUrlSafe(DEFAULT_STATE_BYTES);
2687
+ }
2688
+ function pkceChallengeS256(verifier) {
2689
+ if (!isValidPkceVerifier(verifier)) {
2690
+ throw new CliError("PKCE code verifier must be 43-128 RFC7636 unreserved characters.");
2691
+ }
2692
+ return createHash2("sha256").update(verifier).digest("base64url");
2693
+ }
2572
2694
  function buildLoopbackRedirectUri(args) {
2573
2695
  const host = args.host.trim();
2574
2696
  if (host === "") {
@@ -2641,6 +2763,19 @@ async function registerOAuthClient(args) {
2641
2763
  scope: optionalStringField(response, "scope") ?? "mcp"
2642
2764
  };
2643
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
+ }
2644
2779
  async function exchangeAuthorizationCode(args) {
2645
2780
  const body = new URLSearchParams;
2646
2781
  body.set("grant_type", "authorization_code");
@@ -2659,6 +2794,33 @@ async function exchangeAuthorizationCode(args) {
2659
2794
  });
2660
2795
  return normalizeOAuthTokenResponse(response);
2661
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
+ }
2662
2824
  async function exchangeOAuthForCliSession(args) {
2663
2825
  const body = {};
2664
2826
  if (args.workspaceId !== undefined && args.workspaceId.trim() !== "") {
@@ -2683,6 +2845,7 @@ async function exchangeOAuthForCliSession(args) {
2683
2845
  accessToken: stringField2(response, "access_token"),
2684
2846
  tokenType,
2685
2847
  expiresIn: numberField(response, "expires_in"),
2848
+ oauthResource: stringField2(response, "oauth_resource"),
2686
2849
  ...workspaceId === undefined ? {} : { workspaceId }
2687
2850
  };
2688
2851
  }
@@ -2760,6 +2923,18 @@ async function fetchOAuthJson(args) {
2760
2923
  }
2761
2924
  return payload;
2762
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
+ }
2763
2938
  function tokenUrlSafe(byteLength) {
2764
2939
  return randomBytes(byteLength).toString("base64url");
2765
2940
  }
@@ -2778,10 +2953,7 @@ async function fetchOAuthForm(args) {
2778
2953
  });
2779
2954
  const payload = parseJsonResponse(await response.text());
2780
2955
  if (!response.ok && !(args.allowOAuthError === true && isOAuthError(payload))) {
2781
- throw new CliError(errorMessage2(payload) ?? `OAuth request failed: HTTP ${response.status}`, {
2782
- code: response.status === 401 || response.status === 403 ? ExitCode.AuthRequired : ExitCode.Failure,
2783
- errorCode: "oauth_request_failed"
2784
- });
2956
+ throw oauthHttpError(response.status, payload, args.rejectedGrantErrorCode);
2785
2957
  }
2786
2958
  return payload;
2787
2959
  }
@@ -2836,14 +3008,47 @@ function normalizeOAuthTokenResponse(value) {
2836
3008
  const refreshToken = optionalStringField(value, "refresh_token");
2837
3009
  const scope = optionalStringField(value, "scope");
2838
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
+ }
2839
3018
  return {
2840
3019
  accessToken: stringField2(value, "access_token"),
2841
- tokenType: stringField2(value, "token_type"),
3020
+ tokenType,
2842
3021
  ...typeof expiresInRaw === "number" ? { expiresIn: expiresInRaw } : {},
2843
3022
  ...refreshToken === undefined ? {} : { refreshToken },
2844
3023
  ...scope === undefined ? {} : { scope }
2845
3024
  };
2846
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
+ }
2847
3052
  function stringField2(value, key) {
2848
3053
  const field = value[key];
2849
3054
  if (typeof field !== "string" || field.trim() === "") {
@@ -2880,12 +3085,203 @@ function defaultSleep(milliseconds) {
2880
3085
  return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
2881
3086
  }
2882
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
+
2883
3189
  // src/auth-commands.ts
2884
3190
  var OAUTH_CALLBACK_PATH = "/oauth/callback";
2885
3191
  var OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
2886
3192
  var DEVICE_MAX_POLLS = 120;
2887
- async function resolveStoredToken(credentialStore, apiUrl, config) {
2888
- 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
+ }
2889
3285
  }
2890
3286
  async function resolveFlagToken(flags, stdin) {
2891
3287
  if (flags.token !== undefined) {
@@ -2919,20 +3315,26 @@ async function resolveFlagToken(flags, stdin) {
2919
3315
  }
2920
3316
  async function login(ctx, options = {}) {
2921
3317
  const existingToken = ctx.token === undefined || ctx.token.trim() === "" ? undefined : ctx.token;
2922
- 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
+ };
2923
3324
  let result;
2924
3325
  try {
2925
- result = await validateSessionToken(ctx, token);
3326
+ result = await validateSessionToken(ctx, loginToken.accessToken);
2926
3327
  } catch (error) {
2927
3328
  const storedTokenIsStale = existingToken !== undefined && !ctx.tokenIsExplicit && isAuthRequiredError(error);
2928
3329
  if (!storedTokenIsStale) {
2929
3330
  throw error;
2930
3331
  }
2931
- token = await obtainLoginSessionToken(ctx);
2932
- result = await validateSessionToken(ctx, token);
3332
+ loginToken = await obtainLoginSessionToken(ctx);
3333
+ usedExistingToken = false;
3334
+ result = await validateSessionToken(ctx, loginToken.accessToken);
2933
3335
  }
2934
3336
  const selectedWorkspace = selectWorkspace(result, ctx.workspace);
2935
- await persistLoginToken(ctx, token, selectedWorkspace);
3337
+ await persistLoginToken(ctx, loginToken.storedValue, selectedWorkspace);
2936
3338
  if (options.silentSuccess === true) {
2937
3339
  return;
2938
3340
  }
@@ -2945,12 +3347,15 @@ async function login(ctx, options = {}) {
2945
3347
  workspaceId: selectedWorkspace,
2946
3348
  workspaceSelectionRequired: result.workspaceSelectionRequired,
2947
3349
  storage: ctx.insecureStorage ? "insecure_file" : "keychain",
2948
- auth: ctx.token === token ? "token" : ctx.device ? "device_oauth" : "browser_oauth"
3350
+ auth: usedExistingToken ? "token" : ctx.device ? "device_oauth" : "browser_oauth"
2949
3351
  });
2950
3352
  } else {
2951
3353
  writeText(ctx.io, ctx.insecureStorage ? "Logged in (token stored in local config)" : "Logged in");
2952
3354
  }
2953
3355
  }
3356
+ async function readStoredLoginValue(ctx) {
3357
+ return await ctx.credentialStore.readToken(ctx.apiUrl) ?? (ctx.config.insecureStorage === true ? ctx.config.token : undefined);
3358
+ }
2954
3359
  async function validateSessionToken(ctx, token) {
2955
3360
  const loginClient = new UruApiClient({
2956
3361
  apiUrl: ctx.apiUrl,
@@ -2963,6 +3368,9 @@ async function validateSessionToken(ctx, token) {
2963
3368
  function isAuthRequiredError(error) {
2964
3369
  return error instanceof CliError && error.exitCode === ExitCode.AuthRequired;
2965
3370
  }
3371
+ function isStoredRefreshRejected(error) {
3372
+ return error instanceof CliError && error.errorCode === "oauth_refresh_invalid";
3373
+ }
2966
3374
  async function readAllStdin(input) {
2967
3375
  const chunks = [];
2968
3376
  for await (const chunk of input) {
@@ -2980,6 +3388,10 @@ async function obtainLoginSessionToken(ctx) {
2980
3388
  return loginWithBrowser(ctx);
2981
3389
  }
2982
3390
  async function loginWithDeviceCode(ctx) {
3391
+ const resource = await discoverOAuthResource({
3392
+ apiUrl: ctx.apiUrl,
3393
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
3394
+ });
2983
3395
  const clientRegistration = await registerOAuthClient({
2984
3396
  apiUrl: ctx.apiUrl,
2985
3397
  redirectUri: buildLoopbackRedirectUri({
@@ -2998,6 +3410,7 @@ async function loginWithDeviceCode(ctx) {
2998
3410
  apiUrl: ctx.apiUrl,
2999
3411
  clientId: clientRegistration.clientId,
3000
3412
  scope: clientRegistration.scope,
3413
+ resource,
3001
3414
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3002
3415
  });
3003
3416
  const complete = authorization.verificationUriComplete ?? authorization.verificationUri;
@@ -3014,7 +3427,7 @@ async function loginWithDeviceCode(ctx) {
3014
3427
  maxPolls: Math.min(DEVICE_MAX_POLLS, Math.ceil(authorization.expiresIn / Math.max(1, authorization.interval))),
3015
3428
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3016
3429
  });
3017
- return exchangeOAuthTokenForSession(ctx, token);
3430
+ return exchangeOAuthTokenForSession(ctx, token, clientRegistration.clientId, resource);
3018
3431
  }
3019
3432
  async function loginWithBrowser(ctx) {
3020
3433
  const callbackHost = ctx.callbackHost;
@@ -3023,6 +3436,10 @@ async function loginWithBrowser(ctx) {
3023
3436
  ...ctx.callbackPort === undefined ? {} : { port: ctx.callbackPort }
3024
3437
  });
3025
3438
  try {
3439
+ const resource = await discoverOAuthResource({
3440
+ apiUrl: ctx.apiUrl,
3441
+ ...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
3442
+ });
3026
3443
  const redirectUri = buildLoopbackRedirectUri({
3027
3444
  host: callbackHost,
3028
3445
  port: callback.port,
@@ -3041,7 +3458,8 @@ async function loginWithBrowser(ctx) {
3041
3458
  redirectUri,
3042
3459
  scope: clientRegistration.scope,
3043
3460
  state,
3044
- codeChallenge: pkce.challenge
3461
+ codeChallenge: pkce.challenge,
3462
+ resource
3045
3463
  });
3046
3464
  await maybeCopyLoginUrl(ctx, authorizationUrl);
3047
3465
  if (ctx.noOpen) {
@@ -3060,21 +3478,40 @@ async function loginWithBrowser(ctx) {
3060
3478
  code: authorization.code,
3061
3479
  redirectUri,
3062
3480
  codeVerifier: pkce.verifier,
3481
+ resource,
3063
3482
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3064
3483
  });
3065
- return exchangeOAuthTokenForSession(ctx, token);
3484
+ return exchangeOAuthTokenForSession(ctx, token, clientRegistration.clientId, resource);
3066
3485
  } finally {
3067
3486
  callback.close();
3068
3487
  }
3069
3488
  }
3070
- async function exchangeOAuthTokenForSession(ctx, token) {
3489
+ async function exchangeOAuthTokenForSession(ctx, token, clientId, resource) {
3490
+ const refreshToken = requireRefreshToken(token);
3071
3491
  const session = await exchangeOAuthForCliSession({
3072
3492
  apiUrl: ctx.apiUrl,
3073
3493
  oauthAccessToken: token.accessToken,
3074
3494
  ...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
3075
3495
  ...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
3076
3496
  });
3077
- 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
+ };
3078
3515
  }
3079
3516
  async function persistLoginToken(ctx, token, selectedWorkspace) {
3080
3517
  if (ctx.insecureStorage) {
@@ -3101,6 +3538,40 @@ async function persistLoginToken(ctx, token, selectedWorkspace) {
3101
3538
  };
3102
3539
  await ctx.store.write(nextConfig);
3103
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
+ }
3104
3575
  async function maybeCopyLoginUrl(ctx, url) {
3105
3576
  if (!ctx.clipboard) {
3106
3577
  return;
@@ -3204,6 +3675,7 @@ async function switchWorkspace(ctx, workspaceId) {
3204
3675
  // src/local-commands.ts
3205
3676
  import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
3206
3677
  import { dirname as dirname3, resolve as resolve2 } from "node:path";
3678
+ var DEFAULT_MCP_PACKAGE = package_default.uru.mcpPackage;
3207
3679
  var GENERIC_FAMILY_COMMANDS = {
3208
3680
  workspace: "workspace",
3209
3681
  sharing: "sharing",
@@ -3232,15 +3704,12 @@ var TOP_LEVEL_COMMANDS = [
3232
3704
  "tools",
3233
3705
  "init",
3234
3706
  "build",
3235
- "deploy",
3236
- "promote",
3237
3707
  "pull",
3238
3708
  "status",
3239
3709
  "diff",
3240
3710
  "push",
3241
3711
  "logs",
3242
3712
  "open",
3243
- "rollback",
3244
3713
  "gems",
3245
3714
  "automations",
3246
3715
  "library",
@@ -3333,7 +3802,7 @@ async function dispatchMcp(ctx, sub, args) {
3333
3802
  const config = mcpServerConfig({
3334
3803
  apiKey: flags.values["--api-key"] ?? ctx.token,
3335
3804
  serverName: flags.values["--server-name"] ?? "uru",
3336
- packageName: flags.values["--package"] ?? "uru-mcp@latest"
3805
+ packageName: flags.values["--package"] ?? DEFAULT_MCP_PACKAGE
3337
3806
  });
3338
3807
  if (sub === "config") {
3339
3808
  writeJson(ctx.io, config);
@@ -3697,7 +4166,7 @@ function mcpServerConfig(args) {
3697
4166
  mcpServers: {
3698
4167
  [args.serverName]: {
3699
4168
  command: "npx",
3700
- args: [args.packageName],
4169
+ args: ["-y", args.packageName],
3701
4170
  env: { URU_API_KEY: apiKey }
3702
4171
  }
3703
4172
  }
@@ -3716,38 +4185,380 @@ function familyPrefix(family) {
3716
4185
  if (family === "secrets") {
3717
4186
  return "secret";
3718
4187
  }
3719
- if (family === "tokens") {
3720
- return "token";
4188
+ if (family === "tokens") {
4189
+ return "token";
4190
+ }
4191
+ return family;
4192
+ }
4193
+ function isLikelyCommand(candidate, value) {
4194
+ if (value === "") {
4195
+ return false;
4196
+ }
4197
+ return candidate.startsWith(value) || value.startsWith(candidate) || levenshtein(candidate, value) <= 2;
4198
+ }
4199
+ function levenshtein(left, right) {
4200
+ const previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
4201
+ for (let leftIndex = 0;leftIndex < left.length; leftIndex += 1) {
4202
+ let diagonal = previous[0] ?? 0;
4203
+ previous[0] = leftIndex + 1;
4204
+ for (let rightIndex = 0;rightIndex < right.length; rightIndex += 1) {
4205
+ const upper = previous[rightIndex + 1] ?? 0;
4206
+ const cost = left[leftIndex] === right[rightIndex] ? 0 : 1;
4207
+ previous[rightIndex + 1] = Math.min(upper + 1, (previous[rightIndex] ?? 0) + 1, diagonal + cost);
4208
+ diagonal = upper;
4209
+ }
4210
+ }
4211
+ return previous[right.length] ?? 0;
4212
+ }
4213
+
4214
+ // src/gem-lifecycle-commands.ts
4215
+ import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
4216
+ import { setTimeout as sleep } from "node:timers/promises";
4217
+ import { join as join3, resolve as resolve3 } from "node:path";
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;
4224
+ }
4225
+ if (sub === "edits") {
4226
+ await dispatchGemEdits(ctx, args, linkedProject);
4227
+ return true;
4228
+ }
4229
+ if (sub === "releases") {
4230
+ await dispatchGemReleases(ctx, args, linkedProject);
4231
+ return true;
4232
+ }
4233
+ if (sub === "links") {
4234
+ await dispatchGemLinks(ctx, args, linkedProject);
4235
+ return true;
4236
+ }
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);
4247
+ return;
4248
+ }
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);
4257
+ return;
4258
+ }
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);
4264
+ return;
4265
+ }
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
+ });
4274
+ return;
4275
+ }
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>");
4277
+ }
4278
+ async function gemVersionSave(ctx, args, linkedProject) {
4279
+ const flags = parseLocalFlags(args);
4280
+ assertAllowedLocalFlags(flags, ["--gem", "--idempotency-key", "--message", "--name", "--path"], "uru gems versions save");
4281
+ const gemPath = gemPathFromArgs(flags, linkedProject);
4282
+ const params = { path: gemPath };
4283
+ copyStringFlag(flags, params, "--name", "name");
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;
4302
+ }
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;
4310
+ }
4311
+ throw new CliError("Usage: uru gems edits list <gem> | show <gem> <edit>");
4312
+ }
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}`);
4487
+ }
4488
+ }
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);
4501
+ }
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`);
3721
4530
  }
3722
- return family;
4531
+ params[key] = value;
3723
4532
  }
3724
- function isLikelyCommand(candidate, value) {
3725
- if (value === "") {
3726
- return false;
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(", ")}.`);
3727
4538
  }
3728
- return candidate.startsWith(value) || value.startsWith(candidate) || levenshtein(candidate, value) <= 2;
3729
4539
  }
3730
- function levenshtein(left, right) {
3731
- const previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
3732
- for (let leftIndex = 0;leftIndex < left.length; leftIndex += 1) {
3733
- let diagonal = previous[0] ?? 0;
3734
- previous[0] = leftIndex + 1;
3735
- for (let rightIndex = 0;rightIndex < right.length; rightIndex += 1) {
3736
- const upper = previous[rightIndex + 1] ?? 0;
3737
- const cost = left[leftIndex] === right[rightIndex] ? 0 : 1;
3738
- previous[rightIndex + 1] = Math.min(upper + 1, (previous[rightIndex] ?? 0) + 1, diagonal + cost);
3739
- diagonal = upper;
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
+ }
3740
4547
  }
3741
4548
  }
3742
- return previous[right.length] ?? 0;
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;
3743
4556
  }
3744
4557
 
3745
4558
  // src/gem-lifecycle-commands.ts
3746
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
3747
- import { setTimeout as sleep } from "node:timers/promises";
3748
- import { join as join3, resolve as resolve3 } from "node:path";
3749
4559
  async function dispatchGems(ctx, sub, args, linkedProject) {
3750
- const gemPath = () => gemPathFromArgs(args, linkedProject);
4560
+ const flags = parseLocalFlags(args);
4561
+ const gemPath = () => gemPathFromArgs(flags, linkedProject);
3751
4562
  if (sub === "inspect") {
3752
4563
  await emitGemTool(ctx, "gem.inspect", { path: gemPath() });
3753
4564
  return;
@@ -3760,12 +4571,19 @@ async function dispatchGems(ctx, sub, args, linkedProject) {
3760
4571
  await gemBuild(ctx, args, linkedProject);
3761
4572
  return;
3762
4573
  }
3763
- if (sub === "versions") {
3764
- await dispatchGemVersions(ctx, args, linkedProject);
4574
+ if (await dispatchGemLifecycleResourceCommand(ctx, sub, args, linkedProject)) {
3765
4575
  return;
3766
4576
  }
3767
4577
  if (sub === "checks") {
3768
- await gemChecks(ctx, args, linkedProject);
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);
3769
4587
  return;
3770
4588
  }
3771
4589
  if (sub === "status") {
@@ -3783,7 +4601,7 @@ async function dispatchGems(ctx, sub, args, linkedProject) {
3783
4601
  await gemWrite(ctx, args);
3784
4602
  return;
3785
4603
  }
3786
- throw new CliError("Usage: uru gems inspect|validate|build|versions|checks|status|read|write");
4604
+ throw new CliError("Usage: uru gems inspect|validate|build|edits|versions|releases|links|runs|checks|status|read|write");
3787
4605
  }
3788
4606
  async function gemInit(ctx, args) {
3789
4607
  const flags = parseLocalFlags(args);
@@ -3802,67 +4620,10 @@ async function gemInit(ctx, args) {
3802
4620
  }
3803
4621
  await emitGemTool(ctx, "gem.init", params);
3804
4622
  }
3805
- async function gemDeploy(ctx, args, linkedProject) {
3806
- const flags = parseLocalFlags(args);
3807
- 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.");
3808
- assertAllowedLocalFlags(flags, [
3809
- "--build-id",
3810
- "--dry-run",
3811
- "--force-build",
3812
- "--gem",
3813
- "--link-id",
3814
- "--no-publish",
3815
- "--no-wait",
3816
- "--path",
3817
- "--prod",
3818
- "--skip-promote",
3819
- "--source-version-id"
3820
- ], "uru deploy");
3821
- if (flags.booleans.has("--prod") && flags.booleans.has("--no-publish")) {
3822
- throw new CliError("uru deploy --prod cannot be combined with --no-publish. Use --skip-promote for a staged production deployment.");
3823
- }
3824
- if (flags.booleans.has("--dry-run")) {
3825
- const buildArgs = [...flags.positionals, "--dry-run"];
3826
- if (flags.booleans.has("--force-build")) {
3827
- buildArgs.push("--force");
3828
- }
3829
- const sourceVersionId = flags.values["--source-version-id"];
3830
- if (sourceVersionId !== undefined) {
3831
- buildArgs.push("--source-version-id", sourceVersionId);
3832
- }
3833
- await gemBuild(ctx, buildArgs, linkedProject);
3834
- return;
3835
- }
3836
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
3837
- const publish = !flags.booleans.has("--no-publish") && !flags.booleans.has("--skip-promote");
3838
- const params = {
3839
- path: gemPath,
3840
- publish
3841
- };
3842
- if (flags.booleans.has("--prod")) {
3843
- params["metadata"] = {
3844
- deployment_intent: "production",
3845
- promotion_strategy: publish ? "immediate" : "staged"
3846
- };
3847
- }
3848
- copyStringFlag(flags, params, "--build-id", "build_id");
3849
- copyStringFlag(flags, params, "--link-id", "link_id");
3850
- copyStringFlag(flags, params, "--source-version-id", "source_version_id");
3851
- if (flags.booleans.has("--force-build")) {
3852
- params["force_build"] = true;
3853
- }
3854
- if (flags.booleans.has("--no-wait")) {
3855
- params["wait"] = false;
3856
- }
3857
- await emitGemTool(ctx, "gem.deploy", params, {
3858
- streamStart: { operation: "gem.deploy", path: gemPath },
3859
- textExtractor: extractDeploymentUrl
3860
- });
3861
- }
3862
4623
  async function gemBuild(ctx, args, linkedProject) {
3863
4624
  const flags = parseLocalFlags(args);
3864
- assertAllowedLocalFlags(flags, ["--dry-run", "--force", "--gem", "--outdir", "--path", "--source-version-id"], "uru build");
3865
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
4625
+ assertAllowedLocalFlags2(flags, ["--dry-run", "--force", "--gem", "--outdir", "--path", "--source-revision-id"], "uru build");
4626
+ const gemPath = gemPathFromArgs(flags, linkedProject);
3866
4627
  const params = {
3867
4628
  path: gemPath
3868
4629
  };
@@ -3872,7 +4633,7 @@ async function gemBuild(ctx, args, linkedProject) {
3872
4633
  if (flags.booleans.has("--dry-run")) {
3873
4634
  params["dry_run"] = true;
3874
4635
  }
3875
- copyStringFlag(flags, params, "--source-version-id", "source_version_id");
4636
+ copyStringFlag(flags, params, "--source-revision-id", "source_revision_id");
3876
4637
  if (ctx.outputFormat === "stream-json") {
3877
4638
  writeStreamJson(ctx.io, "started", { operation: "gem.build", path: gemPath });
3878
4639
  writeStreamJson(ctx.io, "phase", {
@@ -3895,63 +4656,9 @@ async function gemBuild(ctx, args, linkedProject) {
3895
4656
  writeText(ctx.io, JSON.stringify(unwrapped, null, 2));
3896
4657
  }
3897
4658
  }
3898
- async function gemPromote(ctx, args, linkedProject) {
3899
- const flags = parseLocalFlags(args);
3900
- assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
3901
- await deployVersionToPublishedLink(ctx, flags, linkedProject, {
3902
- operation: "promote",
3903
- usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] | uru promote --deployment-id <deployment-id> [library/name.gem]"
3904
- });
3905
- }
3906
- async function gemChecks(ctx, args, linkedProject) {
3907
- const flags = parseLocalFlags(args);
3908
- if (flags.positionals[0] === "run") {
3909
- await gemRunCheck(ctx, flags, linkedProject);
3910
- return;
3911
- }
3912
- assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--path"], "uru gems checks");
3913
- const explicitDeploymentId = flags.values["--deployment-id"];
3914
- const gemPathArgs = explicitDeploymentId === undefined ? flags.positionals.slice(1) : flags.positionals;
3915
- const params = {
3916
- path: gemPathFromArgs(gemPathArgs, linkedProject)
3917
- };
3918
- if (explicitDeploymentId !== undefined) {
3919
- params["deployment_id"] = explicitDeploymentId;
3920
- } else if (flags.positionals[0] !== undefined) {
3921
- params["version_id"] = flags.positionals[0];
3922
- }
3923
- await emitGemTool(ctx, "gem.checks", params);
3924
- }
3925
- async function gemRunCheck(ctx, flags, linkedProject) {
3926
- assertAllowedLocalFlags(flags, ["--blocking", "--deep", "--deployment-id", "--gem", "--path"], "uru gems checks run");
3927
- const kind = requireCommandValue(flags.positionals[1], "Usage: uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]");
3928
- const params = {
3929
- path: gemPathFromArgs(flags.positionals.slice(2), linkedProject)
3930
- };
3931
- copyStringFlag(flags, params, "--deployment-id", "deployment_id");
3932
- if (flags.booleans.has("--blocking")) {
3933
- params["blocking"] = true;
3934
- }
3935
- if (kind === "browser" || kind === "browser_verify") {
3936
- if (flags.booleans.has("--deep")) {
3937
- throw new CliError("--deep is only valid for `uru gems checks run security`.");
3938
- }
3939
- await emitGemTool(ctx, "gem.browser_verify", params);
3940
- return;
3941
- }
3942
- if (kind === "security" || kind === "security_scan") {
3943
- if (flags.booleans.has("--deep")) {
3944
- params["deep"] = true;
3945
- }
3946
- await emitGemTool(ctx, "gem.security_scan", params);
3947
- return;
3948
- }
3949
- throw new CliError("Unknown Gem check runner. Use `uru gems checks run browser` or `uru gems checks run security`.");
3950
- }
3951
4659
  async function gemLogs(ctx, args, linkedProject) {
3952
4660
  const flags = parseLocalFlags(args);
3953
- assertAllowedLocalFlags(flags, [
3954
- "--deployment-id",
4661
+ assertAllowedLocalFlags2(flags, [
3955
4662
  "--follow",
3956
4663
  "--follow-duration-ms",
3957
4664
  "--gem",
@@ -3959,15 +4666,16 @@ async function gemLogs(ctx, args, linkedProject) {
3959
4666
  "--limit",
3960
4667
  "--path",
3961
4668
  "--poll-interval-ms",
3962
- "--request-id"
4669
+ "--release",
4670
+ "--run"
3963
4671
  ], "uru logs");
3964
4672
  const params = {
3965
- path: gemPathFromArgs(flags.positionals, linkedProject),
4673
+ path: gemPathFromArgs(flags, linkedProject),
3966
4674
  limit: integerFlag(flags, "--limit", 100)
3967
4675
  };
3968
4676
  copyStringFlag(flags, params, "--level", "level");
3969
- copyStringFlag(flags, params, "--deployment-id", "deployment_id");
3970
- copyStringFlag(flags, params, "--request-id", "request_id");
4677
+ copyStringFlag(flags, params, "--release", "release_id");
4678
+ copyStringFlag(flags, params, "--run", "run_id");
3971
4679
  if (flags.booleans.has("--follow")) {
3972
4680
  await followGemLogs(ctx, params, {
3973
4681
  durationMs: boundedIntegerFlag(flags, "--follow-duration-ms", DEFAULT_LOG_FOLLOW_DURATION_MS, MAX_LOG_FOLLOW_DURATION_MS),
@@ -4060,13 +4768,13 @@ function emitFollowLogEntry(ctx, entry) {
4060
4768
  writeText(ctx.io, formatLogEntry(entry));
4061
4769
  }
4062
4770
  function normalizeLogEntries(value) {
4063
- const root = jsonObjectOrNull(value);
4771
+ const root = jsonObjectOrNull2(value);
4064
4772
  const candidate = root === null ? value : root["logs"];
4065
4773
  if (!Array.isArray(candidate)) {
4066
4774
  return [];
4067
4775
  }
4068
4776
  return candidate.flatMap((entry, index) => {
4069
- const object = jsonObjectOrNull(entry);
4777
+ const object = jsonObjectOrNull2(entry);
4070
4778
  if (object === null) {
4071
4779
  return [];
4072
4780
  }
@@ -4109,35 +4817,6 @@ function boundedIntegerFlag(flags, name, fallback, maximum) {
4109
4817
  }
4110
4818
  return Math.min(value, maximum);
4111
4819
  }
4112
- async function gemOpen(ctx, args, linkedProject) {
4113
- const result = await executeOperation(ctx, "gem.links_list", {
4114
- path: gemPathFromArgs(args, linkedProject)
4115
- });
4116
- const url = firstUrl(unwrapToolResult(result));
4117
- if (url === undefined) {
4118
- throw new CliError("No published URL found for this Gem. Run `uru deploy` first.");
4119
- }
4120
- ctx.json ? writeJson(ctx.io, { url }) : writeText(ctx.io, url);
4121
- }
4122
- async function gemRollback(ctx, args, linkedProject) {
4123
- const flags = parseLocalFlags(args);
4124
- assertAllowedLocalFlags(flags, [
4125
- "--deployment-id",
4126
- "--gem",
4127
- "--link-id",
4128
- "--no-wait",
4129
- "--path",
4130
- "--pct",
4131
- "--yes"
4132
- ], "uru rollback");
4133
- if (!ctx.yes && !flags.booleans.has("--yes")) {
4134
- throw new CliError("Rollback requires --yes to confirm the link pointer change.");
4135
- }
4136
- await deployVersionToPublishedLink(ctx, flags, linkedProject, {
4137
- operation: "rollback",
4138
- usage: "Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes"
4139
- });
4140
- }
4141
4820
  async function gemWrite(ctx, args) {
4142
4821
  const flags = parseLocalFlags(args);
4143
4822
  const gemPath = requireCommandValue(flags.positionals[0], "Usage: uru gems write <gem-path> <file> --content <text>|--file <local-file>");
@@ -4157,142 +4836,13 @@ function gemFilePath(gemPath, filePath) {
4157
4836
  const normalizedFilePath = trimLeadingSlashes(filePath);
4158
4837
  return `${normalizedGemPath}/${normalizedFilePath}`;
4159
4838
  }
4160
- async function dispatchGemVersions(ctx, args, linkedProject) {
4161
- const [verb, ...rest] = args;
4162
- if (verb === "upload") {
4163
- await gemVersionUpload(ctx, rest, linkedProject);
4164
- return;
4165
- }
4166
- if (verb === "deploy") {
4167
- const flags = parseLocalFlags(rest);
4168
- rejectUnsupportedLocalFlags(flags, ["--runtime-provider", "--provider-deployment-id"], "Provider routing is chosen by the Gem runtime contract; version deploy does not accept provider-internal flags.");
4169
- assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct", "--skip-promote"], "uru gems versions deploy");
4170
- const version = requireCommandValue(flags.positionals[0], "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]");
4171
- const [versionId, trafficPercent] = version.split("@", 2);
4172
- const gemPath = gemPathFromArgs(flags.positionals.slice(1), linkedProject);
4173
- const pct = flags.values["--pct"] ?? trafficPercent;
4174
- const trafficPercentValue = pct === undefined || pct === "" ? 100 : parseTrafficPercent(pct);
4175
- const publish = !flags.booleans.has("--skip-promote");
4176
- if (publish && trafficPercentValue !== 100) {
4177
- 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.");
4178
- }
4179
- const params = {
4180
- path: gemPath,
4181
- version_id: requireCommandValue(versionId, "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]"),
4182
- traffic_percent: trafficPercentValue,
4183
- publish
4184
- };
4185
- copyStringFlag(flags, params, "--link-id", "link_id");
4186
- if (flags.booleans.has("--no-wait")) {
4187
- params["wait"] = false;
4188
- }
4189
- await emitGemTool(ctx, "gem.version_deploy", params, {
4190
- streamStart: { operation: "gem.version_deploy", path: gemPath },
4191
- textExtractor: extractDeploymentUrl
4192
- });
4193
- return;
4194
- }
4195
- throw new CliError("Usage: uru gems versions upload [library/name.gem] | deploy <version-id> [library/name.gem]");
4196
- }
4197
- async function gemVersionUpload(ctx, args, linkedProject) {
4198
- const flags = parseLocalFlags(args);
4199
- assertAllowedLocalFlags(flags, [
4200
- "--force",
4201
- "--gem",
4202
- "--message",
4203
- "--path",
4204
- "--preview-alias",
4205
- "--source-version-id",
4206
- "--tag",
4207
- "--tags"
4208
- ], "uru gems versions upload");
4209
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
4210
- const params = {
4211
- path: gemPath
4212
- };
4213
- if (flags.booleans.has("--force")) {
4214
- params["force"] = true;
4215
- }
4216
- copyStringFlag(flags, params, "--source-version-id", "source_version_id");
4217
- copyStringFlag(flags, params, "--message", "message");
4218
- const previewAlias = flags.values["--preview-alias"];
4219
- if (previewAlias !== undefined) {
4220
- params["preview_alias"] = validatePreviewAlias(previewAlias);
4221
- }
4222
- const tags = tagsFromFlags(flags);
4223
- if (tags.length > 0) {
4224
- params["tags"] = tags;
4225
- }
4226
- await emitGemTool(ctx, "gem.version_upload", params, {
4227
- streamStart: { operation: "gem.version_upload", path: gemPath },
4228
- textExtractor: extractVersionUploadSummary
4229
- });
4230
- }
4231
- function parseTrafficPercent(raw) {
4232
- const value = Number(raw);
4233
- if (!Number.isFinite(value) || value < 0 || value > 100) {
4234
- throw new CliError("--pct or @pct must be a number from 0 through 100");
4235
- }
4236
- return value;
4237
- }
4238
- async function deployVersionToPublishedLink(ctx, flags, linkedProject, options) {
4239
- const deploymentId = flags.values["--deployment-id"];
4240
- const firstPositional = flags.positionals[0];
4241
- const versionId = deploymentId === undefined && firstPositional !== undefined ? firstPositional.startsWith("library/") ? undefined : firstPositional : undefined;
4242
- if (deploymentId === undefined && versionId === undefined) {
4243
- throw new CliError(options.usage);
4244
- }
4245
- const gemPath = gemPathFromArgs(versionId === undefined ? flags.positionals : flags.positionals.slice(1), linkedProject);
4246
- const trafficPercent = parseTrafficPercent(flags.values["--pct"] ?? "100");
4247
- if (trafficPercent !== 100) {
4248
- 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.`);
4249
- }
4250
- const params = {
4251
- path: gemPath
4252
- };
4253
- if (deploymentId === undefined) {
4254
- params["version_id"] = requireCommandValue(versionId, options.usage);
4255
- } else {
4256
- params["app_deployment_id"] = deploymentId;
4257
- }
4258
- copyStringFlag(flags, params, "--link-id", "link_id");
4259
- if (flags.booleans.has("--no-wait")) {
4260
- params["wait"] = false;
4261
- }
4262
- const operation = `gem.${options.operation}`;
4263
- await emitGemTool(ctx, operation, params, {
4264
- streamStart: { operation, path: gemPath },
4265
- textExtractor: extractDeploymentUrl
4266
- });
4267
- }
4268
- function tagsFromFlags(flags) {
4269
- const raw = flags.values["--tags"] ?? flags.values["--tag"];
4270
- if (raw === undefined) {
4271
- return [];
4272
- }
4273
- return raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
4274
- }
4275
- function validatePreviewAlias(rawAlias) {
4276
- const alias = rawAlias.trim();
4277
- if (alias.length === 0 || alias.length > 63 || !/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(alias)) {
4278
- 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.");
4279
- }
4280
- return alias;
4281
- }
4282
- function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
4839
+ function assertAllowedLocalFlags2(flags, allowedFlags, commandName) {
4283
4840
  const allowed = new Set(allowedFlags);
4284
4841
  const unexpected = localFlagNames(flags).filter((flag) => !allowed.has(flag));
4285
4842
  if (unexpected.length > 0) {
4286
4843
  throw new CliError(`${commandName} does not support ${unexpected.join(", ")}.`);
4287
4844
  }
4288
4845
  }
4289
- function rejectUnsupportedLocalFlags(flags, unsupportedFlags, message) {
4290
- const unsupported = new Set(unsupportedFlags);
4291
- const found = localFlagNames(flags).filter((flag) => unsupported.has(flag));
4292
- if (found.length > 0) {
4293
- throw new CliError(`${message} Unsupported flag(s): ${found.join(", ")}.`);
4294
- }
4295
- }
4296
4846
  function localFlagNames(flags) {
4297
4847
  return [...new Set([...Object.keys(flags.values), ...flags.booleans])];
4298
4848
  }
@@ -4307,10 +4857,10 @@ async function writeBuildManifest(ctx, outdir, buildResult) {
4307
4857
  `, { mode: 384 });
4308
4858
  }
4309
4859
  function gemBuildManifestFromBuildResult(buildResult) {
4310
- const root = jsonObjectOrNull(buildResult);
4311
- const build = jsonObjectOrNull(root?.["build"]);
4312
- const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
4313
- 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"]);
4314
4864
  const candidates = [
4315
4865
  root?.["gem_build_manifest"],
4316
4866
  root?.["gemBuildManifest"],
@@ -4330,7 +4880,7 @@ function gemBuildManifestFromBuildResult(buildResult) {
4330
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.");
4331
4881
  }
4332
4882
  function normalizeGemBuildManifest(value) {
4333
- const manifest = jsonObjectOrNull(value);
4883
+ const manifest = jsonObjectOrNull2(value);
4334
4884
  if (manifest === null) {
4335
4885
  return null;
4336
4886
  }
@@ -4357,10 +4907,10 @@ function normalizeGemBuildManifest(value) {
4357
4907
  };
4358
4908
  }
4359
4909
  function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4360
- const root = jsonObjectOrNull(buildResult);
4361
- const build = jsonObjectOrNull(root?.["build"]);
4362
- const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
4363
- 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"]);
4364
4914
  const runtimeProfile = firstJsonObject([
4365
4915
  root?.["runtime_profile"],
4366
4916
  root?.["runtimeProfile"],
@@ -4397,8 +4947,8 @@ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4397
4947
  schema: "uru.gem.provider_decision.v1",
4398
4948
  schema_version: 1,
4399
4949
  manifest_kind: "gem_provider_decision",
4400
- provider: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["provider", "runtime_provider", "runtimeProvider", "selected_provider"]),
4401
- 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"]),
4402
4952
  runtime_profile: runtimeProfile,
4403
4953
  provider_decision: providerDecision,
4404
4954
  runtime_contract: runtimeContract,
@@ -4412,14 +4962,14 @@ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
4412
4962
  }
4413
4963
  function firstJsonObject(values) {
4414
4964
  for (const value of values) {
4415
- const object = jsonObjectOrNull(value);
4965
+ const object = jsonObjectOrNull2(value);
4416
4966
  if (object !== null) {
4417
4967
  return object;
4418
4968
  }
4419
4969
  }
4420
4970
  return null;
4421
4971
  }
4422
- function stringFieldFromJsonObjects(objects, keys) {
4972
+ function stringFieldFromJsonObjects2(objects, keys) {
4423
4973
  for (const object of objects) {
4424
4974
  for (const key of keys) {
4425
4975
  const value = object[key];
@@ -4430,57 +4980,12 @@ function stringFieldFromJsonObjects(objects, keys) {
4430
4980
  }
4431
4981
  return null;
4432
4982
  }
4433
- function jsonObjectOrNull(value) {
4983
+ function jsonObjectOrNull2(value) {
4434
4984
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
4435
4985
  return null;
4436
4986
  }
4437
4987
  return value;
4438
4988
  }
4439
- function extractDeploymentUrl(value) {
4440
- return firstUrl(value);
4441
- }
4442
- function extractVersionUploadSummary(value) {
4443
- const objects = collectJsonObjects(value);
4444
- const versionId = stringFieldFromJsonObjects(objects, [
4445
- "version_id",
4446
- "versionId",
4447
- "id"
4448
- ]);
4449
- const previewUrl = stringFieldFromJsonObjects(objects, [
4450
- "preview_url",
4451
- "previewUrl",
4452
- "preview_alias_url",
4453
- "previewAliasUrl",
4454
- "url"
4455
- ]) ?? firstUrl(value);
4456
- const lines = [
4457
- versionId === null ? null : `Version: ${versionId}`,
4458
- previewUrl === undefined || previewUrl === null ? null : `Preview: ${previewUrl}`
4459
- ].filter((line) => line !== null);
4460
- return lines.length === 0 ? undefined : lines.join(`
4461
- `);
4462
- }
4463
- function collectJsonObjects(value) {
4464
- const objects = [];
4465
- collectJsonObjectsInto(value, objects);
4466
- return objects;
4467
- }
4468
- function collectJsonObjectsInto(value, objects) {
4469
- if (Array.isArray(value)) {
4470
- for (const item of value) {
4471
- collectJsonObjectsInto(item, objects);
4472
- }
4473
- return;
4474
- }
4475
- const object = jsonObjectOrNull(value);
4476
- if (object === null) {
4477
- return;
4478
- }
4479
- objects.push(object);
4480
- for (const child of Object.values(object)) {
4481
- collectJsonObjectsInto(child, objects);
4482
- }
4483
- }
4484
4989
  function trimTrailingSlashes(value) {
4485
4990
  let end = value.length;
4486
4991
  while (end > 0 && value.charCodeAt(end - 1) === 47) {
@@ -4496,12 +5001,68 @@ function trimLeadingSlashes(value) {
4496
5001
  return value.slice(start);
4497
5002
  }
4498
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
+
4499
5060
  // src/gem-source-commands.ts
4500
5061
  import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
4501
5062
  import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
4502
5063
  async function gemSourcePull(ctx, args, linkedProject) {
4503
5064
  const flags = parseLocalFlags(args);
4504
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
5065
+ const gemPath = gemPathFromArgs(flags, linkedProject);
4505
5066
  const force = flags.booleans.has("--force");
4506
5067
  const previousLock = await readSourceLock(ctx.cwd);
4507
5068
  const localFilesBeforePull = await collectLocalSourceFiles(ctx.cwd);
@@ -4519,20 +5080,24 @@ async function gemSourcePull(ctx, args, linkedProject) {
4519
5080
  if (filePaths.length === 0) {
4520
5081
  throw new CliError(`No source files found for ${gemPath}.`);
4521
5082
  }
4522
- let sourceVersionId = findSourceVersionId(tree);
5083
+ let sourceRevisionId = findSourceRevisionId(tree);
4523
5084
  const files = {};
5085
+ const pulledFiles = [];
4524
5086
  for (const filePath of filePaths) {
4525
5087
  const readResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_read", {
4526
5088
  path: gemFilePath(gemPath, filePath)
4527
5089
  }));
4528
- sourceVersionId = sourceVersionId ?? findSourceVersionId(readResult);
5090
+ sourceRevisionId = sourceRevisionId ?? findSourceRevisionId(readResult);
4529
5091
  const content = extractRemoteFileContent(readResult, filePath);
4530
- await writePulledSourceFile(ctx.cwd, filePath, content);
5092
+ pulledFiles.push({ path: filePath, content });
4531
5093
  files[filePath] = sha256Hex(content);
4532
5094
  }
5095
+ for (const file of pulledFiles) {
5096
+ await writePulledSourceFile(ctx.cwd, file.path, file.content);
5097
+ }
4533
5098
  const pruned = await pruneRemoteDeletedSourceFiles(ctx.cwd, previousLock, localFilesBeforePull, new Set(filePaths));
4534
5099
  const lock = {
4535
- ...sourceVersionId === undefined ? {} : { sourceVersionId },
5100
+ ...sourceRevisionId === undefined ? {} : { sourceRevisionId },
4536
5101
  files,
4537
5102
  checkedOutAt: new Date().toISOString()
4538
5103
  };
@@ -4543,18 +5108,18 @@ async function gemSourcePull(ctx, args, linkedProject) {
4543
5108
  files: filePaths,
4544
5109
  prunedFiles: pruned.deleted,
4545
5110
  preservedFiles: pruned.preserved,
4546
- ...sourceVersionId === undefined ? {} : { sourceVersionId }
5111
+ ...sourceRevisionId === undefined ? {} : { sourceRevisionId }
4547
5112
  }, formatPullSummary(gemPath, filePaths.length, pruned));
4548
5113
  }
4549
5114
  async function gemSourceStatus(ctx, args, linkedProject) {
4550
5115
  const flags = parseLocalFlags(args);
4551
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
5116
+ const gemPath = gemPathFromArgs(flags, linkedProject);
4552
5117
  const lock = await readSourceLock(ctx.cwd);
4553
5118
  const localFiles = await collectLocalSourceFiles(ctx.cwd);
4554
5119
  const localStatus = compareSourceLock(lock, localFiles);
4555
- const remote = await tryRemoteSourceOperation(ctx, "gem.fs_status", {
5120
+ const remote = await remoteSourceOperation(ctx, "gem.fs_status", {
4556
5121
  path: gemPath,
4557
- ...lock.sourceVersionId === undefined ? {} : { base: lock.sourceVersionId }
5122
+ ...lock.sourceRevisionId === undefined ? {} : { base: lock.sourceRevisionId }
4558
5123
  });
4559
5124
  const report = sourceStatusReport(gemPath, lock, localStatus, remote);
4560
5125
  if (ctx.outputFormat === "stream-json") {
@@ -4567,15 +5132,15 @@ async function gemSourceStatus(ctx, args, linkedProject) {
4567
5132
  }
4568
5133
  async function gemSourceDiff(ctx, args, linkedProject) {
4569
5134
  const flags = parseLocalFlags(args);
4570
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
5135
+ const gemPath = gemPathFromArgs(flags, linkedProject);
4571
5136
  const lock = await readSourceLock(ctx.cwd);
4572
5137
  const localFiles = await collectLocalSourceFiles(ctx.cwd);
4573
5138
  const localStatus = compareSourceLock(lock, localFiles);
4574
5139
  const mode = flags.values["--mode"] ?? "name_status";
4575
- const remote = await tryRemoteSourceOperation(ctx, "gem.fs_diff", {
5140
+ const remote = await remoteSourceOperation(ctx, "gem.fs_diff", {
4576
5141
  path: gemPath,
4577
5142
  mode,
4578
- ...lock.sourceVersionId === undefined ? {} : { base: lock.sourceVersionId }
5143
+ ...lock.sourceRevisionId === undefined ? {} : { base: lock.sourceRevisionId }
4579
5144
  });
4580
5145
  const report = sourceStatusReport(gemPath, lock, localStatus, remote);
4581
5146
  if (ctx.outputFormat === "stream-json") {
@@ -4588,7 +5153,7 @@ async function gemSourceDiff(ctx, args, linkedProject) {
4588
5153
  }
4589
5154
  async function gemSourcePush(ctx, args, linkedProject) {
4590
5155
  const flags = parseLocalFlags(args);
4591
- const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
5156
+ const gemPath = gemPathFromArgs(flags, linkedProject);
4592
5157
  const lock = await readSourceLock(ctx.cwd);
4593
5158
  const force = flags.booleans.has("--force");
4594
5159
  const localFiles = await collectLocalSourceFiles(ctx.cwd);
@@ -4597,11 +5162,11 @@ async function gemSourcePush(ctx, args, linkedProject) {
4597
5162
  if (sortedFiles.length === 0 && localStatus.deleted.length === 0) {
4598
5163
  throw new CliError("No local Gem source files to push.");
4599
5164
  }
4600
- if (lock.sourceVersionId === undefined && !force) {
4601
- 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 });
4602
5167
  }
4603
5168
  let sourceCas = {
4604
- ...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId }
5169
+ ...lock.sourceRevisionId === undefined ? {} : { sourceRevisionId: lock.sourceRevisionId }
4605
5170
  };
4606
5171
  try {
4607
5172
  const syncResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_sync", {
@@ -4615,14 +5180,17 @@ async function gemSourcePush(ctx, args, linkedProject) {
4615
5180
  }));
4616
5181
  sourceCas = nextSourceMutationCas(sourceCas, syncResult);
4617
5182
  } catch (error) {
5183
+ if (error instanceof CliError && error.exitCode === ExitCode.Conflict) {
5184
+ throw error;
5185
+ }
4618
5186
  if (isConflictLikeError(error)) {
4619
5187
  throw new CliError(error instanceof Error ? error.message : "Gem source conflict", { code: ExitCode.Conflict, cause: error });
4620
5188
  }
4621
5189
  throw error;
4622
5190
  }
4623
- const nextSourceVersionId = sourceCas.sourceVersionId;
5191
+ const nextSourceRevisionId = sourceCas.sourceRevisionId;
4624
5192
  await writeSourceLock(ctx.cwd, {
4625
- ...nextSourceVersionId === undefined ? {} : { sourceVersionId: nextSourceVersionId },
5193
+ ...nextSourceRevisionId === undefined ? {} : { sourceRevisionId: nextSourceRevisionId },
4626
5194
  files: sourceHashesFromLocalFiles(localFiles),
4627
5195
  checkedOutAt: new Date().toISOString()
4628
5196
  });
@@ -4631,7 +5199,7 @@ async function gemSourcePush(ctx, args, linkedProject) {
4631
5199
  path: gemPath,
4632
5200
  files: sortedFiles.map((file) => file.path),
4633
5201
  deletedFiles: [...localStatus.deleted],
4634
- ...nextSourceVersionId === undefined ? {} : { sourceVersionId: nextSourceVersionId }
5202
+ ...nextSourceRevisionId === undefined ? {} : { sourceRevisionId: nextSourceRevisionId }
4635
5203
  }, formatPushSummary(gemPath, sortedFiles.length, localStatus.deleted.length));
4636
5204
  }
4637
5205
  function localFileBatchWritePayload(file) {
@@ -4679,17 +5247,17 @@ function formatPullSummary(gemPath, writtenCount, pruned) {
4679
5247
  return parts.join("; ");
4680
5248
  }
4681
5249
  function sourceMutationCasParams(cas, force) {
4682
- if (force || cas.sourceVersionId === undefined) {
5250
+ if (force || cas.sourceRevisionId === undefined) {
4683
5251
  return { force: true };
4684
5252
  }
4685
5253
  return {
4686
- base_source_version_id: cas.sourceVersionId
5254
+ base_source_revision_id: cas.sourceRevisionId
4687
5255
  };
4688
5256
  }
4689
5257
  function nextSourceMutationCas(current, result) {
4690
- const sourceVersionId = findSourceVersionId(result) ?? current.sourceVersionId;
5258
+ const sourceRevisionId = findSourceRevisionId(result) ?? current.sourceRevisionId;
4691
5259
  return {
4692
- ...sourceVersionId === undefined ? {} : { sourceVersionId }
5260
+ ...sourceRevisionId === undefined ? {} : { sourceRevisionId }
4693
5261
  };
4694
5262
  }
4695
5263
  function formatPushSummary(gemPath, writtenCount, deletedCount) {
@@ -4705,7 +5273,7 @@ function formatPushSummary(gemPath, writtenCount, deletedCount) {
4705
5273
  function sourceStatusReport(path, lock, local, remote) {
4706
5274
  return {
4707
5275
  path,
4708
- sourceVersionId: lock.sourceVersionId ?? null,
5276
+ sourceRevisionId: lock.sourceRevisionId ?? null,
4709
5277
  local,
4710
5278
  remote
4711
5279
  };
@@ -4713,7 +5281,7 @@ function sourceStatusReport(path, lock, local, remote) {
4713
5281
  function sourceStatusReportJson(report) {
4714
5282
  return {
4715
5283
  path: report.path,
4716
- sourceVersionId: report.sourceVersionId,
5284
+ sourceRevisionId: report.sourceRevisionId,
4717
5285
  local: localSourceStatusJson(report.local),
4718
5286
  remote: remoteSourceOperationResultJson(report.remote)
4719
5287
  };
@@ -4741,7 +5309,7 @@ function remoteSourceOperationResultJson(remote) {
4741
5309
  function formatSourceStatus(report) {
4742
5310
  const lines = [
4743
5311
  `Gem: ${report.path}`,
4744
- `Source version: ${report.sourceVersionId ?? "unknown"}`,
5312
+ `Source version: ${report.sourceRevisionId ?? "unknown"}`,
4745
5313
  `Local source: ${report.local.clean ? "clean" : "dirty"}`,
4746
5314
  ...formatLocalStatusEntries(report.local),
4747
5315
  `Remote source: ${formatRemoteSummary(report.remote)}`
@@ -4753,7 +5321,7 @@ function formatSourceDiff(report) {
4753
5321
  const localLines = formatLocalStatusEntries(report.local);
4754
5322
  const lines = [
4755
5323
  `Gem: ${report.path}`,
4756
- `Source version: ${report.sourceVersionId ?? "unknown"}`,
5324
+ `Source version: ${report.sourceRevisionId ?? "unknown"}`,
4757
5325
  "Local changes since .uru/source-lock.json:",
4758
5326
  ...localLines.length === 0 ? [" clean"] : localLines.map((line) => ` ${line}`),
4759
5327
  "Remote diff:",
@@ -4780,18 +5348,11 @@ function dirtyPullErrorMessage(status) {
4780
5348
  Run \`uru status\` to inspect them, save or push your changes, or re-run \`uru pull --force\` to replace local files with server source.
4781
5349
  ${preview}${moreLine}`;
4782
5350
  }
4783
- async function tryRemoteSourceOperation(ctx, operationId, params) {
4784
- try {
4785
- return {
4786
- available: true,
4787
- result: unwrapToolResult(await executeOperation(ctx, operationId, params))
4788
- };
4789
- } catch (error) {
4790
- return {
4791
- available: false,
4792
- error: error instanceof Error ? error.message : "Remote operation failed"
4793
- };
4794
- }
5351
+ async function remoteSourceOperation(ctx, operationId, params) {
5352
+ return {
5353
+ available: true,
5354
+ result: unwrapToolResult(await executeOperation(ctx, operationId, params))
5355
+ };
4795
5356
  }
4796
5357
  function formatRemoteSummary(remote) {
4797
5358
  if (!remote.available) {
@@ -4897,17 +5458,17 @@ function safeProjectPath(path) {
4897
5458
  }
4898
5459
  return parts.join("/");
4899
5460
  }
4900
- function findSourceVersionId(value) {
5461
+ function findSourceRevisionId(value) {
4901
5462
  let found;
4902
5463
  visitJson(value, (candidate) => {
4903
5464
  if (!isJsonObject2(candidate)) {
4904
5465
  return;
4905
5466
  }
4906
5467
  const valueAtKey = stringField(candidate, [
4907
- "currentSourceVersionId",
4908
- "current_source_version_id",
4909
- "sourceVersionId",
4910
- "source_version_id"
5468
+ "currentSourceRevisionId",
5469
+ "current_source_revision_id",
5470
+ "sourceRevisionId",
5471
+ "source_revision_id"
4911
5472
  ]);
4912
5473
  if (valueAtKey !== undefined) {
4913
5474
  found = valueAtKey;
@@ -4932,11 +5493,12 @@ function visitJson(value, visitor) {
4932
5493
  }
4933
5494
  function isConflictLikeError(error) {
4934
5495
  const message = error instanceof Error ? error.message.toLowerCase() : "";
4935
- 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");
4936
5497
  }
4937
5498
 
4938
5499
  // src/token-env-secret-commands.ts
4939
5500
  import { spawn as spawn2 } from "node:child_process";
5501
+ import { StringDecoder } from "node:string_decoder";
4940
5502
  import { chmod, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
4941
5503
  import { dirname as dirname5, resolve as resolve5 } from "node:path";
4942
5504
  async function dispatchTokens(ctx, sub, args, linkedProject) {
@@ -5188,48 +5750,69 @@ function unquoteEnvValue(value) {
5188
5750
  }
5189
5751
  async function runEnvCommand(ctx, command, envVars) {
5190
5752
  const executable = requireCommandValue(command[0], "env run command is required");
5191
- const result = await spawnBuffered(executable, command.slice(1), {
5753
+ const redactions = Object.values(envVars).filter((value) => value.length > 0);
5754
+ const code = await spawnStreaming(executable, command.slice(1), {
5192
5755
  cwd: ctx.cwd,
5193
- env: { ...process.env, ...envVars }
5756
+ env: { ...process.env, ...envVars },
5757
+ redactions,
5758
+ stdout: ctx.io.stdout,
5759
+ stderr: ctx.io.stderr
5194
5760
  });
5195
- const redactions = Object.values(envVars).filter((value) => value.length > 0);
5196
- const stdout = redactValues2(result.stdout, redactions);
5197
- const stderr = redactValues2(result.stderr, redactions);
5198
- if (stdout.length > 0) {
5199
- ctx.io.stdout.write(stdout);
5200
- }
5201
- if (stderr.length > 0) {
5202
- ctx.io.stderr.write(stderr);
5203
- }
5204
- if (result.code !== 0) {
5205
- throw new CliError(`env run failed with exit code ${result.code}`);
5761
+ if (code !== 0) {
5762
+ throw new CliError(`env run failed with exit code ${code}`);
5206
5763
  }
5207
5764
  }
5208
- async function spawnBuffered(command, args, options) {
5765
+ async function spawnStreaming(command, args, options) {
5209
5766
  return new Promise((resolvePromise, reject) => {
5210
5767
  const child = spawn2(command, [...args], {
5211
5768
  cwd: options.cwd,
5212
5769
  env: options.env,
5213
- stdio: ["ignore", "pipe", "pipe"]
5770
+ stdio: ["inherit", "pipe", "pipe"]
5214
5771
  });
5215
- const stdout = [];
5216
- const stderr = [];
5772
+ const stdout = redactingLineWriter(options.stdout, options.redactions);
5773
+ const stderr = redactingLineWriter(options.stderr, options.redactions);
5217
5774
  child.stdout?.on("data", (chunk) => {
5218
- stdout.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
5775
+ stdout.write(chunk);
5219
5776
  });
5220
5777
  child.stderr?.on("data", (chunk) => {
5221
- stderr.push(Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk));
5778
+ stderr.write(chunk);
5222
5779
  });
5223
5780
  child.on("error", reject);
5224
5781
  child.on("close", (code) => {
5225
- resolvePromise({
5226
- code: code ?? 1,
5227
- stdout: stdout.join(""),
5228
- stderr: stderr.join("")
5229
- });
5782
+ stdout.end();
5783
+ stderr.end();
5784
+ resolvePromise(code ?? 1);
5230
5785
  });
5231
5786
  });
5232
5787
  }
5788
+ function redactingLineWriter(destination, redactions) {
5789
+ const decoder = new StringDecoder("utf8");
5790
+ let buffered = "";
5791
+ const flushCompleteLines = () => {
5792
+ let newline = buffered.indexOf(`
5793
+ `);
5794
+ while (newline !== -1) {
5795
+ destination.write(redactValues2(buffered.slice(0, newline + 1), redactions));
5796
+ buffered = buffered.slice(newline + 1);
5797
+ newline = buffered.indexOf(`
5798
+ `);
5799
+ }
5800
+ };
5801
+ return {
5802
+ write: (chunk) => {
5803
+ buffered += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5804
+ flushCompleteLines();
5805
+ },
5806
+ end: () => {
5807
+ buffered += decoder.end();
5808
+ flushCompleteLines();
5809
+ if (buffered.length > 0) {
5810
+ destination.write(redactValues2(buffered, redactions));
5811
+ buffered = "";
5812
+ }
5813
+ }
5814
+ };
5815
+ }
5233
5816
  function redactValues2(content, values) {
5234
5817
  let redacted = content;
5235
5818
  const ordered = [...new Set(values)].filter((value) => value.length > 0).sort((left, right) => right.length - left.length);
@@ -5376,11 +5959,21 @@ async function runCli(options) {
5376
5959
  const envGemId = nonEmptyString(options.env?.["URU_GEM_ID"]);
5377
5960
  const explicitGemRef = parsed.flags.gem ?? envGemId;
5378
5961
  const apiUrl = parsed.flags.apiUrl ?? envApiUrl ?? config.apiUrl ?? DEFAULT_API_URL;
5379
- const token = flagToken ?? envToken ?? await resolveStoredToken(credentialStore, apiUrl, config);
5380
- const cwd = parsed.flags.cwd ?? options.env?.["PWD"] ?? process.cwd();
5962
+ const cwd = parsed.flags.cwd ?? process.cwd();
5381
5963
  const linkedProjectFromDisk = await readLinkedProject(cwd);
5382
5964
  const linkedProject = explicitGemRef === undefined ? linkedProjectFromDisk : linkedProjectWithExplicitGemRef(linkedProjectFromDisk, explicitGemRef);
5383
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();
5384
5977
  const outputFormat = parsed.flags.outputFormat ?? (parsed.flags.json ? "json" : "text");
5385
5978
  const callbackHost = parsed.flags.callbackHost ?? "127.0.0.1";
5386
5979
  const callbackPort = parseCallbackPort(parsed.flags.callbackPort);
@@ -5394,7 +5987,7 @@ async function runCli(options) {
5394
5987
  const ctx = {
5395
5988
  io,
5396
5989
  store,
5397
- config,
5990
+ config: runtimeConfig,
5398
5991
  credentialStore,
5399
5992
  apiUrl,
5400
5993
  ...token !== undefined ? { token } : {},
@@ -5500,11 +6093,18 @@ async function dispatchWithImplicitLogin(ctx, command, linkedProject) {
5500
6093
  }
5501
6094
  await login(ctx, { silentSuccess: true });
5502
6095
  const nextConfig = await ctx.store.read();
5503
- 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
+ });
5504
6105
  if (nextToken === undefined) {
5505
6106
  throw new CliError("Login completed but no CLI session token was stored. Re-run `uru login` or pass --token.", { code: ExitCode.AuthRequired });
5506
6107
  }
5507
- const selectedWorkspace = ctx.workspace ?? nextConfig.workspace;
5508
6108
  const nextCtx = {
5509
6109
  ...ctx,
5510
6110
  config: nextConfig,
@@ -5514,7 +6114,14 @@ async function dispatchWithImplicitLogin(ctx, command, linkedProject) {
5514
6114
  await dispatch(nextCtx, command, linkedProject);
5515
6115
  }
5516
6116
  function shouldStartImplicitLogin(ctx, error) {
5517
- 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 ?? "");
5518
6125
  }
5519
6126
  function terminalStdin(stdin) {
5520
6127
  return stdin === undefined ? process.stdin : stdin;
@@ -5526,8 +6133,26 @@ async function dispatch(ctx, command, linkedProject) {
5526
6133
  return;
5527
6134
  }
5528
6135
  if (head === "logout") {
5529
- await ctx.credentialStore.clearToken(ctx.apiUrl);
5530
- 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
+ }
5531
6156
  emit(ctx, { ok: true }, "Logged out");
5532
6157
  return;
5533
6158
  }
@@ -5596,16 +6221,6 @@ async function dispatch(ctx, command, linkedProject) {
5596
6221
  await gemBuild(ctx, command.slice(1), linkedProject);
5597
6222
  return;
5598
6223
  }
5599
- if (head === "deploy") {
5600
- await withWorkspace(ctx);
5601
- await gemDeploy(ctx, command.slice(1), linkedProject);
5602
- return;
5603
- }
5604
- if (head === "promote") {
5605
- await withWorkspace(ctx);
5606
- await gemPromote(ctx, command.slice(1), linkedProject);
5607
- return;
5608
- }
5609
6224
  if (head === "pull") {
5610
6225
  await withWorkspace(ctx);
5611
6226
  await gemSourcePull(ctx, command.slice(1), linkedProject);
@@ -5636,11 +6251,6 @@ async function dispatch(ctx, command, linkedProject) {
5636
6251
  await gemOpen(ctx, command.slice(1), linkedProject);
5637
6252
  return;
5638
6253
  }
5639
- if (head === "rollback") {
5640
- await withWorkspace(ctx);
5641
- await gemRollback(ctx, command.slice(1), linkedProject);
5642
- return;
5643
- }
5644
6254
  if (head === "gems") {
5645
6255
  await withWorkspace(ctx);
5646
6256
  await dispatchGems(ctx, sub, command.slice(2), linkedProject);