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