@uru-intelligence/cli 0.3.55 → 0.3.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -22
- package/dist/uru.mjs +1416 -806
- package/package.json +14 -4
package/dist/uru.mjs
CHANGED
|
@@ -3,10 +3,70 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { spawn as spawn3 } from "node:child_process";
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@uru-intelligence/cli",
|
|
9
|
+
version: "0.3.57",
|
|
10
|
+
private: false,
|
|
11
|
+
description: "Uru full-platform command line interface",
|
|
12
|
+
repository: {
|
|
13
|
+
type: "git",
|
|
14
|
+
url: "git+https://github.com/Ignition-Forward/uru-workspace-platform.git",
|
|
15
|
+
directory: "apps/cli"
|
|
16
|
+
},
|
|
17
|
+
uru: {
|
|
18
|
+
mcpPackage: "uru-mcp@3.7.2"
|
|
19
|
+
},
|
|
20
|
+
type: "module",
|
|
21
|
+
bin: {
|
|
22
|
+
uru: "dist/uru.mjs"
|
|
23
|
+
},
|
|
24
|
+
exports: {
|
|
25
|
+
".": "./dist/uru.mjs"
|
|
26
|
+
},
|
|
27
|
+
scripts: {
|
|
28
|
+
lint: "oxlint . --type-aware",
|
|
29
|
+
"lint:fix": "oxlint . --type-aware --fix",
|
|
30
|
+
"type-check": "tsc --noEmit",
|
|
31
|
+
test: "bun test",
|
|
32
|
+
"test:unit": "bun test",
|
|
33
|
+
format: "prettier --write . --ignore-path ../../.prettierignore",
|
|
34
|
+
"format:check": "prettier --check . --ignore-path ../../.prettierignore",
|
|
35
|
+
ci: "bun run lint && bun run type-check && bun run test:unit && bun run docs:check && bun run format:check && bun run build && bun run smoke:package && bun run smoke:binaries",
|
|
36
|
+
build: "node scripts/build.mjs",
|
|
37
|
+
prepack: "bun run build",
|
|
38
|
+
"docs:generate": "bun scripts/generate-docs.mjs",
|
|
39
|
+
"docs:check": "bun scripts/generate-docs.mjs --check",
|
|
40
|
+
"smoke:staging": "node scripts/staging-smoke.mjs",
|
|
41
|
+
"smoke:package": "node scripts/package-smoke.mjs",
|
|
42
|
+
"smoke:mcp-pin": "node scripts/mcp-pin-smoke.mjs",
|
|
43
|
+
"smoke:registry": "node scripts/registry-smoke.mjs",
|
|
44
|
+
"build:binaries": "node scripts/build-binaries.mjs",
|
|
45
|
+
"build:binaries:matrix": "node scripts/build-binaries.mjs --all",
|
|
46
|
+
"smoke:binaries": "node scripts/binary-smoke.mjs"
|
|
47
|
+
},
|
|
48
|
+
devDependencies: {
|
|
49
|
+
"@uru/platform-operations": "workspace:*",
|
|
50
|
+
"@types/node": "24.13.3",
|
|
51
|
+
"bun-types": "1.3.13",
|
|
52
|
+
typescript: "7.0.2"
|
|
53
|
+
},
|
|
54
|
+
packageManager: "bun@1.3.13",
|
|
55
|
+
engines: {
|
|
56
|
+
node: ">=24"
|
|
57
|
+
},
|
|
58
|
+
files: [
|
|
59
|
+
"dist",
|
|
60
|
+
"README.md"
|
|
61
|
+
],
|
|
62
|
+
publishConfig: {
|
|
63
|
+
access: "public"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
6
66
|
|
|
7
67
|
// src/types.ts
|
|
8
68
|
var DEFAULT_API_URL = "https://api.uruintelligence.com";
|
|
9
|
-
var CLI_VERSION =
|
|
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) {
|
|
@@ -906,171 +987,6 @@ function normalizeTool(value) {
|
|
|
906
987
|
return [tool];
|
|
907
988
|
}
|
|
908
989
|
|
|
909
|
-
// src/cli-helpers.ts
|
|
910
|
-
function parseApiArgs(args) {
|
|
911
|
-
let method = "GET";
|
|
912
|
-
let path;
|
|
913
|
-
let body;
|
|
914
|
-
const fields = {};
|
|
915
|
-
for (let index = 0;index < args.length; index += 1) {
|
|
916
|
-
const arg = args[index];
|
|
917
|
-
if (arg === undefined) {
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
if (arg === "-X" || arg === "--method") {
|
|
921
|
-
method = requireCommandValue(args[index + 1], `${arg} requires a method`).toUpperCase();
|
|
922
|
-
index += 1;
|
|
923
|
-
continue;
|
|
924
|
-
}
|
|
925
|
-
if (arg.startsWith("-X") && arg.length > 2) {
|
|
926
|
-
method = arg.slice(2).toUpperCase();
|
|
927
|
-
continue;
|
|
928
|
-
}
|
|
929
|
-
if (arg === "--body-json" || arg === "--data-json") {
|
|
930
|
-
body = parseParamsJson(requireCommandValue(args[index + 1], `${arg} requires a JSON object`), arg);
|
|
931
|
-
index += 1;
|
|
932
|
-
continue;
|
|
933
|
-
}
|
|
934
|
-
if (arg === "-F" || arg === "--field") {
|
|
935
|
-
addApiField(fields, requireCommandValue(args[index + 1], `${arg} requires key=value`));
|
|
936
|
-
index += 1;
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
if (arg.startsWith("-F") && arg.length > 2) {
|
|
940
|
-
addApiField(fields, arg.slice(2));
|
|
941
|
-
continue;
|
|
942
|
-
}
|
|
943
|
-
if (path === undefined) {
|
|
944
|
-
path = normalizeApiPath(arg);
|
|
945
|
-
continue;
|
|
946
|
-
}
|
|
947
|
-
throw new CliError(`Unexpected api argument: ${arg}`);
|
|
948
|
-
}
|
|
949
|
-
if (Object.keys(fields).length > 0) {
|
|
950
|
-
body = { ...body ?? {}, ...fields };
|
|
951
|
-
if (method === "GET") {
|
|
952
|
-
method = "POST";
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
return {
|
|
956
|
-
path: requireCommandValue(path, "Usage: uru api <path> [-X POST] [-F k=v] [--body-json {...}]"),
|
|
957
|
-
method,
|
|
958
|
-
...body === undefined ? {} : { body }
|
|
959
|
-
};
|
|
960
|
-
}
|
|
961
|
-
function normalizeApiPath(value) {
|
|
962
|
-
const trimmed = value.trim();
|
|
963
|
-
if (trimmed.startsWith("/")) {
|
|
964
|
-
return trimmed;
|
|
965
|
-
}
|
|
966
|
-
return `/api/${trimmed.replace(/^api\//, "")}`;
|
|
967
|
-
}
|
|
968
|
-
function addApiField(target, raw) {
|
|
969
|
-
const equals = raw.indexOf("=");
|
|
970
|
-
if (equals <= 0) {
|
|
971
|
-
throw new CliError("-F/--field requires key=value");
|
|
972
|
-
}
|
|
973
|
-
target[raw.slice(0, equals)] = raw.slice(equals + 1);
|
|
974
|
-
}
|
|
975
|
-
function paramsJson(args) {
|
|
976
|
-
for (let index = 0;index < args.length; index += 1) {
|
|
977
|
-
const arg = args[index];
|
|
978
|
-
if (arg === "--params-json" || arg === "--args-json") {
|
|
979
|
-
const value = args[index + 1];
|
|
980
|
-
if (value === undefined) {
|
|
981
|
-
throw new CliError(`${arg} requires a JSON object`);
|
|
982
|
-
}
|
|
983
|
-
return parseParamsJson(value, arg);
|
|
984
|
-
}
|
|
985
|
-
if (arg?.startsWith("--params-json=")) {
|
|
986
|
-
return parseParamsJson(arg.slice("--params-json=".length), "--params-json");
|
|
987
|
-
}
|
|
988
|
-
if (arg?.startsWith("--args-json=")) {
|
|
989
|
-
return parseParamsJson(arg.slice("--args-json=".length), "--args-json");
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
return {};
|
|
993
|
-
}
|
|
994
|
-
function parseParamsJson(value, label) {
|
|
995
|
-
try {
|
|
996
|
-
return parseJsonObject(value, label);
|
|
997
|
-
} catch (error) {
|
|
998
|
-
throw new CliError(error instanceof Error ? error.message : `Invalid ${label}`, {
|
|
999
|
-
cause: error
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
function parseLocalFlags(args) {
|
|
1004
|
-
const values = {};
|
|
1005
|
-
const booleans = new Set;
|
|
1006
|
-
const positionals = [];
|
|
1007
|
-
for (let index = 0;index < args.length; index += 1) {
|
|
1008
|
-
const arg = args[index];
|
|
1009
|
-
if (arg === undefined) {
|
|
1010
|
-
continue;
|
|
1011
|
-
}
|
|
1012
|
-
if (!arg.startsWith("--")) {
|
|
1013
|
-
positionals.push(arg);
|
|
1014
|
-
continue;
|
|
1015
|
-
}
|
|
1016
|
-
const equals = arg.indexOf("=");
|
|
1017
|
-
if (equals > -1) {
|
|
1018
|
-
values[arg.slice(0, equals)] = arg.slice(equals + 1);
|
|
1019
|
-
continue;
|
|
1020
|
-
}
|
|
1021
|
-
const next = args[index + 1];
|
|
1022
|
-
if (next !== undefined && !next.startsWith("--")) {
|
|
1023
|
-
values[arg] = next;
|
|
1024
|
-
index += 1;
|
|
1025
|
-
} else {
|
|
1026
|
-
booleans.add(arg);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
return { values, booleans, positionals };
|
|
1030
|
-
}
|
|
1031
|
-
function integerFlag(flags, name, fallback) {
|
|
1032
|
-
const raw = flags.values[name];
|
|
1033
|
-
if (raw === undefined) {
|
|
1034
|
-
return fallback;
|
|
1035
|
-
}
|
|
1036
|
-
const value = Number.parseInt(raw, 10);
|
|
1037
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
1038
|
-
throw new CliError(`${name} must be a non-negative integer`);
|
|
1039
|
-
}
|
|
1040
|
-
return value;
|
|
1041
|
-
}
|
|
1042
|
-
function copyStringFlag(flags, target, flagName, paramName) {
|
|
1043
|
-
const value = flags.values[flagName];
|
|
1044
|
-
if (value !== undefined) {
|
|
1045
|
-
target[paramName] = value;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
function isJsonRequested(argv) {
|
|
1049
|
-
for (let index = 0;index < argv.length; index += 1) {
|
|
1050
|
-
const arg = argv[index];
|
|
1051
|
-
if (arg === "--json") {
|
|
1052
|
-
return true;
|
|
1053
|
-
}
|
|
1054
|
-
if (arg === "--output-format=json" || arg === "--output-format=stream-json" || arg === "--output=json" || arg === "--output=stream-json") {
|
|
1055
|
-
return true;
|
|
1056
|
-
}
|
|
1057
|
-
if (arg === "--output-format" || arg === "--output") {
|
|
1058
|
-
const value = argv[index + 1];
|
|
1059
|
-
if (value === "json" || value === "stream-json") {
|
|
1060
|
-
return true;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
return false;
|
|
1065
|
-
}
|
|
1066
|
-
function nonEmptyString(value) {
|
|
1067
|
-
if (value === undefined) {
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
const trimmed = value.trim();
|
|
1071
|
-
return trimmed === "" ? undefined : trimmed;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
990
|
// src/output.ts
|
|
1075
991
|
function writeJson(io, value) {
|
|
1076
992
|
io.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
@@ -1164,19 +1080,18 @@ CORE
|
|
|
1164
1080
|
GEMS
|
|
1165
1081
|
uru init <library/name.gem> Initialize a Gem project
|
|
1166
1082
|
uru build [library/name.gem] --dry-run --outdir Build/validate and export build manifest
|
|
1167
|
-
uru deploy [library/name.gem] [--prod] [--dry-run] Server-side build/deploy/publish
|
|
1168
|
-
uru deploy [library/name.gem] --skip-promote Deploy without promoting the published link
|
|
1169
|
-
uru promote <version-id> [library/name.gem] Promote a staged Gem version/deployment
|
|
1170
1083
|
uru status [library/name.gem] Local source status versus source lock
|
|
1171
1084
|
uru logs [library/name.gem] [--limit 100] [--follow]
|
|
1172
1085
|
Runtime logs
|
|
1173
|
-
uru open [library/name.gem]
|
|
1174
|
-
uru rollback [<version-id>] [library/name.gem] --yes
|
|
1175
|
-
Roll back a live Gem link
|
|
1086
|
+
uru open [library/name.gem] --link <id|name> Print one explicit Link URL
|
|
1176
1087
|
uru gems inspect|validate|build|status [library/name.gem]
|
|
1177
|
-
uru gems
|
|
1178
|
-
uru gems versions
|
|
1179
|
-
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>]
|
|
1180
1095
|
uru gems read <library/name.gem> <file>
|
|
1181
1096
|
uru gems write <library/name.gem> <file> --file ./local.html
|
|
1182
1097
|
|
|
@@ -1328,23 +1243,7 @@ Initialize a Gem project in Library and link the current directory.
|
|
|
1328
1243
|
build: `Usage: uru build [library/name.gem] [--dry-run] [--outdir <dir>] [--json]
|
|
1329
1244
|
|
|
1330
1245
|
Validate/build the current Gem through the server-side build pipeline. Use
|
|
1331
|
-
--dry-run --outdir to inspect the build manifest without
|
|
1332
|
-
`,
|
|
1333
|
-
deploy: `Usage: uru deploy [library/name.gem] [--prod] [--skip-promote] [--no-wait] [--dry-run] [--json]
|
|
1334
|
-
|
|
1335
|
-
Build and deploy a Gem through Uru's server-side lifecycle. The backend chooses
|
|
1336
|
-
Cloudflare/Fly/fallback from the Gem's compiled runtime contract; the CLI does
|
|
1337
|
-
not let users hand-pick providers. Deploy publishes/promotes by default unless
|
|
1338
|
-
--skip-promote is supplied. Use --prod --skip-promote for a staged production
|
|
1339
|
-
deployment that can be promoted after checks; --prod cannot be combined with
|
|
1340
|
-
--no-publish.
|
|
1341
|
-
`,
|
|
1342
|
-
promote: `Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
|
|
1343
|
-
uru promote --deployment-id <deployment-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
|
|
1344
|
-
|
|
1345
|
-
Promote an already-staged immutable Gem version or ready deployment. With
|
|
1346
|
-
--link-id, this is a published-link pointer flip after checks pass. If --link-id
|
|
1347
|
-
is omitted, Uru promotes through the default clean Gem link.
|
|
1246
|
+
--dry-run --outdir to inspect the build manifest without creating a Version.
|
|
1348
1247
|
`,
|
|
1349
1248
|
pull: `Usage: uru pull [library/name.gem] [--force]
|
|
1350
1249
|
|
|
@@ -1365,43 +1264,63 @@ Upload local Gem source with source-version CAS. Without a matching lock, push
|
|
|
1365
1264
|
fails instead of silently overwriting remote edits. Generated directories,
|
|
1366
1265
|
node_modules, VCS metadata, and local env files are excluded.
|
|
1367
1266
|
`,
|
|
1368
|
-
logs: `Usage: uru logs [library/name.gem] [--limit 100] [--follow]
|
|
1267
|
+
logs: `Usage: uru logs [library/name.gem] [--release <id>|--run <id>] [--limit 100] [--follow]
|
|
1369
1268
|
|
|
1370
|
-
Print runtime
|
|
1269
|
+
Print bounded runtime logs for the Gem, one logical Release, or one Run. With --follow,
|
|
1371
1270
|
the CLI polls for a bounded 5 minutes; text status goes to stderr and log lines
|
|
1372
1271
|
go to stdout. Use global --output stream-json or --output-format stream-json
|
|
1373
1272
|
before the command to emit NDJSON events.
|
|
1374
1273
|
`,
|
|
1375
|
-
open: `Usage: uru open [library/name.gem]
|
|
1376
|
-
|
|
1377
|
-
Print the first available hosted/published Gem URL.
|
|
1378
|
-
`,
|
|
1379
|
-
rollback: `Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes
|
|
1274
|
+
open: `Usage: uru open [library/name.gem] [--link <link-id-or-name>]
|
|
1380
1275
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
mutates live traffic. If --link-id is omitted, Uru rolls back through the active
|
|
1384
|
-
Primary Gem link.
|
|
1276
|
+
Print one explicit active Link URL. When a Gem has more than one active Link,
|
|
1277
|
+
use --link to choose the Link rather than guessing which URL you mean.
|
|
1385
1278
|
`,
|
|
1386
|
-
gems: `Usage: uru gems <inspect|validate|build|status|read|write|versions|
|
|
1279
|
+
gems: `Usage: uru gems <inspect|validate|build|status|read|write|edits|versions|releases|links|runs|logs> [...]
|
|
1387
1280
|
|
|
1388
1281
|
Dedicated Gem lifecycle and filesystem commands. These are adapters over the
|
|
1389
1282
|
same platform operations used by MCP and the web UI.
|
|
1390
1283
|
`,
|
|
1391
|
-
"gems versions": `Usage: uru gems versions
|
|
1392
|
-
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>
|
|
1296
|
+
|
|
1297
|
+
Inspect coherent changes in Edit history. Internal source snapshots are available
|
|
1298
|
+
only through diagnostics and are not a normal lifecycle command.
|
|
1299
|
+
`,
|
|
1300
|
+
"gems releases": `Usage: uru gems releases list [library/name.gem] [--cursor <cursor>] [--limit <n>]
|
|
1301
|
+
uru gems releases show [library/name.gem] <release>
|
|
1302
|
+
uru gems releases create [library/name.gem] <version>
|
|
1303
|
+
uru gems releases retry [library/name.gem] <release>
|
|
1304
|
+
|
|
1305
|
+
Create and inspect hosted Releases for explicit Versions. A Release never changes
|
|
1306
|
+
a Link. Embedded Gems are link-ready without a Release.
|
|
1307
|
+
`,
|
|
1308
|
+
"gems links": `Usage: uru gems links list [library/name.gem]
|
|
1309
|
+
uru gems links create [library/name.gem] --name <name> --version <version>
|
|
1310
|
+
uru gems links set-version [library/name.gem] <link> <version>
|
|
1311
|
+
uru gems links set-access [library/name.gem] <link> --gate <gate>
|
|
1312
|
+
uru gems links set-notifications [library/name.gem] <link> <mode>
|
|
1313
|
+
uru gems links rotate|revoke [library/name.gem] <link>
|
|
1393
1314
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
percentages are accepted only with --skip-promote to stage rollout intent; live
|
|
1397
|
-
promote/rollback requires 100 until weighted link routing is enabled.
|
|
1315
|
+
Each Link independently selects one frozen Version. Changing one Link does not
|
|
1316
|
+
move any other Link.
|
|
1398
1317
|
`,
|
|
1399
|
-
"gems
|
|
1400
|
-
uru gems
|
|
1318
|
+
"gems runs": `Usage: uru gems runs list [library/name.gem] [--cursor <cursor>] [--limit <n>] [--status <status>]
|
|
1319
|
+
uru gems runs show [library/name.gem] <run>
|
|
1320
|
+
uru gems runs cancel|retry [library/name.gem] <run>
|
|
1401
1321
|
|
|
1402
|
-
Inspect
|
|
1403
|
-
|
|
1404
|
-
the check a release gate; security also accepts --deep.
|
|
1322
|
+
Inspect and control bounded Gem executions. Builds and Release attempts are not
|
|
1323
|
+
Runs. Gems without a bounded execution capability do not have Runs.
|
|
1405
1324
|
`,
|
|
1406
1325
|
library: `Usage: uru library ls [path]
|
|
1407
1326
|
uru library search <query>
|
|
@@ -1581,7 +1500,14 @@ async function executeOperationStream(ctx, operationId, params, onEvent) {
|
|
|
1581
1500
|
if (operation.backendOp !== undefined) {
|
|
1582
1501
|
toolParams["op"] = operation.backendOp;
|
|
1583
1502
|
}
|
|
1584
|
-
|
|
1503
|
+
const response = await client(ctx).streamTool(operation.backendToolName, toolParams, (event) => {
|
|
1504
|
+
assertToolResultSuccess(event);
|
|
1505
|
+
onEvent(event);
|
|
1506
|
+
});
|
|
1507
|
+
if (response.payload !== undefined) {
|
|
1508
|
+
assertToolResultSuccess(response.payload);
|
|
1509
|
+
}
|
|
1510
|
+
return response;
|
|
1585
1511
|
}
|
|
1586
1512
|
async function executeOperation(ctx, operationId, params) {
|
|
1587
1513
|
const operation = getPlatformOperation(operationId);
|
|
@@ -1592,7 +1518,9 @@ async function executeOperation(ctx, operationId, params) {
|
|
|
1592
1518
|
if (operation.backendOp !== undefined) {
|
|
1593
1519
|
toolParams["op"] = operation.backendOp;
|
|
1594
1520
|
}
|
|
1595
|
-
|
|
1521
|
+
const result = await client(ctx).runTool(operation.backendToolName, toolParams);
|
|
1522
|
+
assertToolResultSuccess(result);
|
|
1523
|
+
return result;
|
|
1596
1524
|
}
|
|
1597
1525
|
function emit(ctx, value, text) {
|
|
1598
1526
|
if (ctx.outputFormat === "stream-json") {
|
|
@@ -1604,19 +1532,51 @@ function emit(ctx, value, text) {
|
|
|
1604
1532
|
}
|
|
1605
1533
|
}
|
|
1606
1534
|
function unwrapToolResult(value) {
|
|
1607
|
-
|
|
1608
|
-
|
|
1535
|
+
assertToolResultSuccess(value);
|
|
1536
|
+
if (isJsonObject2(value) && Object.hasOwn(value, "result") && (isToolResultEnvelope(value) || isJsonObject2(value["result"]))) {
|
|
1537
|
+
const result = value["result"];
|
|
1538
|
+
if (result !== undefined) {
|
|
1539
|
+
return result;
|
|
1540
|
+
}
|
|
1609
1541
|
}
|
|
1610
1542
|
return value;
|
|
1611
1543
|
}
|
|
1544
|
+
function assertToolResultSuccess(value) {
|
|
1545
|
+
if (!isJsonObject2(value) || !isToolResultEnvelope(value)) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const httpStatus = toolResultHttpStatus(value);
|
|
1549
|
+
const failed = value["success"] === false || Object.hasOwn(value, "error") || Object.hasOwn(value, "errorMessage") || Object.hasOwn(value, "error_message") || Object.hasOwn(value, "errorCode") || Object.hasOwn(value, "error_code") || Object.hasOwn(value, "code") || httpStatus !== undefined && httpStatus >= 400;
|
|
1550
|
+
if (!failed) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
const errorCode = stringField(value, ["errorCode", "error_code", "code"]);
|
|
1554
|
+
const toolId = stringField(value, ["toolId", "tool_id"]);
|
|
1555
|
+
const message = stringField(value, ["errorMessage", "error_message", "error", "message"]) ?? `${toolId === undefined ? "Tool operation" : `Tool ${toolId}`} failed`;
|
|
1556
|
+
const details = [
|
|
1557
|
+
errorCode === undefined ? undefined : `[${errorCode}]`,
|
|
1558
|
+
httpStatus === undefined ? undefined : `(HTTP ${httpStatus})`
|
|
1559
|
+
].filter((detail) => detail !== undefined);
|
|
1560
|
+
const tip = stringField(value, ["tip"]);
|
|
1561
|
+
throw new CliError(`${message}${details.length === 0 ? "" : ` ${details.join(" ")}`}${tip === undefined ? "" : ` ${tip}`}`, {
|
|
1562
|
+
code: httpStatus === 409 ? ExitCode.Conflict : httpStatus === 401 || httpStatus === 403 ? ExitCode.AuthRequired : ExitCode.Failure,
|
|
1563
|
+
...errorCode === undefined ? {} : { errorCode }
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
function isToolResultEnvelope(value) {
|
|
1567
|
+
return typeof value["success"] === "boolean" && (typeof value["toolId"] === "string" || typeof value["tool_id"] === "string" || typeof value["executionTimeMs"] === "number" || typeof value["execution_time_ms"] === "number");
|
|
1568
|
+
}
|
|
1569
|
+
function toolResultHttpStatus(value) {
|
|
1570
|
+
const status = value["httpStatus"] ?? value["http_status"];
|
|
1571
|
+
return typeof status === "number" && Number.isFinite(status) ? status : undefined;
|
|
1572
|
+
}
|
|
1612
1573
|
function delegatedPayload(value) {
|
|
1613
1574
|
if (isJsonObject2(value) && isJsonObject2(value["payload"])) {
|
|
1614
1575
|
return value["payload"];
|
|
1615
1576
|
}
|
|
1616
1577
|
return value;
|
|
1617
1578
|
}
|
|
1618
|
-
function gemPathFromArgs(
|
|
1619
|
-
const flags = parseLocalFlags(args);
|
|
1579
|
+
function gemPathFromArgs(flags, linkedProject) {
|
|
1620
1580
|
return flags.values["--path"] ?? flags.values["--gem"] ?? flags.positionals.find((value) => value.startsWith("library/")) ?? linkedProject.libraryPath ?? linkedProject.gemId ?? (() => {
|
|
1621
1581
|
throw new CliError("Gem path required. Pass library/<name>.gem or run `uru link --path library/<name>.gem`.");
|
|
1622
1582
|
})();
|
|
@@ -1661,43 +1621,6 @@ function stringField(record, keys) {
|
|
|
1661
1621
|
}
|
|
1662
1622
|
return;
|
|
1663
1623
|
}
|
|
1664
|
-
function firstUrl(value) {
|
|
1665
|
-
if (typeof value === "string" && /^https?:\/\//.test(value)) {
|
|
1666
|
-
return value;
|
|
1667
|
-
}
|
|
1668
|
-
if (Array.isArray(value)) {
|
|
1669
|
-
for (const item of value) {
|
|
1670
|
-
const found = firstUrl(item);
|
|
1671
|
-
if (found !== undefined) {
|
|
1672
|
-
return found;
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
if (!isJsonObject2(value)) {
|
|
1678
|
-
return;
|
|
1679
|
-
}
|
|
1680
|
-
for (const key of [
|
|
1681
|
-
"published_url",
|
|
1682
|
-
"publishedUrl",
|
|
1683
|
-
"preview_url",
|
|
1684
|
-
"previewUrl",
|
|
1685
|
-
"url",
|
|
1686
|
-
"href"
|
|
1687
|
-
]) {
|
|
1688
|
-
const candidate = value[key];
|
|
1689
|
-
if (typeof candidate === "string" && /^https?:\/\//.test(candidate)) {
|
|
1690
|
-
return candidate;
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
for (const candidate of Object.values(value)) {
|
|
1694
|
-
const found = firstUrl(candidate);
|
|
1695
|
-
if (found !== undefined) {
|
|
1696
|
-
return found;
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
1624
|
function isJsonObject2(value) {
|
|
1702
1625
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1703
1626
|
}
|
|
@@ -1840,16 +1763,9 @@ function createMacosKeychainCredentialStore(runCommand) {
|
|
|
1840
1763
|
},
|
|
1841
1764
|
async writeToken(apiUrl, token) {
|
|
1842
1765
|
try {
|
|
1843
|
-
await runCommand("security", [
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
accountForApiUrl(apiUrl),
|
|
1847
|
-
"-s",
|
|
1848
|
-
KEYCHAIN_SERVICE,
|
|
1849
|
-
"-w",
|
|
1850
|
-
token,
|
|
1851
|
-
"-U"
|
|
1852
|
-
]);
|
|
1766
|
+
await runCommand("security", ["-i"], {
|
|
1767
|
+
input: macosKeychainWriteCommand(apiUrl, token)
|
|
1768
|
+
});
|
|
1853
1769
|
} catch (error) {
|
|
1854
1770
|
throw credentialStoreError("macOS Keychain", error);
|
|
1855
1771
|
}
|
|
@@ -1871,6 +1787,11 @@ function createMacosKeychainCredentialStore(runCommand) {
|
|
|
1871
1787
|
}
|
|
1872
1788
|
};
|
|
1873
1789
|
}
|
|
1790
|
+
function macosKeychainWriteCommand(apiUrl, token) {
|
|
1791
|
+
const tokenHex = Buffer.from(token, "utf8").toString("hex");
|
|
1792
|
+
return `add-generic-password -a "${accountForApiUrl(apiUrl)}" -s "${KEYCHAIN_SERVICE}" -X ${tokenHex} -U
|
|
1793
|
+
`;
|
|
1794
|
+
}
|
|
1874
1795
|
function createLinuxSecretToolCredentialStore(runCommand) {
|
|
1875
1796
|
return {
|
|
1876
1797
|
async readToken(apiUrl) {
|
|
@@ -2060,7 +1981,6 @@ function windowsCredentialNativePowerShell() {
|
|
|
2060
1981
|
Add-Type -TypeDefinition @"
|
|
2061
1982
|
using System;
|
|
2062
1983
|
using System.Runtime.InteropServices;
|
|
2063
|
-
using System.Runtime.InteropServices.ComTypes;
|
|
2064
1984
|
|
|
2065
1985
|
public static class UruCredentialNative
|
|
2066
1986
|
{
|
|
@@ -2071,7 +1991,7 @@ public static class UruCredentialNative
|
|
|
2071
1991
|
public UInt32 Type;
|
|
2072
1992
|
public string TargetName;
|
|
2073
1993
|
public string Comment;
|
|
2074
|
-
public FILETIME LastWritten;
|
|
1994
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
2075
1995
|
public UInt32 CredentialBlobSize;
|
|
2076
1996
|
public IntPtr CredentialBlob;
|
|
2077
1997
|
public UInt32 Persist;
|
|
@@ -2185,6 +2105,12 @@ var ERROR_TIPS = {
|
|
|
2185
2105
|
oauth_request_failed: "Check the API URL and network access, then retry `uru login`; use `uru login --device` from SSH/headless environments.",
|
|
2186
2106
|
oauth_device_failed: "Restart `uru login --device` and complete authorization in the browser before the code expires.",
|
|
2187
2107
|
oauth_device_timeout: "Run `uru login --device` again and complete browser authorization before the timeout.",
|
|
2108
|
+
oauth_refresh_invalid: "Run `uru login` again; the stored renewable login has expired or was revoked.",
|
|
2109
|
+
oauth_refresh_token_missing: "Run `uru login` again; the OAuth server did not issue the renewable credential required by the CLI.",
|
|
2110
|
+
oauth_resource_mismatch: "Check the configured API URL, then restart `uru login`; the OAuth server returned a different protected resource.",
|
|
2111
|
+
oauth_invalid_token_type: "Restart `uru login`; the OAuth server returned an unsupported access-token type.",
|
|
2112
|
+
oauth_logout_revocation_failed: "Local credentials were cleared. Check network access before logging in again if remote revocation must be confirmed.",
|
|
2113
|
+
stored_oauth_credential_invalid: "Run `uru logout`, then `uru login` to replace the unreadable stored OAuth credential.",
|
|
2188
2114
|
cancelled: "The command was cancelled; rerun it only if you still want the operation to continue."
|
|
2189
2115
|
};
|
|
2190
2116
|
function cliErrorJson(error) {
|
|
@@ -2256,8 +2182,8 @@ async function readSourceLock(cwd) {
|
|
|
2256
2182
|
return {};
|
|
2257
2183
|
}
|
|
2258
2184
|
const lock = {};
|
|
2259
|
-
if (typeof parsed["
|
|
2260
|
-
lock.
|
|
2185
|
+
if (typeof parsed["sourceRevisionId"] === "string") {
|
|
2186
|
+
lock.sourceRevisionId = parsed["sourceRevisionId"];
|
|
2261
2187
|
}
|
|
2262
2188
|
if (isRecord(parsed["files"])) {
|
|
2263
2189
|
lock.files = Object.fromEntries(Object.entries(parsed["files"]).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
|
|
@@ -2276,7 +2202,7 @@ async function readSourceLock(cwd) {
|
|
|
2276
2202
|
async function writeSourceLock(cwd, lock) {
|
|
2277
2203
|
const path = sourceLockPath(cwd);
|
|
2278
2204
|
const sanitized = {
|
|
2279
|
-
...lock.
|
|
2205
|
+
...lock.sourceRevisionId === undefined ? {} : { sourceRevisionId: lock.sourceRevisionId },
|
|
2280
2206
|
files: stableStringRecord(lock.files ?? {}),
|
|
2281
2207
|
...lock.checkedOutAt === undefined ? {} : { checkedOutAt: lock.checkedOutAt }
|
|
2282
2208
|
};
|
|
@@ -2369,7 +2295,8 @@ var SKIPPED_SOURCE_PATH_SEGMENTS = new Set([
|
|
|
2369
2295
|
"build",
|
|
2370
2296
|
"coverage",
|
|
2371
2297
|
"tmp",
|
|
2372
|
-
"temp"
|
|
2298
|
+
"temp",
|
|
2299
|
+
"__MACOSX"
|
|
2373
2300
|
]);
|
|
2374
2301
|
var SKIPPED_SOURCE_FILE_NAMES = new Set([
|
|
2375
2302
|
".DS_Store",
|
|
@@ -2382,7 +2309,8 @@ var SKIPPED_SOURCE_FILE_NAMES = new Set([
|
|
|
2382
2309
|
]);
|
|
2383
2310
|
function shouldSkipProjectPath(path) {
|
|
2384
2311
|
const segments = path.split("/");
|
|
2385
|
-
|
|
2312
|
+
const name = segments.at(-1) ?? "";
|
|
2313
|
+
return segments.some((segment) => SKIPPED_SOURCE_PATH_SEGMENTS.has(segment)) || SKIPPED_SOURCE_FILE_NAMES.has(name) || name.startsWith("._");
|
|
2386
2314
|
}
|
|
2387
2315
|
function stableStringRecord(record) {
|
|
2388
2316
|
return Object.fromEntries(Object.entries(record).filter(([, value]) => typeof value === "string" && value.trim() !== "").sort(([left], [right]) => compareStrings(left, right)));
|
|
@@ -2437,11 +2365,12 @@ async function maybeNotifyCliUpdate(ctx, options = {}) {
|
|
|
2437
2365
|
if (!shouldCheckForUpdate(ctx, options.env ?? process.env)) {
|
|
2438
2366
|
return;
|
|
2439
2367
|
}
|
|
2368
|
+
const currentConfig = await readConfigBestEffort(ctx);
|
|
2440
2369
|
const now = options.now?.() ?? Date.now();
|
|
2441
|
-
if (!isUpdateCheckDue(
|
|
2370
|
+
if (!isUpdateCheckDue(currentConfig, now)) {
|
|
2442
2371
|
return;
|
|
2443
2372
|
}
|
|
2444
|
-
const checkedConfig = withUpdateCheckTimestamp(
|
|
2373
|
+
const checkedConfig = withUpdateCheckTimestamp(currentConfig, now);
|
|
2445
2374
|
await writeConfigBestEffort(ctx, checkedConfig);
|
|
2446
2375
|
const latestVersion = await fetchLatestVersion({
|
|
2447
2376
|
fetchImpl: ctx.fetch ?? fetch,
|
|
@@ -2463,6 +2392,13 @@ async function maybeNotifyCliUpdate(ctx, options = {}) {
|
|
|
2463
2392
|
ctx.io.stderr.write(`A newer Uru CLI is available: ${CLI_VERSION} → ${latestVersion}. ` + `Run npm install -g ${PACKAGE_NAME}@latest
|
|
2464
2393
|
`);
|
|
2465
2394
|
}
|
|
2395
|
+
async function readConfigBestEffort(ctx) {
|
|
2396
|
+
try {
|
|
2397
|
+
return await ctx.store.read();
|
|
2398
|
+
} catch {
|
|
2399
|
+
return ctx.config;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2466
2402
|
function shouldCheckForUpdate(ctx, env) {
|
|
2467
2403
|
return ctx.outputFormat === "text" && ctx.terminal.stderrIsTty && !ctx.terminal.ci && env["NO_UPDATE_NOTIFIER"] === undefined && env["URU_NO_UPDATE_NOTIFIER"] === undefined;
|
|
2468
2404
|
}
|
|
@@ -2542,33 +2478,219 @@ function isRecord2(value) {
|
|
|
2542
2478
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2543
2479
|
}
|
|
2544
2480
|
|
|
2545
|
-
// src/
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
function
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2481
|
+
// src/cli-helpers.ts
|
|
2482
|
+
var BOOLEAN_LOCAL_FLAGS = new Set([
|
|
2483
|
+
"--blocking",
|
|
2484
|
+
"--create",
|
|
2485
|
+
"--deep",
|
|
2486
|
+
"--dry-run",
|
|
2487
|
+
"--follow",
|
|
2488
|
+
"--force",
|
|
2489
|
+
"--force-build",
|
|
2490
|
+
"--hard-delete",
|
|
2491
|
+
"--no-create",
|
|
2492
|
+
"--no-wait",
|
|
2493
|
+
"--permanent",
|
|
2494
|
+
"--prod",
|
|
2495
|
+
"--stdin",
|
|
2496
|
+
"--value-stdin",
|
|
2497
|
+
"--yes"
|
|
2498
|
+
]);
|
|
2499
|
+
function parseApiArgs(args) {
|
|
2500
|
+
let method = "GET";
|
|
2501
|
+
let path;
|
|
2502
|
+
let body;
|
|
2503
|
+
const fields = {};
|
|
2504
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
2505
|
+
const arg = args[index];
|
|
2506
|
+
if (arg === undefined) {
|
|
2507
|
+
continue;
|
|
2508
|
+
}
|
|
2509
|
+
if (arg === "-X" || arg === "--method") {
|
|
2510
|
+
method = requireCommandValue(args[index + 1], `${arg} requires a method`).toUpperCase();
|
|
2511
|
+
index += 1;
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
if (arg.startsWith("-X") && arg.length > 2) {
|
|
2515
|
+
method = arg.slice(2).toUpperCase();
|
|
2516
|
+
continue;
|
|
2517
|
+
}
|
|
2518
|
+
if (arg === "--body-json" || arg === "--data-json") {
|
|
2519
|
+
body = parseParamsJson(requireCommandValue(args[index + 1], `${arg} requires a JSON object`), arg);
|
|
2520
|
+
index += 1;
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (arg === "-F" || arg === "--field") {
|
|
2524
|
+
addApiField(fields, requireCommandValue(args[index + 1], `${arg} requires key=value`));
|
|
2525
|
+
index += 1;
|
|
2526
|
+
continue;
|
|
2527
|
+
}
|
|
2528
|
+
if (arg.startsWith("-F") && arg.length > 2) {
|
|
2529
|
+
addApiField(fields, arg.slice(2));
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
if (path === undefined) {
|
|
2533
|
+
path = normalizeApiPath(arg);
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
throw new CliError(`Unexpected api argument: ${arg}`);
|
|
2537
|
+
}
|
|
2538
|
+
if (Object.keys(fields).length > 0) {
|
|
2539
|
+
body = { ...body ?? {}, ...fields };
|
|
2540
|
+
if (method === "GET") {
|
|
2541
|
+
method = "POST";
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return {
|
|
2545
|
+
path: requireCommandValue(path, "Usage: uru api <path> [-X POST] [-F k=v] [--body-json {...}]"),
|
|
2546
|
+
method,
|
|
2547
|
+
...body === undefined ? {} : { body }
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
function normalizeApiPath(value) {
|
|
2551
|
+
const trimmed = value.trim();
|
|
2552
|
+
if (trimmed.startsWith("/")) {
|
|
2553
|
+
return trimmed;
|
|
2554
|
+
}
|
|
2555
|
+
return `/api/${trimmed.replace(/^api\//, "")}`;
|
|
2556
|
+
}
|
|
2557
|
+
function addApiField(target, raw) {
|
|
2558
|
+
const equals = raw.indexOf("=");
|
|
2559
|
+
if (equals <= 0) {
|
|
2560
|
+
throw new CliError("-F/--field requires key=value");
|
|
2561
|
+
}
|
|
2562
|
+
target[raw.slice(0, equals)] = raw.slice(equals + 1);
|
|
2563
|
+
}
|
|
2564
|
+
function paramsJson(args) {
|
|
2565
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
2566
|
+
const arg = args[index];
|
|
2567
|
+
if (arg === "--params-json" || arg === "--args-json") {
|
|
2568
|
+
const value = args[index + 1];
|
|
2569
|
+
if (value === undefined) {
|
|
2570
|
+
throw new CliError(`${arg} requires a JSON object`);
|
|
2571
|
+
}
|
|
2572
|
+
return parseParamsJson(value, arg);
|
|
2573
|
+
}
|
|
2574
|
+
if (arg?.startsWith("--params-json=")) {
|
|
2575
|
+
return parseParamsJson(arg.slice("--params-json=".length), "--params-json");
|
|
2576
|
+
}
|
|
2577
|
+
if (arg?.startsWith("--args-json=")) {
|
|
2578
|
+
return parseParamsJson(arg.slice("--args-json=".length), "--args-json");
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
return {};
|
|
2582
|
+
}
|
|
2583
|
+
function parseParamsJson(value, label) {
|
|
2584
|
+
try {
|
|
2585
|
+
return parseJsonObject(value, label);
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
throw new CliError(error instanceof Error ? error.message : `Invalid ${label}`, {
|
|
2588
|
+
cause: error
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
function parseLocalFlags(args) {
|
|
2593
|
+
const values = {};
|
|
2594
|
+
const booleans = new Set;
|
|
2595
|
+
const positionals = [];
|
|
2596
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
2597
|
+
const arg = args[index];
|
|
2598
|
+
if (arg === undefined) {
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (!arg.startsWith("--")) {
|
|
2602
|
+
positionals.push(arg);
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
const equals = arg.indexOf("=");
|
|
2606
|
+
if (equals > -1) {
|
|
2607
|
+
values[arg.slice(0, equals)] = arg.slice(equals + 1);
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (BOOLEAN_LOCAL_FLAGS.has(arg)) {
|
|
2611
|
+
booleans.add(arg);
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
const next = args[index + 1];
|
|
2615
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
2616
|
+
values[arg] = next;
|
|
2617
|
+
index += 1;
|
|
2618
|
+
} else {
|
|
2619
|
+
booleans.add(arg);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return { values, booleans, positionals };
|
|
2623
|
+
}
|
|
2624
|
+
function integerFlag(flags, name, fallback) {
|
|
2625
|
+
const raw = flags.values[name];
|
|
2626
|
+
if (raw === undefined) {
|
|
2627
|
+
return fallback;
|
|
2628
|
+
}
|
|
2629
|
+
const value = Number.parseInt(raw, 10);
|
|
2630
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2631
|
+
throw new CliError(`${name} must be a non-negative integer`);
|
|
2632
|
+
}
|
|
2633
|
+
return value;
|
|
2634
|
+
}
|
|
2635
|
+
function copyStringFlag(flags, target, flagName, paramName) {
|
|
2636
|
+
const value = flags.values[flagName];
|
|
2637
|
+
if (value !== undefined) {
|
|
2638
|
+
target[paramName] = value;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
function isJsonRequested(argv) {
|
|
2642
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
2643
|
+
const arg = argv[index];
|
|
2644
|
+
if (arg === "--json") {
|
|
2645
|
+
return true;
|
|
2646
|
+
}
|
|
2647
|
+
if (arg === "--output-format=json" || arg === "--output-format=stream-json" || arg === "--output=json" || arg === "--output=stream-json") {
|
|
2648
|
+
return true;
|
|
2649
|
+
}
|
|
2650
|
+
if (arg === "--output-format" || arg === "--output") {
|
|
2651
|
+
const value = argv[index + 1];
|
|
2652
|
+
if (value === "json" || value === "stream-json") {
|
|
2653
|
+
return true;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
return false;
|
|
2658
|
+
}
|
|
2659
|
+
function nonEmptyString(value) {
|
|
2660
|
+
if (value === undefined) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
const trimmed = value.trim();
|
|
2664
|
+
return trimmed === "" ? undefined : trimmed;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
2667
|
+
// src/auth-commands.ts
|
|
2668
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
2669
|
+
import { createServer } from "node:http";
|
|
2670
|
+
|
|
2671
|
+
// src/oauth.ts
|
|
2672
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
2673
|
+
var DEFAULT_PKCE_VERIFIER_BYTES = 32;
|
|
2674
|
+
var DEFAULT_STATE_BYTES = 24;
|
|
2675
|
+
var OAUTH_DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2676
|
+
var DEVICE_POLL_SLOW_DOWN_SECONDS = 5;
|
|
2677
|
+
function generatePkcePair() {
|
|
2678
|
+
const verifier = tokenUrlSafe(DEFAULT_PKCE_VERIFIER_BYTES);
|
|
2679
|
+
return {
|
|
2680
|
+
verifier,
|
|
2681
|
+
challenge: pkceChallengeS256(verifier),
|
|
2682
|
+
method: "S256"
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
function generateOAuthState() {
|
|
2686
|
+
return tokenUrlSafe(DEFAULT_STATE_BYTES);
|
|
2687
|
+
}
|
|
2688
|
+
function pkceChallengeS256(verifier) {
|
|
2689
|
+
if (!isValidPkceVerifier(verifier)) {
|
|
2690
|
+
throw new CliError("PKCE code verifier must be 43-128 RFC7636 unreserved characters.");
|
|
2691
|
+
}
|
|
2692
|
+
return createHash2("sha256").update(verifier).digest("base64url");
|
|
2693
|
+
}
|
|
2572
2694
|
function buildLoopbackRedirectUri(args) {
|
|
2573
2695
|
const host = args.host.trim();
|
|
2574
2696
|
if (host === "") {
|
|
@@ -2641,6 +2763,19 @@ async function registerOAuthClient(args) {
|
|
|
2641
2763
|
scope: optionalStringField(response, "scope") ?? "mcp"
|
|
2642
2764
|
};
|
|
2643
2765
|
}
|
|
2766
|
+
async function discoverOAuthResource(args) {
|
|
2767
|
+
const response = await fetchOAuthGet({
|
|
2768
|
+
apiUrl: args.apiUrl,
|
|
2769
|
+
path: "/.well-known/oauth-protected-resource",
|
|
2770
|
+
...args.fetch === undefined ? {} : { fetch: args.fetch }
|
|
2771
|
+
});
|
|
2772
|
+
if (!isRecord(response)) {
|
|
2773
|
+
throw new CliError("Unexpected OAuth protected-resource metadata response.");
|
|
2774
|
+
}
|
|
2775
|
+
const resource = stringField2(response, "resource");
|
|
2776
|
+
assertHttpUrl(resource, "OAuth protected resource");
|
|
2777
|
+
return resource;
|
|
2778
|
+
}
|
|
2644
2779
|
async function exchangeAuthorizationCode(args) {
|
|
2645
2780
|
const body = new URLSearchParams;
|
|
2646
2781
|
body.set("grant_type", "authorization_code");
|
|
@@ -2659,6 +2794,33 @@ async function exchangeAuthorizationCode(args) {
|
|
|
2659
2794
|
});
|
|
2660
2795
|
return normalizeOAuthTokenResponse(response);
|
|
2661
2796
|
}
|
|
2797
|
+
async function refreshOAuthToken(args) {
|
|
2798
|
+
const body = new URLSearchParams;
|
|
2799
|
+
body.set("grant_type", "refresh_token");
|
|
2800
|
+
body.set("client_id", args.clientId);
|
|
2801
|
+
body.set("refresh_token", args.refreshToken);
|
|
2802
|
+
body.set("resource", args.resource);
|
|
2803
|
+
const response = await fetchOAuthForm({
|
|
2804
|
+
apiUrl: args.apiUrl,
|
|
2805
|
+
path: "/oauth/token",
|
|
2806
|
+
body,
|
|
2807
|
+
rejectedGrantErrorCode: "oauth_refresh_invalid",
|
|
2808
|
+
...args.fetch === undefined ? {} : { fetch: args.fetch }
|
|
2809
|
+
});
|
|
2810
|
+
return normalizeOAuthTokenResponse(response);
|
|
2811
|
+
}
|
|
2812
|
+
async function revokeOAuthToken(args) {
|
|
2813
|
+
const body = new URLSearchParams;
|
|
2814
|
+
body.set("token", args.token);
|
|
2815
|
+
body.set("token_type_hint", "refresh_token");
|
|
2816
|
+
body.set("client_id", args.clientId);
|
|
2817
|
+
await fetchOAuthForm({
|
|
2818
|
+
apiUrl: args.apiUrl,
|
|
2819
|
+
path: "/oauth/revoke",
|
|
2820
|
+
body,
|
|
2821
|
+
...args.fetch === undefined ? {} : { fetch: args.fetch }
|
|
2822
|
+
});
|
|
2823
|
+
}
|
|
2662
2824
|
async function exchangeOAuthForCliSession(args) {
|
|
2663
2825
|
const body = {};
|
|
2664
2826
|
if (args.workspaceId !== undefined && args.workspaceId.trim() !== "") {
|
|
@@ -2683,6 +2845,7 @@ async function exchangeOAuthForCliSession(args) {
|
|
|
2683
2845
|
accessToken: stringField2(response, "access_token"),
|
|
2684
2846
|
tokenType,
|
|
2685
2847
|
expiresIn: numberField(response, "expires_in"),
|
|
2848
|
+
oauthResource: stringField2(response, "oauth_resource"),
|
|
2686
2849
|
...workspaceId === undefined ? {} : { workspaceId }
|
|
2687
2850
|
};
|
|
2688
2851
|
}
|
|
@@ -2760,6 +2923,18 @@ async function fetchOAuthJson(args) {
|
|
|
2760
2923
|
}
|
|
2761
2924
|
return payload;
|
|
2762
2925
|
}
|
|
2926
|
+
async function fetchOAuthGet(args) {
|
|
2927
|
+
const fetchImpl = args.fetch ?? fetch;
|
|
2928
|
+
const response = await fetchImpl(`${normalizeApiUrl(args.apiUrl)}${args.path}`, {
|
|
2929
|
+
method: "GET",
|
|
2930
|
+
headers: { Accept: "application/json" }
|
|
2931
|
+
});
|
|
2932
|
+
const payload = parseJsonResponse(await response.text());
|
|
2933
|
+
if (!response.ok) {
|
|
2934
|
+
throw oauthHttpError(response.status, payload);
|
|
2935
|
+
}
|
|
2936
|
+
return payload;
|
|
2937
|
+
}
|
|
2763
2938
|
function tokenUrlSafe(byteLength) {
|
|
2764
2939
|
return randomBytes(byteLength).toString("base64url");
|
|
2765
2940
|
}
|
|
@@ -2778,10 +2953,7 @@ async function fetchOAuthForm(args) {
|
|
|
2778
2953
|
});
|
|
2779
2954
|
const payload = parseJsonResponse(await response.text());
|
|
2780
2955
|
if (!response.ok && !(args.allowOAuthError === true && isOAuthError(payload))) {
|
|
2781
|
-
throw
|
|
2782
|
-
code: response.status === 401 || response.status === 403 ? ExitCode.AuthRequired : ExitCode.Failure,
|
|
2783
|
-
errorCode: "oauth_request_failed"
|
|
2784
|
-
});
|
|
2956
|
+
throw oauthHttpError(response.status, payload, args.rejectedGrantErrorCode);
|
|
2785
2957
|
}
|
|
2786
2958
|
return payload;
|
|
2787
2959
|
}
|
|
@@ -2836,14 +3008,47 @@ function normalizeOAuthTokenResponse(value) {
|
|
|
2836
3008
|
const refreshToken = optionalStringField(value, "refresh_token");
|
|
2837
3009
|
const scope = optionalStringField(value, "scope");
|
|
2838
3010
|
const expiresInRaw = value["expires_in"];
|
|
3011
|
+
const tokenType = stringField2(value, "token_type");
|
|
3012
|
+
if (tokenType !== "Bearer") {
|
|
3013
|
+
throw new CliError("OAuth token response used an unsupported token type.", {
|
|
3014
|
+
code: ExitCode.AuthRequired,
|
|
3015
|
+
errorCode: "oauth_invalid_token_type"
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
2839
3018
|
return {
|
|
2840
3019
|
accessToken: stringField2(value, "access_token"),
|
|
2841
|
-
tokenType
|
|
3020
|
+
tokenType,
|
|
2842
3021
|
...typeof expiresInRaw === "number" ? { expiresIn: expiresInRaw } : {},
|
|
2843
3022
|
...refreshToken === undefined ? {} : { refreshToken },
|
|
2844
3023
|
...scope === undefined ? {} : { scope }
|
|
2845
3024
|
};
|
|
2846
3025
|
}
|
|
3026
|
+
function oauthHttpError(status, payload, rejectedGrantErrorCode) {
|
|
3027
|
+
const oauthError = isRecord(payload) && typeof payload["error"] === "string" ? payload["error"] : null;
|
|
3028
|
+
const authRequired = status === 400 || status === 401 || status === 403 || status === 409;
|
|
3029
|
+
const grantRejected = rejectedGrantErrorCode !== undefined && (oauthError === "invalid_grant" || oauthError === "invalid_target" || oauthError === "invalid_client");
|
|
3030
|
+
return new CliError(errorMessage2(payload) ?? `OAuth request failed: HTTP ${status}`, {
|
|
3031
|
+
code: authRequired ? ExitCode.AuthRequired : ExitCode.Failure,
|
|
3032
|
+
errorCode: grantRejected ? rejectedGrantErrorCode : "oauth_request_failed"
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
function assertHttpUrl(value, label) {
|
|
3036
|
+
let url;
|
|
3037
|
+
try {
|
|
3038
|
+
url = new URL(value);
|
|
3039
|
+
} catch (error) {
|
|
3040
|
+
throw new CliError(`${label} is not a valid URL.`, {
|
|
3041
|
+
errorCode: "oauth_invalid_response",
|
|
3042
|
+
cause: error
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
const loopbackHostname = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
3046
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopbackHostname)) {
|
|
3047
|
+
throw new CliError(`${label} must use HTTPS except on loopback.`, {
|
|
3048
|
+
errorCode: "oauth_invalid_response"
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
2847
3052
|
function stringField2(value, key) {
|
|
2848
3053
|
const field = value[key];
|
|
2849
3054
|
if (typeof field !== "string" || field.trim() === "") {
|
|
@@ -2880,12 +3085,203 @@ function defaultSleep(milliseconds) {
|
|
|
2880
3085
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
2881
3086
|
}
|
|
2882
3087
|
|
|
3088
|
+
// src/oauth-credential.ts
|
|
3089
|
+
var OAUTH_CREDENTIAL_PREFIX = "uru_cli_oauth_v1:";
|
|
3090
|
+
var SESSION_REFRESH_LEEWAY_MS = 5 * 60 * 1000;
|
|
3091
|
+
function createStoredCliOAuthCredential(args) {
|
|
3092
|
+
const nowMs = args.nowMs ?? Date.now();
|
|
3093
|
+
if (!Number.isFinite(args.sessionExpiresIn) || args.sessionExpiresIn <= 0) {
|
|
3094
|
+
throw invalidStoredCredential("CLI session lifetime is invalid.");
|
|
3095
|
+
}
|
|
3096
|
+
return {
|
|
3097
|
+
version: 1,
|
|
3098
|
+
sessionAccessToken: requiredSecret(args.sessionAccessToken, "CLI session access token"),
|
|
3099
|
+
sessionExpiresAt: nowMs + args.sessionExpiresIn * 1000,
|
|
3100
|
+
oauthRefreshToken: requiredSecret(args.oauthRefreshToken, "OAuth refresh token"),
|
|
3101
|
+
oauthClientId: requiredValue(args.oauthClientId, "OAuth client id"),
|
|
3102
|
+
oauthResource: requiredHttpsResource(args.oauthResource),
|
|
3103
|
+
...args.workspaceId === undefined || args.workspaceId.trim() === "" ? {} : { workspaceId: args.workspaceId.trim() }
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
function serializeStoredCliOAuthCredential(credential) {
|
|
3107
|
+
return `${OAUTH_CREDENTIAL_PREFIX}${Buffer.from(JSON.stringify(credential), "utf8").toString("base64url")}`;
|
|
3108
|
+
}
|
|
3109
|
+
function parseStoredCliOAuthCredential(storedValue) {
|
|
3110
|
+
if (!storedValue.startsWith(OAUTH_CREDENTIAL_PREFIX)) {
|
|
3111
|
+
return null;
|
|
3112
|
+
}
|
|
3113
|
+
let parsed;
|
|
3114
|
+
try {
|
|
3115
|
+
const encoded = storedValue.slice(OAUTH_CREDENTIAL_PREFIX.length);
|
|
3116
|
+
parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
3117
|
+
} catch (error) {
|
|
3118
|
+
throw invalidStoredCredential("Stored CLI OAuth credential is unreadable.", error);
|
|
3119
|
+
}
|
|
3120
|
+
if (!isRecord(parsed) || parsed["version"] !== 1) {
|
|
3121
|
+
throw invalidStoredCredential("Stored CLI OAuth credential version is invalid.");
|
|
3122
|
+
}
|
|
3123
|
+
const sessionAccessToken = recordString(parsed, "sessionAccessToken");
|
|
3124
|
+
const oauthRefreshToken = recordString(parsed, "oauthRefreshToken");
|
|
3125
|
+
const oauthClientId = recordString(parsed, "oauthClientId");
|
|
3126
|
+
const oauthResource = recordString(parsed, "oauthResource");
|
|
3127
|
+
const sessionExpiresAt = parsed["sessionExpiresAt"];
|
|
3128
|
+
const workspaceId = optionalRecordString(parsed, "workspaceId");
|
|
3129
|
+
if (sessionAccessToken === null || oauthRefreshToken === null || oauthClientId === null || oauthResource === null || typeof sessionExpiresAt !== "number" || !Number.isFinite(sessionExpiresAt) || sessionExpiresAt <= 0) {
|
|
3130
|
+
throw invalidStoredCredential("Stored CLI OAuth credential is incomplete.");
|
|
3131
|
+
}
|
|
3132
|
+
return {
|
|
3133
|
+
version: 1,
|
|
3134
|
+
sessionAccessToken,
|
|
3135
|
+
sessionExpiresAt,
|
|
3136
|
+
oauthRefreshToken,
|
|
3137
|
+
oauthClientId,
|
|
3138
|
+
oauthResource: requiredHttpsResource(oauthResource),
|
|
3139
|
+
...workspaceId === null ? {} : { workspaceId }
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
function cliOAuthCredentialNeedsRefresh(credential, workspaceId, nowMs = Date.now()) {
|
|
3143
|
+
const requestedWorkspace = workspaceId?.trim();
|
|
3144
|
+
if (requestedWorkspace !== undefined && requestedWorkspace !== "" && requestedWorkspace !== credential.workspaceId) {
|
|
3145
|
+
return true;
|
|
3146
|
+
}
|
|
3147
|
+
return credential.sessionExpiresAt <= nowMs + SESSION_REFRESH_LEEWAY_MS;
|
|
3148
|
+
}
|
|
3149
|
+
function recordString(value, key) {
|
|
3150
|
+
const item = value[key];
|
|
3151
|
+
return typeof item === "string" && item.trim() !== "" ? item : null;
|
|
3152
|
+
}
|
|
3153
|
+
function optionalRecordString(value, key) {
|
|
3154
|
+
const item = value[key];
|
|
3155
|
+
return typeof item === "string" && item.trim() !== "" ? item.trim() : null;
|
|
3156
|
+
}
|
|
3157
|
+
function requiredSecret(value, label) {
|
|
3158
|
+
return requiredValue(value, label);
|
|
3159
|
+
}
|
|
3160
|
+
function requiredValue(value, label) {
|
|
3161
|
+
const normalized = value.trim();
|
|
3162
|
+
if (normalized === "") {
|
|
3163
|
+
throw invalidStoredCredential(`${label} is missing.`);
|
|
3164
|
+
}
|
|
3165
|
+
return normalized;
|
|
3166
|
+
}
|
|
3167
|
+
function requiredHttpsResource(value) {
|
|
3168
|
+
const normalized = requiredValue(value, "OAuth resource");
|
|
3169
|
+
let resource;
|
|
3170
|
+
try {
|
|
3171
|
+
resource = new URL(normalized);
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
throw invalidStoredCredential("OAuth resource is not a valid URL.", error);
|
|
3174
|
+
}
|
|
3175
|
+
const loopbackHostname = resource.hostname === "localhost" || resource.hostname === "127.0.0.1" || resource.hostname === "[::1]";
|
|
3176
|
+
if (resource.protocol !== "https:" && !(resource.protocol === "http:" && loopbackHostname)) {
|
|
3177
|
+
throw invalidStoredCredential("OAuth resource must use HTTPS.");
|
|
3178
|
+
}
|
|
3179
|
+
return normalized;
|
|
3180
|
+
}
|
|
3181
|
+
function invalidStoredCredential(message, cause) {
|
|
3182
|
+
return new CliError(message, {
|
|
3183
|
+
code: ExitCode.AuthRequired,
|
|
3184
|
+
errorCode: "stored_oauth_credential_invalid",
|
|
3185
|
+
...cause === undefined ? {} : { cause }
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
|
|
2883
3189
|
// src/auth-commands.ts
|
|
2884
3190
|
var OAUTH_CALLBACK_PATH = "/oauth/callback";
|
|
2885
3191
|
var OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
2886
3192
|
var DEVICE_MAX_POLLS = 120;
|
|
2887
|
-
async function resolveStoredToken(
|
|
2888
|
-
|
|
3193
|
+
async function resolveStoredToken(options) {
|
|
3194
|
+
const keychainValue = await options.credentialStore.readToken(options.apiUrl);
|
|
3195
|
+
const source = keychainValue === undefined ? "config" : "keychain";
|
|
3196
|
+
const storedValue = keychainValue ?? (options.config.insecureStorage === true ? options.config.token : undefined);
|
|
3197
|
+
if (storedValue === undefined) {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
let credential;
|
|
3201
|
+
try {
|
|
3202
|
+
credential = parseStoredCliOAuthCredential(storedValue);
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
if (options.recoverInvalidCredential === true) {
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
throw error;
|
|
3208
|
+
}
|
|
3209
|
+
if (credential === null) {
|
|
3210
|
+
return storedValue;
|
|
3211
|
+
}
|
|
3212
|
+
if (!cliOAuthCredentialNeedsRefresh(credential, options.workspaceId)) {
|
|
3213
|
+
return credential.sessionAccessToken;
|
|
3214
|
+
}
|
|
3215
|
+
try {
|
|
3216
|
+
const refreshed = await refreshOAuthToken({
|
|
3217
|
+
apiUrl: options.apiUrl,
|
|
3218
|
+
clientId: credential.oauthClientId,
|
|
3219
|
+
refreshToken: credential.oauthRefreshToken,
|
|
3220
|
+
resource: credential.oauthResource,
|
|
3221
|
+
...options.fetch === undefined ? {} : { fetch: options.fetch }
|
|
3222
|
+
});
|
|
3223
|
+
const rotatedRefreshToken = requireRefreshToken(refreshed);
|
|
3224
|
+
await persistResolvedCredential(options, source, serializeStoredCliOAuthCredential({
|
|
3225
|
+
...credential,
|
|
3226
|
+
oauthRefreshToken: rotatedRefreshToken
|
|
3227
|
+
}));
|
|
3228
|
+
const session = await exchangeOAuthForCliSession({
|
|
3229
|
+
apiUrl: options.apiUrl,
|
|
3230
|
+
oauthAccessToken: refreshed.accessToken,
|
|
3231
|
+
...options.workspaceId === undefined ? {} : { workspaceId: options.workspaceId },
|
|
3232
|
+
...options.fetch === undefined ? {} : { fetch: options.fetch }
|
|
3233
|
+
});
|
|
3234
|
+
if (session.oauthResource !== credential.oauthResource) {
|
|
3235
|
+
throw new CliError("Refreshed CLI session OAuth resource did not match the stored credential.", {
|
|
3236
|
+
code: ExitCode.AuthRequired,
|
|
3237
|
+
errorCode: "oauth_resource_mismatch"
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3240
|
+
const nextCredential = createStoredCliOAuthCredential({
|
|
3241
|
+
sessionAccessToken: session.accessToken,
|
|
3242
|
+
sessionExpiresIn: session.expiresIn,
|
|
3243
|
+
oauthRefreshToken: rotatedRefreshToken,
|
|
3244
|
+
oauthClientId: credential.oauthClientId,
|
|
3245
|
+
oauthResource: session.oauthResource,
|
|
3246
|
+
...session.workspaceId === undefined ? {} : { workspaceId: session.workspaceId }
|
|
3247
|
+
});
|
|
3248
|
+
await persistResolvedCredential(options, source, serializeStoredCliOAuthCredential(nextCredential));
|
|
3249
|
+
return nextCredential.sessionAccessToken;
|
|
3250
|
+
} catch (error) {
|
|
3251
|
+
if (!isStoredRefreshRejected(error)) {
|
|
3252
|
+
throw error;
|
|
3253
|
+
}
|
|
3254
|
+
await clearResolvedCredential(options, source);
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
async function revokeStoredOAuthLogin(args) {
|
|
3259
|
+
const storedValue = await args.credentialStore.readToken(args.apiUrl) ?? (args.config.insecureStorage === true ? args.config.token : undefined);
|
|
3260
|
+
if (storedValue === undefined) {
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
const credential = parseStoredCliOAuthCredential(storedValue);
|
|
3264
|
+
if (credential === null) {
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
const sessionClient = new UruApiClient({
|
|
3268
|
+
apiUrl: args.apiUrl,
|
|
3269
|
+
token: credential.sessionAccessToken,
|
|
3270
|
+
...args.fetch === undefined ? {} : { fetch: args.fetch }
|
|
3271
|
+
});
|
|
3272
|
+
const revocations = await Promise.allSettled([
|
|
3273
|
+
sessionClient.api("/api/auth/logout", { method: "POST" }),
|
|
3274
|
+
revokeOAuthToken({
|
|
3275
|
+
apiUrl: args.apiUrl,
|
|
3276
|
+
clientId: credential.oauthClientId,
|
|
3277
|
+
token: credential.oauthRefreshToken,
|
|
3278
|
+
...args.fetch === undefined ? {} : { fetch: args.fetch }
|
|
3279
|
+
})
|
|
3280
|
+
]);
|
|
3281
|
+
const rejected = revocations.find((result) => result.status === "rejected");
|
|
3282
|
+
if (rejected !== undefined) {
|
|
3283
|
+
throw rejected.reason;
|
|
3284
|
+
}
|
|
2889
3285
|
}
|
|
2890
3286
|
async function resolveFlagToken(flags, stdin) {
|
|
2891
3287
|
if (flags.token !== undefined) {
|
|
@@ -2919,20 +3315,26 @@ async function resolveFlagToken(flags, stdin) {
|
|
|
2919
3315
|
}
|
|
2920
3316
|
async function login(ctx, options = {}) {
|
|
2921
3317
|
const existingToken = ctx.token === undefined || ctx.token.trim() === "" ? undefined : ctx.token;
|
|
2922
|
-
|
|
3318
|
+
const existingStoredValue = existingToken !== undefined && !ctx.tokenIsExplicit ? await readStoredLoginValue(ctx) : undefined;
|
|
3319
|
+
let usedExistingToken = existingToken !== undefined;
|
|
3320
|
+
let loginToken = existingToken === undefined ? await obtainLoginSessionToken(ctx) : {
|
|
3321
|
+
accessToken: existingToken,
|
|
3322
|
+
storedValue: existingStoredValue ?? existingToken
|
|
3323
|
+
};
|
|
2923
3324
|
let result;
|
|
2924
3325
|
try {
|
|
2925
|
-
result = await validateSessionToken(ctx,
|
|
3326
|
+
result = await validateSessionToken(ctx, loginToken.accessToken);
|
|
2926
3327
|
} catch (error) {
|
|
2927
3328
|
const storedTokenIsStale = existingToken !== undefined && !ctx.tokenIsExplicit && isAuthRequiredError(error);
|
|
2928
3329
|
if (!storedTokenIsStale) {
|
|
2929
3330
|
throw error;
|
|
2930
3331
|
}
|
|
2931
|
-
|
|
2932
|
-
|
|
3332
|
+
loginToken = await obtainLoginSessionToken(ctx);
|
|
3333
|
+
usedExistingToken = false;
|
|
3334
|
+
result = await validateSessionToken(ctx, loginToken.accessToken);
|
|
2933
3335
|
}
|
|
2934
3336
|
const selectedWorkspace = selectWorkspace(result, ctx.workspace);
|
|
2935
|
-
await persistLoginToken(ctx,
|
|
3337
|
+
await persistLoginToken(ctx, loginToken.storedValue, selectedWorkspace);
|
|
2936
3338
|
if (options.silentSuccess === true) {
|
|
2937
3339
|
return;
|
|
2938
3340
|
}
|
|
@@ -2945,12 +3347,15 @@ async function login(ctx, options = {}) {
|
|
|
2945
3347
|
workspaceId: selectedWorkspace,
|
|
2946
3348
|
workspaceSelectionRequired: result.workspaceSelectionRequired,
|
|
2947
3349
|
storage: ctx.insecureStorage ? "insecure_file" : "keychain",
|
|
2948
|
-
auth:
|
|
3350
|
+
auth: usedExistingToken ? "token" : ctx.device ? "device_oauth" : "browser_oauth"
|
|
2949
3351
|
});
|
|
2950
3352
|
} else {
|
|
2951
3353
|
writeText(ctx.io, ctx.insecureStorage ? "Logged in (token stored in local config)" : "Logged in");
|
|
2952
3354
|
}
|
|
2953
3355
|
}
|
|
3356
|
+
async function readStoredLoginValue(ctx) {
|
|
3357
|
+
return await ctx.credentialStore.readToken(ctx.apiUrl) ?? (ctx.config.insecureStorage === true ? ctx.config.token : undefined);
|
|
3358
|
+
}
|
|
2954
3359
|
async function validateSessionToken(ctx, token) {
|
|
2955
3360
|
const loginClient = new UruApiClient({
|
|
2956
3361
|
apiUrl: ctx.apiUrl,
|
|
@@ -2963,6 +3368,9 @@ async function validateSessionToken(ctx, token) {
|
|
|
2963
3368
|
function isAuthRequiredError(error) {
|
|
2964
3369
|
return error instanceof CliError && error.exitCode === ExitCode.AuthRequired;
|
|
2965
3370
|
}
|
|
3371
|
+
function isStoredRefreshRejected(error) {
|
|
3372
|
+
return error instanceof CliError && error.errorCode === "oauth_refresh_invalid";
|
|
3373
|
+
}
|
|
2966
3374
|
async function readAllStdin(input) {
|
|
2967
3375
|
const chunks = [];
|
|
2968
3376
|
for await (const chunk of input) {
|
|
@@ -2980,6 +3388,10 @@ async function obtainLoginSessionToken(ctx) {
|
|
|
2980
3388
|
return loginWithBrowser(ctx);
|
|
2981
3389
|
}
|
|
2982
3390
|
async function loginWithDeviceCode(ctx) {
|
|
3391
|
+
const resource = await discoverOAuthResource({
|
|
3392
|
+
apiUrl: ctx.apiUrl,
|
|
3393
|
+
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
3394
|
+
});
|
|
2983
3395
|
const clientRegistration = await registerOAuthClient({
|
|
2984
3396
|
apiUrl: ctx.apiUrl,
|
|
2985
3397
|
redirectUri: buildLoopbackRedirectUri({
|
|
@@ -2998,6 +3410,7 @@ async function loginWithDeviceCode(ctx) {
|
|
|
2998
3410
|
apiUrl: ctx.apiUrl,
|
|
2999
3411
|
clientId: clientRegistration.clientId,
|
|
3000
3412
|
scope: clientRegistration.scope,
|
|
3413
|
+
resource,
|
|
3001
3414
|
...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
|
|
3002
3415
|
});
|
|
3003
3416
|
const complete = authorization.verificationUriComplete ?? authorization.verificationUri;
|
|
@@ -3014,7 +3427,7 @@ async function loginWithDeviceCode(ctx) {
|
|
|
3014
3427
|
maxPolls: Math.min(DEVICE_MAX_POLLS, Math.ceil(authorization.expiresIn / Math.max(1, authorization.interval))),
|
|
3015
3428
|
...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
|
|
3016
3429
|
});
|
|
3017
|
-
return exchangeOAuthTokenForSession(ctx, token);
|
|
3430
|
+
return exchangeOAuthTokenForSession(ctx, token, clientRegistration.clientId, resource);
|
|
3018
3431
|
}
|
|
3019
3432
|
async function loginWithBrowser(ctx) {
|
|
3020
3433
|
const callbackHost = ctx.callbackHost;
|
|
@@ -3023,6 +3436,10 @@ async function loginWithBrowser(ctx) {
|
|
|
3023
3436
|
...ctx.callbackPort === undefined ? {} : { port: ctx.callbackPort }
|
|
3024
3437
|
});
|
|
3025
3438
|
try {
|
|
3439
|
+
const resource = await discoverOAuthResource({
|
|
3440
|
+
apiUrl: ctx.apiUrl,
|
|
3441
|
+
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
3442
|
+
});
|
|
3026
3443
|
const redirectUri = buildLoopbackRedirectUri({
|
|
3027
3444
|
host: callbackHost,
|
|
3028
3445
|
port: callback.port,
|
|
@@ -3041,7 +3458,8 @@ async function loginWithBrowser(ctx) {
|
|
|
3041
3458
|
redirectUri,
|
|
3042
3459
|
scope: clientRegistration.scope,
|
|
3043
3460
|
state,
|
|
3044
|
-
codeChallenge: pkce.challenge
|
|
3461
|
+
codeChallenge: pkce.challenge,
|
|
3462
|
+
resource
|
|
3045
3463
|
});
|
|
3046
3464
|
await maybeCopyLoginUrl(ctx, authorizationUrl);
|
|
3047
3465
|
if (ctx.noOpen) {
|
|
@@ -3060,21 +3478,40 @@ async function loginWithBrowser(ctx) {
|
|
|
3060
3478
|
code: authorization.code,
|
|
3061
3479
|
redirectUri,
|
|
3062
3480
|
codeVerifier: pkce.verifier,
|
|
3481
|
+
resource,
|
|
3063
3482
|
...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
|
|
3064
3483
|
});
|
|
3065
|
-
return exchangeOAuthTokenForSession(ctx, token);
|
|
3484
|
+
return exchangeOAuthTokenForSession(ctx, token, clientRegistration.clientId, resource);
|
|
3066
3485
|
} finally {
|
|
3067
3486
|
callback.close();
|
|
3068
3487
|
}
|
|
3069
3488
|
}
|
|
3070
|
-
async function exchangeOAuthTokenForSession(ctx, token) {
|
|
3489
|
+
async function exchangeOAuthTokenForSession(ctx, token, clientId, resource) {
|
|
3490
|
+
const refreshToken = requireRefreshToken(token);
|
|
3071
3491
|
const session = await exchangeOAuthForCliSession({
|
|
3072
3492
|
apiUrl: ctx.apiUrl,
|
|
3073
3493
|
oauthAccessToken: token.accessToken,
|
|
3074
3494
|
...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
|
|
3075
3495
|
...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
|
|
3076
3496
|
});
|
|
3077
|
-
|
|
3497
|
+
if (session.oauthResource !== resource) {
|
|
3498
|
+
throw new CliError("CLI session OAuth resource did not match discovery.", {
|
|
3499
|
+
code: ExitCode.AuthRequired,
|
|
3500
|
+
errorCode: "oauth_resource_mismatch"
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
const credential = createStoredCliOAuthCredential({
|
|
3504
|
+
sessionAccessToken: session.accessToken,
|
|
3505
|
+
sessionExpiresIn: session.expiresIn,
|
|
3506
|
+
oauthRefreshToken: refreshToken,
|
|
3507
|
+
oauthClientId: clientId,
|
|
3508
|
+
oauthResource: session.oauthResource,
|
|
3509
|
+
...session.workspaceId === undefined ? {} : { workspaceId: session.workspaceId }
|
|
3510
|
+
});
|
|
3511
|
+
return {
|
|
3512
|
+
accessToken: session.accessToken,
|
|
3513
|
+
storedValue: serializeStoredCliOAuthCredential(credential)
|
|
3514
|
+
};
|
|
3078
3515
|
}
|
|
3079
3516
|
async function persistLoginToken(ctx, token, selectedWorkspace) {
|
|
3080
3517
|
if (ctx.insecureStorage) {
|
|
@@ -3101,6 +3538,40 @@ async function persistLoginToken(ctx, token, selectedWorkspace) {
|
|
|
3101
3538
|
};
|
|
3102
3539
|
await ctx.store.write(nextConfig);
|
|
3103
3540
|
}
|
|
3541
|
+
function requireRefreshToken(token) {
|
|
3542
|
+
const refreshToken = token.refreshToken?.trim();
|
|
3543
|
+
if (refreshToken === undefined || refreshToken === "") {
|
|
3544
|
+
throw new CliError("OAuth server did not issue a refresh token for the CLI.", {
|
|
3545
|
+
code: ExitCode.AuthRequired,
|
|
3546
|
+
errorCode: "oauth_refresh_token_missing"
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
return refreshToken;
|
|
3550
|
+
}
|
|
3551
|
+
async function persistResolvedCredential(options, source, storedValue) {
|
|
3552
|
+
if (source === "keychain") {
|
|
3553
|
+
await options.credentialStore.writeToken(options.apiUrl, storedValue);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
await options.store.write({
|
|
3557
|
+
...options.config,
|
|
3558
|
+
apiUrl: options.apiUrl,
|
|
3559
|
+
token: storedValue,
|
|
3560
|
+
insecureStorage: true
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
async function clearResolvedCredential(options, source) {
|
|
3564
|
+
if (source === "keychain") {
|
|
3565
|
+
await options.credentialStore.clearToken(options.apiUrl);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
const {
|
|
3569
|
+
token: _token,
|
|
3570
|
+
insecureStorage: _insecureStorage,
|
|
3571
|
+
...safeConfig
|
|
3572
|
+
} = options.config;
|
|
3573
|
+
await options.store.write(safeConfig);
|
|
3574
|
+
}
|
|
3104
3575
|
async function maybeCopyLoginUrl(ctx, url) {
|
|
3105
3576
|
if (!ctx.clipboard) {
|
|
3106
3577
|
return;
|
|
@@ -3204,6 +3675,7 @@ async function switchWorkspace(ctx, workspaceId) {
|
|
|
3204
3675
|
// src/local-commands.ts
|
|
3205
3676
|
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
3206
3677
|
import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
3678
|
+
var DEFAULT_MCP_PACKAGE = package_default.uru.mcpPackage;
|
|
3207
3679
|
var GENERIC_FAMILY_COMMANDS = {
|
|
3208
3680
|
workspace: "workspace",
|
|
3209
3681
|
sharing: "sharing",
|
|
@@ -3232,15 +3704,12 @@ var TOP_LEVEL_COMMANDS = [
|
|
|
3232
3704
|
"tools",
|
|
3233
3705
|
"init",
|
|
3234
3706
|
"build",
|
|
3235
|
-
"deploy",
|
|
3236
|
-
"promote",
|
|
3237
3707
|
"pull",
|
|
3238
3708
|
"status",
|
|
3239
3709
|
"diff",
|
|
3240
3710
|
"push",
|
|
3241
3711
|
"logs",
|
|
3242
3712
|
"open",
|
|
3243
|
-
"rollback",
|
|
3244
3713
|
"gems",
|
|
3245
3714
|
"automations",
|
|
3246
3715
|
"library",
|
|
@@ -3333,7 +3802,7 @@ async function dispatchMcp(ctx, sub, args) {
|
|
|
3333
3802
|
const config = mcpServerConfig({
|
|
3334
3803
|
apiKey: flags.values["--api-key"] ?? ctx.token,
|
|
3335
3804
|
serverName: flags.values["--server-name"] ?? "uru",
|
|
3336
|
-
packageName: flags.values["--package"] ??
|
|
3805
|
+
packageName: flags.values["--package"] ?? DEFAULT_MCP_PACKAGE
|
|
3337
3806
|
});
|
|
3338
3807
|
if (sub === "config") {
|
|
3339
3808
|
writeJson(ctx.io, config);
|
|
@@ -3697,7 +4166,7 @@ function mcpServerConfig(args) {
|
|
|
3697
4166
|
mcpServers: {
|
|
3698
4167
|
[args.serverName]: {
|
|
3699
4168
|
command: "npx",
|
|
3700
|
-
args: [args.packageName],
|
|
4169
|
+
args: ["-y", args.packageName],
|
|
3701
4170
|
env: { URU_API_KEY: apiKey }
|
|
3702
4171
|
}
|
|
3703
4172
|
}
|
|
@@ -3716,38 +4185,380 @@ function familyPrefix(family) {
|
|
|
3716
4185
|
if (family === "secrets") {
|
|
3717
4186
|
return "secret";
|
|
3718
4187
|
}
|
|
3719
|
-
if (family === "tokens") {
|
|
3720
|
-
return "token";
|
|
4188
|
+
if (family === "tokens") {
|
|
4189
|
+
return "token";
|
|
4190
|
+
}
|
|
4191
|
+
return family;
|
|
4192
|
+
}
|
|
4193
|
+
function isLikelyCommand(candidate, value) {
|
|
4194
|
+
if (value === "") {
|
|
4195
|
+
return false;
|
|
4196
|
+
}
|
|
4197
|
+
return candidate.startsWith(value) || value.startsWith(candidate) || levenshtein(candidate, value) <= 2;
|
|
4198
|
+
}
|
|
4199
|
+
function levenshtein(left, right) {
|
|
4200
|
+
const previous = Array.from({ length: right.length + 1 }, (_value, index) => index);
|
|
4201
|
+
for (let leftIndex = 0;leftIndex < left.length; leftIndex += 1) {
|
|
4202
|
+
let diagonal = previous[0] ?? 0;
|
|
4203
|
+
previous[0] = leftIndex + 1;
|
|
4204
|
+
for (let rightIndex = 0;rightIndex < right.length; rightIndex += 1) {
|
|
4205
|
+
const upper = previous[rightIndex + 1] ?? 0;
|
|
4206
|
+
const cost = left[leftIndex] === right[rightIndex] ? 0 : 1;
|
|
4207
|
+
previous[rightIndex + 1] = Math.min(upper + 1, (previous[rightIndex] ?? 0) + 1, diagonal + cost);
|
|
4208
|
+
diagonal = upper;
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
return previous[right.length] ?? 0;
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
// src/gem-lifecycle-commands.ts
|
|
4215
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
4216
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
4217
|
+
import { join as join3, resolve as resolve3 } from "node:path";
|
|
4218
|
+
|
|
4219
|
+
// src/gem-lifecycle-resource-commands.ts
|
|
4220
|
+
async function dispatchGemLifecycleResourceCommand(ctx, sub, args, linkedProject) {
|
|
4221
|
+
if (sub === "versions") {
|
|
4222
|
+
await dispatchGemVersions(ctx, args, linkedProject);
|
|
4223
|
+
return true;
|
|
4224
|
+
}
|
|
4225
|
+
if (sub === "edits") {
|
|
4226
|
+
await dispatchGemEdits(ctx, args, linkedProject);
|
|
4227
|
+
return true;
|
|
4228
|
+
}
|
|
4229
|
+
if (sub === "releases") {
|
|
4230
|
+
await dispatchGemReleases(ctx, args, linkedProject);
|
|
4231
|
+
return true;
|
|
4232
|
+
}
|
|
4233
|
+
if (sub === "links") {
|
|
4234
|
+
await dispatchGemLinks(ctx, args, linkedProject);
|
|
4235
|
+
return true;
|
|
4236
|
+
}
|
|
4237
|
+
if (sub === "runs") {
|
|
4238
|
+
await dispatchGemRuns(ctx, args, linkedProject);
|
|
4239
|
+
return true;
|
|
4240
|
+
}
|
|
4241
|
+
return false;
|
|
4242
|
+
}
|
|
4243
|
+
async function dispatchGemVersions(ctx, args, linkedProject) {
|
|
4244
|
+
const [verb, ...rest] = args;
|
|
4245
|
+
if (verb === "save") {
|
|
4246
|
+
await gemVersionSave(ctx, rest, linkedProject);
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
if (verb === "list") {
|
|
4250
|
+
const flags = parseLocalFlags(rest);
|
|
4251
|
+
const params = {
|
|
4252
|
+
path: gemPathFromArgs(flags, linkedProject)
|
|
4253
|
+
};
|
|
4254
|
+
copyStringFlag(flags, params, "--cursor", "cursor");
|
|
4255
|
+
copyOptionalIntegerFlag(flags, params, "--limit", "limit");
|
|
4256
|
+
await emitGemTool(ctx, "gem.versions_list", params);
|
|
4257
|
+
return;
|
|
4258
|
+
}
|
|
4259
|
+
if (verb === "show" || verb === "restore" || verb === "preview") {
|
|
4260
|
+
const flags = parseLocalFlags(rest);
|
|
4261
|
+
const version = lifecycleResourceArg(flags, `Usage: uru gems versions ${verb} <gem> <version>`);
|
|
4262
|
+
const path = gemPathFromArgs(flags, linkedProject);
|
|
4263
|
+
await emitGemTool(ctx, verb === "show" ? "gem.version_get" : verb === "restore" ? "gem.version_restore" : "gem.version_preview", { path, version_id: version }, verb === "restore" ? { textExtractor: extractVersionRestoreSummary } : undefined);
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
if (verb === "diff") {
|
|
4267
|
+
const flags = parseLocalFlags(rest);
|
|
4268
|
+
const [fromVersion, toVersion] = lifecycleResourceArgs(flags, 2, "Usage: uru gems versions diff <gem> <from-version> <to-version>");
|
|
4269
|
+
await emitGemTool(ctx, "gem.version_diff", {
|
|
4270
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4271
|
+
from_version_id: requireCommandValue(fromVersion, "Missing from-version"),
|
|
4272
|
+
to_version_id: requireCommandValue(toVersion, "Missing to-version")
|
|
4273
|
+
});
|
|
4274
|
+
return;
|
|
4275
|
+
}
|
|
4276
|
+
throw new CliError("Usage: uru gems versions list <gem> | save <gem> [--name ...] [--message ...] | show <gem> <version> | diff <gem> <from> <to> | restore <gem> <version> | preview <gem> <version>");
|
|
4277
|
+
}
|
|
4278
|
+
async function gemVersionSave(ctx, args, linkedProject) {
|
|
4279
|
+
const flags = parseLocalFlags(args);
|
|
4280
|
+
assertAllowedLocalFlags(flags, ["--gem", "--idempotency-key", "--message", "--name", "--path"], "uru gems versions save");
|
|
4281
|
+
const gemPath = gemPathFromArgs(flags, linkedProject);
|
|
4282
|
+
const params = { path: gemPath };
|
|
4283
|
+
copyStringFlag(flags, params, "--name", "name");
|
|
4284
|
+
copyStringFlag(flags, params, "--message", "message");
|
|
4285
|
+
copyStringFlag(flags, params, "--idempotency-key", "idempotency_key");
|
|
4286
|
+
await emitGemTool(ctx, "gem.version_save", params, {
|
|
4287
|
+
streamStart: { operation: "gem.version_save", path: gemPath },
|
|
4288
|
+
textExtractor: extractVersionSaveSummary
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
async function dispatchGemEdits(ctx, args, linkedProject) {
|
|
4292
|
+
const [verb, ...rest] = args;
|
|
4293
|
+
const flags = parseLocalFlags(rest);
|
|
4294
|
+
if (verb === "list") {
|
|
4295
|
+
const params = {
|
|
4296
|
+
path: gemPathFromArgs(flags, linkedProject)
|
|
4297
|
+
};
|
|
4298
|
+
copyStringFlag(flags, params, "--cursor", "cursor");
|
|
4299
|
+
copyOptionalIntegerFlag(flags, params, "--limit", "limit");
|
|
4300
|
+
await emitGemTool(ctx, "gem.edits_list", params);
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
if (verb === "show") {
|
|
4304
|
+
const editId = requireCommandValue(lifecycleResourceArg(flags, "Usage: uru gems edits show <gem> <edit>"), "Usage: uru gems edits show <gem> <edit>");
|
|
4305
|
+
await emitGemTool(ctx, "gem.edit_get", {
|
|
4306
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4307
|
+
edit_id: editId
|
|
4308
|
+
});
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
4311
|
+
throw new CliError("Usage: uru gems edits list <gem> | show <gem> <edit>");
|
|
4312
|
+
}
|
|
4313
|
+
async function dispatchGemReleases(ctx, args, linkedProject) {
|
|
4314
|
+
const [verb, ...rest] = args;
|
|
4315
|
+
const flags = parseLocalFlags(rest);
|
|
4316
|
+
if (verb === "list") {
|
|
4317
|
+
const params = {
|
|
4318
|
+
path: gemPathFromArgs(flags, linkedProject)
|
|
4319
|
+
};
|
|
4320
|
+
copyStringFlag(flags, params, "--cursor", "cursor");
|
|
4321
|
+
copyOptionalIntegerFlag(flags, params, "--limit", "limit");
|
|
4322
|
+
await emitGemTool(ctx, "gem.releases_list", params);
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
if (verb === "create") {
|
|
4326
|
+
const versionId = lifecycleResourceArg(flags, "Usage: uru gems releases create <gem> <version>");
|
|
4327
|
+
const path = gemPathFromArgs(flags, linkedProject);
|
|
4328
|
+
await emitGemTool(ctx, "gem.release_create", {
|
|
4329
|
+
path,
|
|
4330
|
+
version_id: versionId
|
|
4331
|
+
}, {
|
|
4332
|
+
streamStart: { operation: "gem.release_create", path },
|
|
4333
|
+
textExtractor: extractReleaseSummary
|
|
4334
|
+
});
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
if (verb === "show" || verb === "retry") {
|
|
4338
|
+
const releaseId = lifecycleResourceArg(flags, `Usage: uru gems releases ${verb} <gem> <release>`);
|
|
4339
|
+
const path = gemPathFromArgs(flags, linkedProject);
|
|
4340
|
+
const params = { path, release_id: releaseId };
|
|
4341
|
+
await emitGemTool(ctx, verb === "show" ? "gem.release_get" : "gem.release_retry", params, verb === "retry" ? { textExtractor: extractReleaseSummary } : undefined);
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
throw new CliError("Usage: uru gems releases list|show|create|retry");
|
|
4345
|
+
}
|
|
4346
|
+
async function dispatchGemLinks(ctx, args, linkedProject) {
|
|
4347
|
+
const [verb, ...rest] = args;
|
|
4348
|
+
const flags = parseLocalFlags(rest);
|
|
4349
|
+
if (verb === "list") {
|
|
4350
|
+
await emitGemTool(ctx, "gem.links_list", {
|
|
4351
|
+
path: gemPathFromArgs(flags, linkedProject)
|
|
4352
|
+
});
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
if (verb === "create") {
|
|
4356
|
+
const path = gemPathFromArgs(flags, linkedProject);
|
|
4357
|
+
await emitGemTool(ctx, "gem.link_create", {
|
|
4358
|
+
path,
|
|
4359
|
+
name: requireCommandValue(flags.values["--name"], "links create requires --name"),
|
|
4360
|
+
version_id: requireCommandValue(flags.values["--version"], "links create requires --version")
|
|
4361
|
+
}, { textExtractor: extractLinkCreateSummary });
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
if (verb === "set-version") {
|
|
4365
|
+
const [linkId, versionId] = lifecycleResourceArgs(flags, 2, "Usage: uru gems links set-version <gem> <link> <version>");
|
|
4366
|
+
await emitGemTool(ctx, "gem.link_set_version", {
|
|
4367
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4368
|
+
link_id: requireCommandValue(linkId, "Missing Link"),
|
|
4369
|
+
version_id: requireCommandValue(versionId, "Missing Version")
|
|
4370
|
+
}, {
|
|
4371
|
+
textExtractor: extractLinkVersionSummary
|
|
4372
|
+
});
|
|
4373
|
+
return;
|
|
4374
|
+
}
|
|
4375
|
+
if (verb === "set-access") {
|
|
4376
|
+
const linkId = lifecycleResourceArg(flags, "Usage: uru gems links set-access <gem> <link> --gate <public|password|email>");
|
|
4377
|
+
const params = {
|
|
4378
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4379
|
+
link_id: linkId,
|
|
4380
|
+
gate_type: requireCommandValue(flags.values["--gate"], "links set-access requires --gate")
|
|
4381
|
+
};
|
|
4382
|
+
copyStringFlag(flags, params, "--passcode", "passcode");
|
|
4383
|
+
const emailAllowlist = flags.values["--email-allowlist"];
|
|
4384
|
+
if (emailAllowlist !== undefined) {
|
|
4385
|
+
params["email_allowlist"] = emailAllowlist.split(",").map((email) => email.trim()).filter((email) => email.length > 0);
|
|
4386
|
+
}
|
|
4387
|
+
copyStringFlag(flags, params, "--expires-at", "expires_at");
|
|
4388
|
+
await emitGemTool(ctx, "gem.link_set_access", params);
|
|
4389
|
+
return;
|
|
4390
|
+
}
|
|
4391
|
+
if (verb === "set-notifications") {
|
|
4392
|
+
const [linkId, mode] = lifecycleResourceArgs(flags, 2, "Usage: uru gems links set-notifications <gem> <link> <mode>");
|
|
4393
|
+
await emitGemTool(ctx, "gem.link_set_notifications", {
|
|
4394
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4395
|
+
link_id: requireCommandValue(linkId, "Missing Link"),
|
|
4396
|
+
view_notification_mode: requireCommandValue(mode, "Missing notification mode")
|
|
4397
|
+
});
|
|
4398
|
+
return;
|
|
4399
|
+
}
|
|
4400
|
+
if (verb === "rotate" || verb === "revoke") {
|
|
4401
|
+
const linkId = lifecycleResourceArg(flags, `Usage: uru gems links ${verb} <gem> <link>`);
|
|
4402
|
+
await emitGemTool(ctx, verb === "rotate" ? "gem.link_rotate" : "gem.link_revoke", {
|
|
4403
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4404
|
+
link_id: linkId
|
|
4405
|
+
});
|
|
4406
|
+
return;
|
|
4407
|
+
}
|
|
4408
|
+
throw new CliError("Usage: uru gems links list|create|set-version|set-access|set-notifications|rotate|revoke");
|
|
4409
|
+
}
|
|
4410
|
+
async function dispatchGemRuns(ctx, args, linkedProject) {
|
|
4411
|
+
const [verb, ...rest] = args;
|
|
4412
|
+
const flags = parseLocalFlags(rest);
|
|
4413
|
+
if (verb === "list") {
|
|
4414
|
+
const params = {
|
|
4415
|
+
path: gemPathFromArgs(flags, linkedProject)
|
|
4416
|
+
};
|
|
4417
|
+
copyStringFlag(flags, params, "--cursor", "cursor");
|
|
4418
|
+
copyStringFlag(flags, params, "--status", "status");
|
|
4419
|
+
copyOptionalIntegerFlag(flags, params, "--limit", "limit");
|
|
4420
|
+
await emitGemTool(ctx, "gem.runs_list", params);
|
|
4421
|
+
return;
|
|
4422
|
+
}
|
|
4423
|
+
if (verb === "show" || verb === "cancel" || verb === "retry") {
|
|
4424
|
+
const runId = lifecycleResourceArg(flags, `Usage: uru gems runs ${verb} <gem> <run>`);
|
|
4425
|
+
await emitGemTool(ctx, verb === "show" ? "gem.run_get" : verb === "cancel" ? "gem.run_cancel" : "gem.run_retry", {
|
|
4426
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
4427
|
+
run_id: runId
|
|
4428
|
+
});
|
|
4429
|
+
return;
|
|
4430
|
+
}
|
|
4431
|
+
throw new CliError("Usage: uru gems runs list <gem> | show|cancel|retry <gem> <run>");
|
|
4432
|
+
}
|
|
4433
|
+
function extractVersionSaveSummary(value) {
|
|
4434
|
+
const objects = collectJsonObjects(value);
|
|
4435
|
+
const version = versionLabel(objects);
|
|
4436
|
+
return `Saved Version${version === null ? "" : ` ${version}`}. No Links changed.`;
|
|
4437
|
+
}
|
|
4438
|
+
function extractVersionRestoreSummary(value) {
|
|
4439
|
+
const version = versionLabel(collectJsonObjects(value));
|
|
4440
|
+
return `Restored Version${version === null ? "" : ` ${version}`} to Draft. No Links changed.`;
|
|
4441
|
+
}
|
|
4442
|
+
function extractReleaseSummary(value) {
|
|
4443
|
+
const objects = collectJsonObjects(value);
|
|
4444
|
+
const version = versionLabel(objects);
|
|
4445
|
+
const state = stringFieldFromJsonObjects(objects, [
|
|
4446
|
+
"state",
|
|
4447
|
+
"status",
|
|
4448
|
+
"release_state",
|
|
4449
|
+
"release_status"
|
|
4450
|
+
]);
|
|
4451
|
+
const stateLabel = state === null || state === "ready" || state === "succeeded" ? "ready" : state === "queued" || state === "building" || state === "preparing" ? "preparing" : state.replaceAll("_", " ").toLowerCase();
|
|
4452
|
+
return `Release ${stateLabel} for Version${version === null ? "" : ` ${version}`}. No Links changed.`;
|
|
4453
|
+
}
|
|
4454
|
+
function extractLinkCreateSummary(value) {
|
|
4455
|
+
const objects = collectJsonObjects(value);
|
|
4456
|
+
const name = stringFieldFromJsonObjects(objects, ["name", "link_name"]);
|
|
4457
|
+
const version = versionLabel(objects);
|
|
4458
|
+
return `Created ${name ?? "Link"}. It opens Version${version === null ? "" : ` ${version}`}. Other Links did not change.`;
|
|
4459
|
+
}
|
|
4460
|
+
function extractLinkVersionSummary(value) {
|
|
4461
|
+
const objects = collectJsonObjects(value);
|
|
4462
|
+
const name = stringFieldFromJsonObjects(objects, ["name", "link_name"]);
|
|
4463
|
+
const version = versionLabel(objects);
|
|
4464
|
+
return `${name ?? "Link"} now opens Version${version === null ? "" : ` ${version}`}. Other Links did not change.`;
|
|
4465
|
+
}
|
|
4466
|
+
function versionLabel(objects) {
|
|
4467
|
+
for (const object of objects) {
|
|
4468
|
+
for (const key of [
|
|
4469
|
+
"version_number",
|
|
4470
|
+
"versionNumber",
|
|
4471
|
+
"gem_version_number",
|
|
4472
|
+
"gemVersionNumber"
|
|
4473
|
+
]) {
|
|
4474
|
+
const value = object[key];
|
|
4475
|
+
if (typeof value === "number") {
|
|
4476
|
+
if (Number.isSafeInteger(value) && value > 0) {
|
|
4477
|
+
return String(value);
|
|
4478
|
+
}
|
|
4479
|
+
throw new CliError(`Invalid unsafe Version number: ${value}`);
|
|
4480
|
+
}
|
|
4481
|
+
if (typeof value === "string") {
|
|
4482
|
+
const normalized = value.trim();
|
|
4483
|
+
if (/^[1-9]\d*$/u.test(normalized) && BigInt(normalized) <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
4484
|
+
return normalized;
|
|
4485
|
+
}
|
|
4486
|
+
throw new CliError(`Invalid unsafe Version number: ${value}`);
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
return null;
|
|
4491
|
+
}
|
|
4492
|
+
function collectJsonObjects(value) {
|
|
4493
|
+
const objects = [];
|
|
4494
|
+
collectJsonObjectsInto(value, objects);
|
|
4495
|
+
return objects;
|
|
4496
|
+
}
|
|
4497
|
+
function collectJsonObjectsInto(value, objects) {
|
|
4498
|
+
if (Array.isArray(value)) {
|
|
4499
|
+
for (const item of value) {
|
|
4500
|
+
collectJsonObjectsInto(item, objects);
|
|
4501
|
+
}
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
const object = jsonObjectOrNull(value);
|
|
4505
|
+
if (object === null) {
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
objects.push(object);
|
|
4509
|
+
for (const child of Object.values(object)) {
|
|
4510
|
+
collectJsonObjectsInto(child, objects);
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
function lifecycleResourceArg(flags, usage) {
|
|
4514
|
+
return lifecycleResourceArgs(flags, 1, usage)[0];
|
|
4515
|
+
}
|
|
4516
|
+
function lifecycleResourceArgs(flags, count, usage) {
|
|
4517
|
+
const resources = flags.positionals.filter((positional) => !positional.startsWith("library/"));
|
|
4518
|
+
if (resources.length !== count) {
|
|
4519
|
+
throw new CliError(usage);
|
|
4520
|
+
}
|
|
4521
|
+
return resources;
|
|
4522
|
+
}
|
|
4523
|
+
function copyOptionalIntegerFlag(flags, params, flag, key) {
|
|
4524
|
+
if (flags.values[flag] === undefined) {
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
const value = integerFlag(flags, flag, 0);
|
|
4528
|
+
if (value <= 0) {
|
|
4529
|
+
throw new CliError(`${flag} must be greater than zero`);
|
|
3721
4530
|
}
|
|
3722
|
-
|
|
4531
|
+
params[key] = value;
|
|
3723
4532
|
}
|
|
3724
|
-
function
|
|
3725
|
-
|
|
3726
|
-
|
|
4533
|
+
function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
|
|
4534
|
+
const allowed = new Set(allowedFlags);
|
|
4535
|
+
const unexpected = Object.keys(flags.values).filter((flag) => flags.values[flag] !== undefined && !allowed.has(flag));
|
|
4536
|
+
if (unexpected.length > 0) {
|
|
4537
|
+
throw new CliError(`${commandName} does not support ${unexpected.join(", ")}.`);
|
|
3727
4538
|
}
|
|
3728
|
-
return candidate.startsWith(value) || value.startsWith(candidate) || levenshtein(candidate, value) <= 2;
|
|
3729
4539
|
}
|
|
3730
|
-
function
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
const cost = left[leftIndex] === right[rightIndex] ? 0 : 1;
|
|
3738
|
-
previous[rightIndex + 1] = Math.min(upper + 1, (previous[rightIndex] ?? 0) + 1, diagonal + cost);
|
|
3739
|
-
diagonal = upper;
|
|
4540
|
+
function stringFieldFromJsonObjects(objects, fields) {
|
|
4541
|
+
for (const object of objects) {
|
|
4542
|
+
for (const field of fields) {
|
|
4543
|
+
const value = object[field];
|
|
4544
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
4545
|
+
return value.trim();
|
|
4546
|
+
}
|
|
3740
4547
|
}
|
|
3741
4548
|
}
|
|
3742
|
-
return
|
|
4549
|
+
return null;
|
|
4550
|
+
}
|
|
4551
|
+
function jsonObjectOrNull(value) {
|
|
4552
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4553
|
+
return null;
|
|
4554
|
+
}
|
|
4555
|
+
return value;
|
|
3743
4556
|
}
|
|
3744
4557
|
|
|
3745
4558
|
// src/gem-lifecycle-commands.ts
|
|
3746
|
-
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile4 } from "node:fs/promises";
|
|
3747
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
3748
|
-
import { join as join3, resolve as resolve3 } from "node:path";
|
|
3749
4559
|
async function dispatchGems(ctx, sub, args, linkedProject) {
|
|
3750
|
-
const
|
|
4560
|
+
const flags = parseLocalFlags(args);
|
|
4561
|
+
const gemPath = () => gemPathFromArgs(flags, linkedProject);
|
|
3751
4562
|
if (sub === "inspect") {
|
|
3752
4563
|
await emitGemTool(ctx, "gem.inspect", { path: gemPath() });
|
|
3753
4564
|
return;
|
|
@@ -3760,12 +4571,19 @@ async function dispatchGems(ctx, sub, args, linkedProject) {
|
|
|
3760
4571
|
await gemBuild(ctx, args, linkedProject);
|
|
3761
4572
|
return;
|
|
3762
4573
|
}
|
|
3763
|
-
if (sub
|
|
3764
|
-
await dispatchGemVersions(ctx, args, linkedProject);
|
|
4574
|
+
if (await dispatchGemLifecycleResourceCommand(ctx, sub, args, linkedProject)) {
|
|
3765
4575
|
return;
|
|
3766
4576
|
}
|
|
3767
4577
|
if (sub === "checks") {
|
|
3768
|
-
|
|
4578
|
+
const versionId = requireCommandValue(flags.positionals[0], "Usage: uru gems checks <version> [gem]");
|
|
4579
|
+
await emitGemTool(ctx, "gem.checks", {
|
|
4580
|
+
path: gemPathFromArgs(shiftPositionals(flags, 1), linkedProject),
|
|
4581
|
+
version_id: versionId
|
|
4582
|
+
});
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
if (sub === "logs") {
|
|
4586
|
+
await gemLogs(ctx, args, linkedProject);
|
|
3769
4587
|
return;
|
|
3770
4588
|
}
|
|
3771
4589
|
if (sub === "status") {
|
|
@@ -3783,7 +4601,7 @@ async function dispatchGems(ctx, sub, args, linkedProject) {
|
|
|
3783
4601
|
await gemWrite(ctx, args);
|
|
3784
4602
|
return;
|
|
3785
4603
|
}
|
|
3786
|
-
throw new CliError("Usage: uru gems inspect|validate|build|versions|checks|status|read|write");
|
|
4604
|
+
throw new CliError("Usage: uru gems inspect|validate|build|edits|versions|releases|links|runs|checks|status|read|write");
|
|
3787
4605
|
}
|
|
3788
4606
|
async function gemInit(ctx, args) {
|
|
3789
4607
|
const flags = parseLocalFlags(args);
|
|
@@ -3802,67 +4620,10 @@ async function gemInit(ctx, args) {
|
|
|
3802
4620
|
}
|
|
3803
4621
|
await emitGemTool(ctx, "gem.init", params);
|
|
3804
4622
|
}
|
|
3805
|
-
async function gemDeploy(ctx, args, linkedProject) {
|
|
3806
|
-
const flags = parseLocalFlags(args);
|
|
3807
|
-
rejectUnsupportedLocalFlags(flags, ["--runtime-provider", "--provider-deployment-id"], "Provider routing is chosen by the Gem runtime contract; the CLI does not accept provider-internal deploy flags.");
|
|
3808
|
-
assertAllowedLocalFlags(flags, [
|
|
3809
|
-
"--build-id",
|
|
3810
|
-
"--dry-run",
|
|
3811
|
-
"--force-build",
|
|
3812
|
-
"--gem",
|
|
3813
|
-
"--link-id",
|
|
3814
|
-
"--no-publish",
|
|
3815
|
-
"--no-wait",
|
|
3816
|
-
"--path",
|
|
3817
|
-
"--prod",
|
|
3818
|
-
"--skip-promote",
|
|
3819
|
-
"--source-version-id"
|
|
3820
|
-
], "uru deploy");
|
|
3821
|
-
if (flags.booleans.has("--prod") && flags.booleans.has("--no-publish")) {
|
|
3822
|
-
throw new CliError("uru deploy --prod cannot be combined with --no-publish. Use --skip-promote for a staged production deployment.");
|
|
3823
|
-
}
|
|
3824
|
-
if (flags.booleans.has("--dry-run")) {
|
|
3825
|
-
const buildArgs = [...flags.positionals, "--dry-run"];
|
|
3826
|
-
if (flags.booleans.has("--force-build")) {
|
|
3827
|
-
buildArgs.push("--force");
|
|
3828
|
-
}
|
|
3829
|
-
const sourceVersionId = flags.values["--source-version-id"];
|
|
3830
|
-
if (sourceVersionId !== undefined) {
|
|
3831
|
-
buildArgs.push("--source-version-id", sourceVersionId);
|
|
3832
|
-
}
|
|
3833
|
-
await gemBuild(ctx, buildArgs, linkedProject);
|
|
3834
|
-
return;
|
|
3835
|
-
}
|
|
3836
|
-
const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
|
|
3837
|
-
const publish = !flags.booleans.has("--no-publish") && !flags.booleans.has("--skip-promote");
|
|
3838
|
-
const params = {
|
|
3839
|
-
path: gemPath,
|
|
3840
|
-
publish
|
|
3841
|
-
};
|
|
3842
|
-
if (flags.booleans.has("--prod")) {
|
|
3843
|
-
params["metadata"] = {
|
|
3844
|
-
deployment_intent: "production",
|
|
3845
|
-
promotion_strategy: publish ? "immediate" : "staged"
|
|
3846
|
-
};
|
|
3847
|
-
}
|
|
3848
|
-
copyStringFlag(flags, params, "--build-id", "build_id");
|
|
3849
|
-
copyStringFlag(flags, params, "--link-id", "link_id");
|
|
3850
|
-
copyStringFlag(flags, params, "--source-version-id", "source_version_id");
|
|
3851
|
-
if (flags.booleans.has("--force-build")) {
|
|
3852
|
-
params["force_build"] = true;
|
|
3853
|
-
}
|
|
3854
|
-
if (flags.booleans.has("--no-wait")) {
|
|
3855
|
-
params["wait"] = false;
|
|
3856
|
-
}
|
|
3857
|
-
await emitGemTool(ctx, "gem.deploy", params, {
|
|
3858
|
-
streamStart: { operation: "gem.deploy", path: gemPath },
|
|
3859
|
-
textExtractor: extractDeploymentUrl
|
|
3860
|
-
});
|
|
3861
|
-
}
|
|
3862
4623
|
async function gemBuild(ctx, args, linkedProject) {
|
|
3863
4624
|
const flags = parseLocalFlags(args);
|
|
3864
|
-
|
|
3865
|
-
const gemPath = gemPathFromArgs(flags
|
|
4625
|
+
assertAllowedLocalFlags2(flags, ["--dry-run", "--force", "--gem", "--outdir", "--path", "--source-revision-id"], "uru build");
|
|
4626
|
+
const gemPath = gemPathFromArgs(flags, linkedProject);
|
|
3866
4627
|
const params = {
|
|
3867
4628
|
path: gemPath
|
|
3868
4629
|
};
|
|
@@ -3872,7 +4633,7 @@ async function gemBuild(ctx, args, linkedProject) {
|
|
|
3872
4633
|
if (flags.booleans.has("--dry-run")) {
|
|
3873
4634
|
params["dry_run"] = true;
|
|
3874
4635
|
}
|
|
3875
|
-
copyStringFlag(flags, params, "--source-
|
|
4636
|
+
copyStringFlag(flags, params, "--source-revision-id", "source_revision_id");
|
|
3876
4637
|
if (ctx.outputFormat === "stream-json") {
|
|
3877
4638
|
writeStreamJson(ctx.io, "started", { operation: "gem.build", path: gemPath });
|
|
3878
4639
|
writeStreamJson(ctx.io, "phase", {
|
|
@@ -3895,63 +4656,9 @@ async function gemBuild(ctx, args, linkedProject) {
|
|
|
3895
4656
|
writeText(ctx.io, JSON.stringify(unwrapped, null, 2));
|
|
3896
4657
|
}
|
|
3897
4658
|
}
|
|
3898
|
-
async function gemPromote(ctx, args, linkedProject) {
|
|
3899
|
-
const flags = parseLocalFlags(args);
|
|
3900
|
-
assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
|
|
3901
|
-
await deployVersionToPublishedLink(ctx, flags, linkedProject, {
|
|
3902
|
-
operation: "promote",
|
|
3903
|
-
usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] | uru promote --deployment-id <deployment-id> [library/name.gem]"
|
|
3904
|
-
});
|
|
3905
|
-
}
|
|
3906
|
-
async function gemChecks(ctx, args, linkedProject) {
|
|
3907
|
-
const flags = parseLocalFlags(args);
|
|
3908
|
-
if (flags.positionals[0] === "run") {
|
|
3909
|
-
await gemRunCheck(ctx, flags, linkedProject);
|
|
3910
|
-
return;
|
|
3911
|
-
}
|
|
3912
|
-
assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--path"], "uru gems checks");
|
|
3913
|
-
const explicitDeploymentId = flags.values["--deployment-id"];
|
|
3914
|
-
const gemPathArgs = explicitDeploymentId === undefined ? flags.positionals.slice(1) : flags.positionals;
|
|
3915
|
-
const params = {
|
|
3916
|
-
path: gemPathFromArgs(gemPathArgs, linkedProject)
|
|
3917
|
-
};
|
|
3918
|
-
if (explicitDeploymentId !== undefined) {
|
|
3919
|
-
params["deployment_id"] = explicitDeploymentId;
|
|
3920
|
-
} else if (flags.positionals[0] !== undefined) {
|
|
3921
|
-
params["version_id"] = flags.positionals[0];
|
|
3922
|
-
}
|
|
3923
|
-
await emitGemTool(ctx, "gem.checks", params);
|
|
3924
|
-
}
|
|
3925
|
-
async function gemRunCheck(ctx, flags, linkedProject) {
|
|
3926
|
-
assertAllowedLocalFlags(flags, ["--blocking", "--deep", "--deployment-id", "--gem", "--path"], "uru gems checks run");
|
|
3927
|
-
const kind = requireCommandValue(flags.positionals[1], "Usage: uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]");
|
|
3928
|
-
const params = {
|
|
3929
|
-
path: gemPathFromArgs(flags.positionals.slice(2), linkedProject)
|
|
3930
|
-
};
|
|
3931
|
-
copyStringFlag(flags, params, "--deployment-id", "deployment_id");
|
|
3932
|
-
if (flags.booleans.has("--blocking")) {
|
|
3933
|
-
params["blocking"] = true;
|
|
3934
|
-
}
|
|
3935
|
-
if (kind === "browser" || kind === "browser_verify") {
|
|
3936
|
-
if (flags.booleans.has("--deep")) {
|
|
3937
|
-
throw new CliError("--deep is only valid for `uru gems checks run security`.");
|
|
3938
|
-
}
|
|
3939
|
-
await emitGemTool(ctx, "gem.browser_verify", params);
|
|
3940
|
-
return;
|
|
3941
|
-
}
|
|
3942
|
-
if (kind === "security" || kind === "security_scan") {
|
|
3943
|
-
if (flags.booleans.has("--deep")) {
|
|
3944
|
-
params["deep"] = true;
|
|
3945
|
-
}
|
|
3946
|
-
await emitGemTool(ctx, "gem.security_scan", params);
|
|
3947
|
-
return;
|
|
3948
|
-
}
|
|
3949
|
-
throw new CliError("Unknown Gem check runner. Use `uru gems checks run browser` or `uru gems checks run security`.");
|
|
3950
|
-
}
|
|
3951
4659
|
async function gemLogs(ctx, args, linkedProject) {
|
|
3952
4660
|
const flags = parseLocalFlags(args);
|
|
3953
|
-
|
|
3954
|
-
"--deployment-id",
|
|
4661
|
+
assertAllowedLocalFlags2(flags, [
|
|
3955
4662
|
"--follow",
|
|
3956
4663
|
"--follow-duration-ms",
|
|
3957
4664
|
"--gem",
|
|
@@ -3959,15 +4666,16 @@ async function gemLogs(ctx, args, linkedProject) {
|
|
|
3959
4666
|
"--limit",
|
|
3960
4667
|
"--path",
|
|
3961
4668
|
"--poll-interval-ms",
|
|
3962
|
-
"--
|
|
4669
|
+
"--release",
|
|
4670
|
+
"--run"
|
|
3963
4671
|
], "uru logs");
|
|
3964
4672
|
const params = {
|
|
3965
|
-
path: gemPathFromArgs(flags
|
|
4673
|
+
path: gemPathFromArgs(flags, linkedProject),
|
|
3966
4674
|
limit: integerFlag(flags, "--limit", 100)
|
|
3967
4675
|
};
|
|
3968
4676
|
copyStringFlag(flags, params, "--level", "level");
|
|
3969
|
-
copyStringFlag(flags, params, "--
|
|
3970
|
-
copyStringFlag(flags, params, "--
|
|
4677
|
+
copyStringFlag(flags, params, "--release", "release_id");
|
|
4678
|
+
copyStringFlag(flags, params, "--run", "run_id");
|
|
3971
4679
|
if (flags.booleans.has("--follow")) {
|
|
3972
4680
|
await followGemLogs(ctx, params, {
|
|
3973
4681
|
durationMs: boundedIntegerFlag(flags, "--follow-duration-ms", DEFAULT_LOG_FOLLOW_DURATION_MS, MAX_LOG_FOLLOW_DURATION_MS),
|
|
@@ -4060,13 +4768,13 @@ function emitFollowLogEntry(ctx, entry) {
|
|
|
4060
4768
|
writeText(ctx.io, formatLogEntry(entry));
|
|
4061
4769
|
}
|
|
4062
4770
|
function normalizeLogEntries(value) {
|
|
4063
|
-
const root =
|
|
4771
|
+
const root = jsonObjectOrNull2(value);
|
|
4064
4772
|
const candidate = root === null ? value : root["logs"];
|
|
4065
4773
|
if (!Array.isArray(candidate)) {
|
|
4066
4774
|
return [];
|
|
4067
4775
|
}
|
|
4068
4776
|
return candidate.flatMap((entry, index) => {
|
|
4069
|
-
const object =
|
|
4777
|
+
const object = jsonObjectOrNull2(entry);
|
|
4070
4778
|
if (object === null) {
|
|
4071
4779
|
return [];
|
|
4072
4780
|
}
|
|
@@ -4109,35 +4817,6 @@ function boundedIntegerFlag(flags, name, fallback, maximum) {
|
|
|
4109
4817
|
}
|
|
4110
4818
|
return Math.min(value, maximum);
|
|
4111
4819
|
}
|
|
4112
|
-
async function gemOpen(ctx, args, linkedProject) {
|
|
4113
|
-
const result = await executeOperation(ctx, "gem.links_list", {
|
|
4114
|
-
path: gemPathFromArgs(args, linkedProject)
|
|
4115
|
-
});
|
|
4116
|
-
const url = firstUrl(unwrapToolResult(result));
|
|
4117
|
-
if (url === undefined) {
|
|
4118
|
-
throw new CliError("No published URL found for this Gem. Run `uru deploy` first.");
|
|
4119
|
-
}
|
|
4120
|
-
ctx.json ? writeJson(ctx.io, { url }) : writeText(ctx.io, url);
|
|
4121
|
-
}
|
|
4122
|
-
async function gemRollback(ctx, args, linkedProject) {
|
|
4123
|
-
const flags = parseLocalFlags(args);
|
|
4124
|
-
assertAllowedLocalFlags(flags, [
|
|
4125
|
-
"--deployment-id",
|
|
4126
|
-
"--gem",
|
|
4127
|
-
"--link-id",
|
|
4128
|
-
"--no-wait",
|
|
4129
|
-
"--path",
|
|
4130
|
-
"--pct",
|
|
4131
|
-
"--yes"
|
|
4132
|
-
], "uru rollback");
|
|
4133
|
-
if (!ctx.yes && !flags.booleans.has("--yes")) {
|
|
4134
|
-
throw new CliError("Rollback requires --yes to confirm the link pointer change.");
|
|
4135
|
-
}
|
|
4136
|
-
await deployVersionToPublishedLink(ctx, flags, linkedProject, {
|
|
4137
|
-
operation: "rollback",
|
|
4138
|
-
usage: "Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes"
|
|
4139
|
-
});
|
|
4140
|
-
}
|
|
4141
4820
|
async function gemWrite(ctx, args) {
|
|
4142
4821
|
const flags = parseLocalFlags(args);
|
|
4143
4822
|
const gemPath = requireCommandValue(flags.positionals[0], "Usage: uru gems write <gem-path> <file> --content <text>|--file <local-file>");
|
|
@@ -4157,142 +4836,13 @@ function gemFilePath(gemPath, filePath) {
|
|
|
4157
4836
|
const normalizedFilePath = trimLeadingSlashes(filePath);
|
|
4158
4837
|
return `${normalizedGemPath}/${normalizedFilePath}`;
|
|
4159
4838
|
}
|
|
4160
|
-
|
|
4161
|
-
const [verb, ...rest] = args;
|
|
4162
|
-
if (verb === "upload") {
|
|
4163
|
-
await gemVersionUpload(ctx, rest, linkedProject);
|
|
4164
|
-
return;
|
|
4165
|
-
}
|
|
4166
|
-
if (verb === "deploy") {
|
|
4167
|
-
const flags = parseLocalFlags(rest);
|
|
4168
|
-
rejectUnsupportedLocalFlags(flags, ["--runtime-provider", "--provider-deployment-id"], "Provider routing is chosen by the Gem runtime contract; version deploy does not accept provider-internal flags.");
|
|
4169
|
-
assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct", "--skip-promote"], "uru gems versions deploy");
|
|
4170
|
-
const version = requireCommandValue(flags.positionals[0], "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]");
|
|
4171
|
-
const [versionId, trafficPercent] = version.split("@", 2);
|
|
4172
|
-
const gemPath = gemPathFromArgs(flags.positionals.slice(1), linkedProject);
|
|
4173
|
-
const pct = flags.values["--pct"] ?? trafficPercent;
|
|
4174
|
-
const trafficPercentValue = pct === undefined || pct === "" ? 100 : parseTrafficPercent(pct);
|
|
4175
|
-
const publish = !flags.booleans.has("--skip-promote");
|
|
4176
|
-
if (publish && trafficPercentValue !== 100) {
|
|
4177
|
-
throw new CliError("Gradual Gem traffic shifts are not enabled yet. Use --skip-promote to create the deployment without changing live traffic, or deploy with --pct 100.");
|
|
4178
|
-
}
|
|
4179
|
-
const params = {
|
|
4180
|
-
path: gemPath,
|
|
4181
|
-
version_id: requireCommandValue(versionId, "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]"),
|
|
4182
|
-
traffic_percent: trafficPercentValue,
|
|
4183
|
-
publish
|
|
4184
|
-
};
|
|
4185
|
-
copyStringFlag(flags, params, "--link-id", "link_id");
|
|
4186
|
-
if (flags.booleans.has("--no-wait")) {
|
|
4187
|
-
params["wait"] = false;
|
|
4188
|
-
}
|
|
4189
|
-
await emitGemTool(ctx, "gem.version_deploy", params, {
|
|
4190
|
-
streamStart: { operation: "gem.version_deploy", path: gemPath },
|
|
4191
|
-
textExtractor: extractDeploymentUrl
|
|
4192
|
-
});
|
|
4193
|
-
return;
|
|
4194
|
-
}
|
|
4195
|
-
throw new CliError("Usage: uru gems versions upload [library/name.gem] | deploy <version-id> [library/name.gem]");
|
|
4196
|
-
}
|
|
4197
|
-
async function gemVersionUpload(ctx, args, linkedProject) {
|
|
4198
|
-
const flags = parseLocalFlags(args);
|
|
4199
|
-
assertAllowedLocalFlags(flags, [
|
|
4200
|
-
"--force",
|
|
4201
|
-
"--gem",
|
|
4202
|
-
"--message",
|
|
4203
|
-
"--path",
|
|
4204
|
-
"--preview-alias",
|
|
4205
|
-
"--source-version-id",
|
|
4206
|
-
"--tag",
|
|
4207
|
-
"--tags"
|
|
4208
|
-
], "uru gems versions upload");
|
|
4209
|
-
const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
|
|
4210
|
-
const params = {
|
|
4211
|
-
path: gemPath
|
|
4212
|
-
};
|
|
4213
|
-
if (flags.booleans.has("--force")) {
|
|
4214
|
-
params["force"] = true;
|
|
4215
|
-
}
|
|
4216
|
-
copyStringFlag(flags, params, "--source-version-id", "source_version_id");
|
|
4217
|
-
copyStringFlag(flags, params, "--message", "message");
|
|
4218
|
-
const previewAlias = flags.values["--preview-alias"];
|
|
4219
|
-
if (previewAlias !== undefined) {
|
|
4220
|
-
params["preview_alias"] = validatePreviewAlias(previewAlias);
|
|
4221
|
-
}
|
|
4222
|
-
const tags = tagsFromFlags(flags);
|
|
4223
|
-
if (tags.length > 0) {
|
|
4224
|
-
params["tags"] = tags;
|
|
4225
|
-
}
|
|
4226
|
-
await emitGemTool(ctx, "gem.version_upload", params, {
|
|
4227
|
-
streamStart: { operation: "gem.version_upload", path: gemPath },
|
|
4228
|
-
textExtractor: extractVersionUploadSummary
|
|
4229
|
-
});
|
|
4230
|
-
}
|
|
4231
|
-
function parseTrafficPercent(raw) {
|
|
4232
|
-
const value = Number(raw);
|
|
4233
|
-
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
4234
|
-
throw new CliError("--pct or @pct must be a number from 0 through 100");
|
|
4235
|
-
}
|
|
4236
|
-
return value;
|
|
4237
|
-
}
|
|
4238
|
-
async function deployVersionToPublishedLink(ctx, flags, linkedProject, options) {
|
|
4239
|
-
const deploymentId = flags.values["--deployment-id"];
|
|
4240
|
-
const firstPositional = flags.positionals[0];
|
|
4241
|
-
const versionId = deploymentId === undefined && firstPositional !== undefined ? firstPositional.startsWith("library/") ? undefined : firstPositional : undefined;
|
|
4242
|
-
if (deploymentId === undefined && versionId === undefined) {
|
|
4243
|
-
throw new CliError(options.usage);
|
|
4244
|
-
}
|
|
4245
|
-
const gemPath = gemPathFromArgs(versionId === undefined ? flags.positionals : flags.positionals.slice(1), linkedProject);
|
|
4246
|
-
const trafficPercent = parseTrafficPercent(flags.values["--pct"] ?? "100");
|
|
4247
|
-
if (trafficPercent !== 100) {
|
|
4248
|
-
throw new CliError(`${options.operation} does not support partial traffic shifts yet. Deploy the version with --skip-promote for validation, or ${options.operation} with --pct 100.`);
|
|
4249
|
-
}
|
|
4250
|
-
const params = {
|
|
4251
|
-
path: gemPath
|
|
4252
|
-
};
|
|
4253
|
-
if (deploymentId === undefined) {
|
|
4254
|
-
params["version_id"] = requireCommandValue(versionId, options.usage);
|
|
4255
|
-
} else {
|
|
4256
|
-
params["app_deployment_id"] = deploymentId;
|
|
4257
|
-
}
|
|
4258
|
-
copyStringFlag(flags, params, "--link-id", "link_id");
|
|
4259
|
-
if (flags.booleans.has("--no-wait")) {
|
|
4260
|
-
params["wait"] = false;
|
|
4261
|
-
}
|
|
4262
|
-
const operation = `gem.${options.operation}`;
|
|
4263
|
-
await emitGemTool(ctx, operation, params, {
|
|
4264
|
-
streamStart: { operation, path: gemPath },
|
|
4265
|
-
textExtractor: extractDeploymentUrl
|
|
4266
|
-
});
|
|
4267
|
-
}
|
|
4268
|
-
function tagsFromFlags(flags) {
|
|
4269
|
-
const raw = flags.values["--tags"] ?? flags.values["--tag"];
|
|
4270
|
-
if (raw === undefined) {
|
|
4271
|
-
return [];
|
|
4272
|
-
}
|
|
4273
|
-
return raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
4274
|
-
}
|
|
4275
|
-
function validatePreviewAlias(rawAlias) {
|
|
4276
|
-
const alias = rawAlias.trim();
|
|
4277
|
-
if (alias.length === 0 || alias.length > 63 || !/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(alias)) {
|
|
4278
|
-
throw new CliError("--preview-alias must be a DNS-safe label: 1-63 characters, start with a lowercase letter, contain only lowercase letters, numbers, and dashes, and not end with a dash.");
|
|
4279
|
-
}
|
|
4280
|
-
return alias;
|
|
4281
|
-
}
|
|
4282
|
-
function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
|
|
4839
|
+
function assertAllowedLocalFlags2(flags, allowedFlags, commandName) {
|
|
4283
4840
|
const allowed = new Set(allowedFlags);
|
|
4284
4841
|
const unexpected = localFlagNames(flags).filter((flag) => !allowed.has(flag));
|
|
4285
4842
|
if (unexpected.length > 0) {
|
|
4286
4843
|
throw new CliError(`${commandName} does not support ${unexpected.join(", ")}.`);
|
|
4287
4844
|
}
|
|
4288
4845
|
}
|
|
4289
|
-
function rejectUnsupportedLocalFlags(flags, unsupportedFlags, message) {
|
|
4290
|
-
const unsupported = new Set(unsupportedFlags);
|
|
4291
|
-
const found = localFlagNames(flags).filter((flag) => unsupported.has(flag));
|
|
4292
|
-
if (found.length > 0) {
|
|
4293
|
-
throw new CliError(`${message} Unsupported flag(s): ${found.join(", ")}.`);
|
|
4294
|
-
}
|
|
4295
|
-
}
|
|
4296
4846
|
function localFlagNames(flags) {
|
|
4297
4847
|
return [...new Set([...Object.keys(flags.values), ...flags.booleans])];
|
|
4298
4848
|
}
|
|
@@ -4307,10 +4857,10 @@ async function writeBuildManifest(ctx, outdir, buildResult) {
|
|
|
4307
4857
|
`, { mode: 384 });
|
|
4308
4858
|
}
|
|
4309
4859
|
function gemBuildManifestFromBuildResult(buildResult) {
|
|
4310
|
-
const root =
|
|
4311
|
-
const build =
|
|
4312
|
-
const buildResultDetails =
|
|
4313
|
-
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"]);
|
|
4314
4864
|
const candidates = [
|
|
4315
4865
|
root?.["gem_build_manifest"],
|
|
4316
4866
|
root?.["gemBuildManifest"],
|
|
@@ -4330,7 +4880,7 @@ function gemBuildManifestFromBuildResult(buildResult) {
|
|
|
4330
4880
|
throw new CliError("Build result did not include a GemBuildManifestV1. Re-run after the backend build manifest projection is deployed, or use --force to create a fresh ready build.");
|
|
4331
4881
|
}
|
|
4332
4882
|
function normalizeGemBuildManifest(value) {
|
|
4333
|
-
const manifest =
|
|
4883
|
+
const manifest = jsonObjectOrNull2(value);
|
|
4334
4884
|
if (manifest === null) {
|
|
4335
4885
|
return null;
|
|
4336
4886
|
}
|
|
@@ -4357,10 +4907,10 @@ function normalizeGemBuildManifest(value) {
|
|
|
4357
4907
|
};
|
|
4358
4908
|
}
|
|
4359
4909
|
function gemProviderDecisionFromBuildResult(buildResult, manifest) {
|
|
4360
|
-
const root =
|
|
4361
|
-
const build =
|
|
4362
|
-
const buildResultDetails =
|
|
4363
|
-
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"]);
|
|
4364
4914
|
const runtimeProfile = firstJsonObject([
|
|
4365
4915
|
root?.["runtime_profile"],
|
|
4366
4916
|
root?.["runtimeProfile"],
|
|
@@ -4397,8 +4947,8 @@ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
|
|
|
4397
4947
|
schema: "uru.gem.provider_decision.v1",
|
|
4398
4948
|
schema_version: 1,
|
|
4399
4949
|
manifest_kind: "gem_provider_decision",
|
|
4400
|
-
provider:
|
|
4401
|
-
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"]),
|
|
4402
4952
|
runtime_profile: runtimeProfile,
|
|
4403
4953
|
provider_decision: providerDecision,
|
|
4404
4954
|
runtime_contract: runtimeContract,
|
|
@@ -4412,14 +4962,14 @@ function gemProviderDecisionFromBuildResult(buildResult, manifest) {
|
|
|
4412
4962
|
}
|
|
4413
4963
|
function firstJsonObject(values) {
|
|
4414
4964
|
for (const value of values) {
|
|
4415
|
-
const object =
|
|
4965
|
+
const object = jsonObjectOrNull2(value);
|
|
4416
4966
|
if (object !== null) {
|
|
4417
4967
|
return object;
|
|
4418
4968
|
}
|
|
4419
4969
|
}
|
|
4420
4970
|
return null;
|
|
4421
4971
|
}
|
|
4422
|
-
function
|
|
4972
|
+
function stringFieldFromJsonObjects2(objects, keys) {
|
|
4423
4973
|
for (const object of objects) {
|
|
4424
4974
|
for (const key of keys) {
|
|
4425
4975
|
const value = object[key];
|
|
@@ -4430,57 +4980,12 @@ function stringFieldFromJsonObjects(objects, keys) {
|
|
|
4430
4980
|
}
|
|
4431
4981
|
return null;
|
|
4432
4982
|
}
|
|
4433
|
-
function
|
|
4983
|
+
function jsonObjectOrNull2(value) {
|
|
4434
4984
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4435
4985
|
return null;
|
|
4436
4986
|
}
|
|
4437
4987
|
return value;
|
|
4438
4988
|
}
|
|
4439
|
-
function extractDeploymentUrl(value) {
|
|
4440
|
-
return firstUrl(value);
|
|
4441
|
-
}
|
|
4442
|
-
function extractVersionUploadSummary(value) {
|
|
4443
|
-
const objects = collectJsonObjects(value);
|
|
4444
|
-
const versionId = stringFieldFromJsonObjects(objects, [
|
|
4445
|
-
"version_id",
|
|
4446
|
-
"versionId",
|
|
4447
|
-
"id"
|
|
4448
|
-
]);
|
|
4449
|
-
const previewUrl = stringFieldFromJsonObjects(objects, [
|
|
4450
|
-
"preview_url",
|
|
4451
|
-
"previewUrl",
|
|
4452
|
-
"preview_alias_url",
|
|
4453
|
-
"previewAliasUrl",
|
|
4454
|
-
"url"
|
|
4455
|
-
]) ?? firstUrl(value);
|
|
4456
|
-
const lines = [
|
|
4457
|
-
versionId === null ? null : `Version: ${versionId}`,
|
|
4458
|
-
previewUrl === undefined || previewUrl === null ? null : `Preview: ${previewUrl}`
|
|
4459
|
-
].filter((line) => line !== null);
|
|
4460
|
-
return lines.length === 0 ? undefined : lines.join(`
|
|
4461
|
-
`);
|
|
4462
|
-
}
|
|
4463
|
-
function collectJsonObjects(value) {
|
|
4464
|
-
const objects = [];
|
|
4465
|
-
collectJsonObjectsInto(value, objects);
|
|
4466
|
-
return objects;
|
|
4467
|
-
}
|
|
4468
|
-
function collectJsonObjectsInto(value, objects) {
|
|
4469
|
-
if (Array.isArray(value)) {
|
|
4470
|
-
for (const item of value) {
|
|
4471
|
-
collectJsonObjectsInto(item, objects);
|
|
4472
|
-
}
|
|
4473
|
-
return;
|
|
4474
|
-
}
|
|
4475
|
-
const object = jsonObjectOrNull(value);
|
|
4476
|
-
if (object === null) {
|
|
4477
|
-
return;
|
|
4478
|
-
}
|
|
4479
|
-
objects.push(object);
|
|
4480
|
-
for (const child of Object.values(object)) {
|
|
4481
|
-
collectJsonObjectsInto(child, objects);
|
|
4482
|
-
}
|
|
4483
|
-
}
|
|
4484
4989
|
function trimTrailingSlashes(value) {
|
|
4485
4990
|
let end = value.length;
|
|
4486
4991
|
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
@@ -4496,12 +5001,68 @@ function trimLeadingSlashes(value) {
|
|
|
4496
5001
|
return value.slice(start);
|
|
4497
5002
|
}
|
|
4498
5003
|
|
|
5004
|
+
// src/gem-open-command.ts
|
|
5005
|
+
async function gemOpen(ctx, args, linkedProject) {
|
|
5006
|
+
const flags = parseLocalFlags(args);
|
|
5007
|
+
const result = await executeOperation(ctx, "gem.links_list", {
|
|
5008
|
+
path: gemPathFromArgs(flags, linkedProject)
|
|
5009
|
+
});
|
|
5010
|
+
const selector = flags.values["--link"]?.trim();
|
|
5011
|
+
const links = publicLinks(unwrapToolResult(result));
|
|
5012
|
+
const selected = selector === undefined || selector === "" ? links.length === 1 ? links[0] : null : links.find((link) => link.id === selector || link.name === selector) ?? null;
|
|
5013
|
+
if (selected === null || selected === undefined) {
|
|
5014
|
+
if (links.length === 0) {
|
|
5015
|
+
throw new CliError("No active Link found. Create a Version, then create a Link for the Version.");
|
|
5016
|
+
}
|
|
5017
|
+
if (selector === undefined || selector === "") {
|
|
5018
|
+
throw new CliError("This Gem has multiple active Links. Run `uru gems links list`, then use `uru open --link <link-id-or-name>`.");
|
|
5019
|
+
}
|
|
5020
|
+
throw new CliError(`No active Link matches '${selector}'. Run \`uru gems links list\`.`);
|
|
5021
|
+
}
|
|
5022
|
+
if (ctx.json) {
|
|
5023
|
+
writeJson(ctx.io, {
|
|
5024
|
+
linkId: selected.id,
|
|
5025
|
+
name: selected.name,
|
|
5026
|
+
url: selected.url
|
|
5027
|
+
});
|
|
5028
|
+
return;
|
|
5029
|
+
}
|
|
5030
|
+
writeText(ctx.io, selected.url);
|
|
5031
|
+
}
|
|
5032
|
+
function publicLinks(value) {
|
|
5033
|
+
const root = jsonObjectOrNull3(value);
|
|
5034
|
+
const candidates = root === null ? [] : root["links"];
|
|
5035
|
+
if (!Array.isArray(candidates)) {
|
|
5036
|
+
return [];
|
|
5037
|
+
}
|
|
5038
|
+
return candidates.flatMap((candidate) => {
|
|
5039
|
+
const link = jsonObjectOrNull3(candidate);
|
|
5040
|
+
const id = link === null ? undefined : stringField3(link, ["id", "link_id"]);
|
|
5041
|
+
const name = link === null ? undefined : stringField3(link, ["name"]);
|
|
5042
|
+
const url = link === null ? undefined : stringField3(link, ["publish_url", "publishUrl", "url"]);
|
|
5043
|
+
const status = link === null ? undefined : stringField3(link, ["status"]);
|
|
5044
|
+
return id !== undefined && name !== undefined && url !== undefined && status === "active" ? [{ id, name, url }] : [];
|
|
5045
|
+
});
|
|
5046
|
+
}
|
|
5047
|
+
function jsonObjectOrNull3(value) {
|
|
5048
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
5049
|
+
}
|
|
5050
|
+
function stringField3(entry, keys) {
|
|
5051
|
+
for (const key of keys) {
|
|
5052
|
+
const value = entry[key];
|
|
5053
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
5054
|
+
return value.trim();
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
|
|
4499
5060
|
// src/gem-source-commands.ts
|
|
4500
5061
|
import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
4501
5062
|
import { dirname as dirname4, join as join4, resolve as resolve4 } from "node:path";
|
|
4502
5063
|
async function gemSourcePull(ctx, args, linkedProject) {
|
|
4503
5064
|
const flags = parseLocalFlags(args);
|
|
4504
|
-
const gemPath = gemPathFromArgs(flags
|
|
5065
|
+
const gemPath = gemPathFromArgs(flags, linkedProject);
|
|
4505
5066
|
const force = flags.booleans.has("--force");
|
|
4506
5067
|
const previousLock = await readSourceLock(ctx.cwd);
|
|
4507
5068
|
const localFilesBeforePull = await collectLocalSourceFiles(ctx.cwd);
|
|
@@ -4519,20 +5080,24 @@ async function gemSourcePull(ctx, args, linkedProject) {
|
|
|
4519
5080
|
if (filePaths.length === 0) {
|
|
4520
5081
|
throw new CliError(`No source files found for ${gemPath}.`);
|
|
4521
5082
|
}
|
|
4522
|
-
let
|
|
5083
|
+
let sourceRevisionId = findSourceRevisionId(tree);
|
|
4523
5084
|
const files = {};
|
|
5085
|
+
const pulledFiles = [];
|
|
4524
5086
|
for (const filePath of filePaths) {
|
|
4525
5087
|
const readResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_read", {
|
|
4526
5088
|
path: gemFilePath(gemPath, filePath)
|
|
4527
5089
|
}));
|
|
4528
|
-
|
|
5090
|
+
sourceRevisionId = sourceRevisionId ?? findSourceRevisionId(readResult);
|
|
4529
5091
|
const content = extractRemoteFileContent(readResult, filePath);
|
|
4530
|
-
|
|
5092
|
+
pulledFiles.push({ path: filePath, content });
|
|
4531
5093
|
files[filePath] = sha256Hex(content);
|
|
4532
5094
|
}
|
|
5095
|
+
for (const file of pulledFiles) {
|
|
5096
|
+
await writePulledSourceFile(ctx.cwd, file.path, file.content);
|
|
5097
|
+
}
|
|
4533
5098
|
const pruned = await pruneRemoteDeletedSourceFiles(ctx.cwd, previousLock, localFilesBeforePull, new Set(filePaths));
|
|
4534
5099
|
const lock = {
|
|
4535
|
-
...
|
|
5100
|
+
...sourceRevisionId === undefined ? {} : { sourceRevisionId },
|
|
4536
5101
|
files,
|
|
4537
5102
|
checkedOutAt: new Date().toISOString()
|
|
4538
5103
|
};
|
|
@@ -4543,18 +5108,18 @@ async function gemSourcePull(ctx, args, linkedProject) {
|
|
|
4543
5108
|
files: filePaths,
|
|
4544
5109
|
prunedFiles: pruned.deleted,
|
|
4545
5110
|
preservedFiles: pruned.preserved,
|
|
4546
|
-
...
|
|
5111
|
+
...sourceRevisionId === undefined ? {} : { sourceRevisionId }
|
|
4547
5112
|
}, formatPullSummary(gemPath, filePaths.length, pruned));
|
|
4548
5113
|
}
|
|
4549
5114
|
async function gemSourceStatus(ctx, args, linkedProject) {
|
|
4550
5115
|
const flags = parseLocalFlags(args);
|
|
4551
|
-
const gemPath = gemPathFromArgs(flags
|
|
5116
|
+
const gemPath = gemPathFromArgs(flags, linkedProject);
|
|
4552
5117
|
const lock = await readSourceLock(ctx.cwd);
|
|
4553
5118
|
const localFiles = await collectLocalSourceFiles(ctx.cwd);
|
|
4554
5119
|
const localStatus = compareSourceLock(lock, localFiles);
|
|
4555
|
-
const remote = await
|
|
5120
|
+
const remote = await remoteSourceOperation(ctx, "gem.fs_status", {
|
|
4556
5121
|
path: gemPath,
|
|
4557
|
-
...lock.
|
|
5122
|
+
...lock.sourceRevisionId === undefined ? {} : { base: lock.sourceRevisionId }
|
|
4558
5123
|
});
|
|
4559
5124
|
const report = sourceStatusReport(gemPath, lock, localStatus, remote);
|
|
4560
5125
|
if (ctx.outputFormat === "stream-json") {
|
|
@@ -4567,15 +5132,15 @@ async function gemSourceStatus(ctx, args, linkedProject) {
|
|
|
4567
5132
|
}
|
|
4568
5133
|
async function gemSourceDiff(ctx, args, linkedProject) {
|
|
4569
5134
|
const flags = parseLocalFlags(args);
|
|
4570
|
-
const gemPath = gemPathFromArgs(flags
|
|
5135
|
+
const gemPath = gemPathFromArgs(flags, linkedProject);
|
|
4571
5136
|
const lock = await readSourceLock(ctx.cwd);
|
|
4572
5137
|
const localFiles = await collectLocalSourceFiles(ctx.cwd);
|
|
4573
5138
|
const localStatus = compareSourceLock(lock, localFiles);
|
|
4574
5139
|
const mode = flags.values["--mode"] ?? "name_status";
|
|
4575
|
-
const remote = await
|
|
5140
|
+
const remote = await remoteSourceOperation(ctx, "gem.fs_diff", {
|
|
4576
5141
|
path: gemPath,
|
|
4577
5142
|
mode,
|
|
4578
|
-
...lock.
|
|
5143
|
+
...lock.sourceRevisionId === undefined ? {} : { base: lock.sourceRevisionId }
|
|
4579
5144
|
});
|
|
4580
5145
|
const report = sourceStatusReport(gemPath, lock, localStatus, remote);
|
|
4581
5146
|
if (ctx.outputFormat === "stream-json") {
|
|
@@ -4588,7 +5153,7 @@ async function gemSourceDiff(ctx, args, linkedProject) {
|
|
|
4588
5153
|
}
|
|
4589
5154
|
async function gemSourcePush(ctx, args, linkedProject) {
|
|
4590
5155
|
const flags = parseLocalFlags(args);
|
|
4591
|
-
const gemPath = gemPathFromArgs(flags
|
|
5156
|
+
const gemPath = gemPathFromArgs(flags, linkedProject);
|
|
4592
5157
|
const lock = await readSourceLock(ctx.cwd);
|
|
4593
5158
|
const force = flags.booleans.has("--force");
|
|
4594
5159
|
const localFiles = await collectLocalSourceFiles(ctx.cwd);
|
|
@@ -4597,11 +5162,11 @@ async function gemSourcePush(ctx, args, linkedProject) {
|
|
|
4597
5162
|
if (sortedFiles.length === 0 && localStatus.deleted.length === 0) {
|
|
4598
5163
|
throw new CliError("No local Gem source files to push.");
|
|
4599
5164
|
}
|
|
4600
|
-
if (lock.
|
|
4601
|
-
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 });
|
|
4602
5167
|
}
|
|
4603
5168
|
let sourceCas = {
|
|
4604
|
-
...lock.
|
|
5169
|
+
...lock.sourceRevisionId === undefined ? {} : { sourceRevisionId: lock.sourceRevisionId }
|
|
4605
5170
|
};
|
|
4606
5171
|
try {
|
|
4607
5172
|
const syncResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_sync", {
|
|
@@ -4615,14 +5180,17 @@ async function gemSourcePush(ctx, args, linkedProject) {
|
|
|
4615
5180
|
}));
|
|
4616
5181
|
sourceCas = nextSourceMutationCas(sourceCas, syncResult);
|
|
4617
5182
|
} catch (error) {
|
|
5183
|
+
if (error instanceof CliError && error.exitCode === ExitCode.Conflict) {
|
|
5184
|
+
throw error;
|
|
5185
|
+
}
|
|
4618
5186
|
if (isConflictLikeError(error)) {
|
|
4619
5187
|
throw new CliError(error instanceof Error ? error.message : "Gem source conflict", { code: ExitCode.Conflict, cause: error });
|
|
4620
5188
|
}
|
|
4621
5189
|
throw error;
|
|
4622
5190
|
}
|
|
4623
|
-
const
|
|
5191
|
+
const nextSourceRevisionId = sourceCas.sourceRevisionId;
|
|
4624
5192
|
await writeSourceLock(ctx.cwd, {
|
|
4625
|
-
...
|
|
5193
|
+
...nextSourceRevisionId === undefined ? {} : { sourceRevisionId: nextSourceRevisionId },
|
|
4626
5194
|
files: sourceHashesFromLocalFiles(localFiles),
|
|
4627
5195
|
checkedOutAt: new Date().toISOString()
|
|
4628
5196
|
});
|
|
@@ -4631,7 +5199,7 @@ async function gemSourcePush(ctx, args, linkedProject) {
|
|
|
4631
5199
|
path: gemPath,
|
|
4632
5200
|
files: sortedFiles.map((file) => file.path),
|
|
4633
5201
|
deletedFiles: [...localStatus.deleted],
|
|
4634
|
-
...
|
|
5202
|
+
...nextSourceRevisionId === undefined ? {} : { sourceRevisionId: nextSourceRevisionId }
|
|
4635
5203
|
}, formatPushSummary(gemPath, sortedFiles.length, localStatus.deleted.length));
|
|
4636
5204
|
}
|
|
4637
5205
|
function localFileBatchWritePayload(file) {
|
|
@@ -4679,17 +5247,17 @@ function formatPullSummary(gemPath, writtenCount, pruned) {
|
|
|
4679
5247
|
return parts.join("; ");
|
|
4680
5248
|
}
|
|
4681
5249
|
function sourceMutationCasParams(cas, force) {
|
|
4682
|
-
if (force || cas.
|
|
5250
|
+
if (force || cas.sourceRevisionId === undefined) {
|
|
4683
5251
|
return { force: true };
|
|
4684
5252
|
}
|
|
4685
5253
|
return {
|
|
4686
|
-
|
|
5254
|
+
base_source_revision_id: cas.sourceRevisionId
|
|
4687
5255
|
};
|
|
4688
5256
|
}
|
|
4689
5257
|
function nextSourceMutationCas(current, result) {
|
|
4690
|
-
const
|
|
5258
|
+
const sourceRevisionId = findSourceRevisionId(result) ?? current.sourceRevisionId;
|
|
4691
5259
|
return {
|
|
4692
|
-
...
|
|
5260
|
+
...sourceRevisionId === undefined ? {} : { sourceRevisionId }
|
|
4693
5261
|
};
|
|
4694
5262
|
}
|
|
4695
5263
|
function formatPushSummary(gemPath, writtenCount, deletedCount) {
|
|
@@ -4705,7 +5273,7 @@ function formatPushSummary(gemPath, writtenCount, deletedCount) {
|
|
|
4705
5273
|
function sourceStatusReport(path, lock, local, remote) {
|
|
4706
5274
|
return {
|
|
4707
5275
|
path,
|
|
4708
|
-
|
|
5276
|
+
sourceRevisionId: lock.sourceRevisionId ?? null,
|
|
4709
5277
|
local,
|
|
4710
5278
|
remote
|
|
4711
5279
|
};
|
|
@@ -4713,7 +5281,7 @@ function sourceStatusReport(path, lock, local, remote) {
|
|
|
4713
5281
|
function sourceStatusReportJson(report) {
|
|
4714
5282
|
return {
|
|
4715
5283
|
path: report.path,
|
|
4716
|
-
|
|
5284
|
+
sourceRevisionId: report.sourceRevisionId,
|
|
4717
5285
|
local: localSourceStatusJson(report.local),
|
|
4718
5286
|
remote: remoteSourceOperationResultJson(report.remote)
|
|
4719
5287
|
};
|
|
@@ -4741,7 +5309,7 @@ function remoteSourceOperationResultJson(remote) {
|
|
|
4741
5309
|
function formatSourceStatus(report) {
|
|
4742
5310
|
const lines = [
|
|
4743
5311
|
`Gem: ${report.path}`,
|
|
4744
|
-
`Source version: ${report.
|
|
5312
|
+
`Source version: ${report.sourceRevisionId ?? "unknown"}`,
|
|
4745
5313
|
`Local source: ${report.local.clean ? "clean" : "dirty"}`,
|
|
4746
5314
|
...formatLocalStatusEntries(report.local),
|
|
4747
5315
|
`Remote source: ${formatRemoteSummary(report.remote)}`
|
|
@@ -4753,7 +5321,7 @@ function formatSourceDiff(report) {
|
|
|
4753
5321
|
const localLines = formatLocalStatusEntries(report.local);
|
|
4754
5322
|
const lines = [
|
|
4755
5323
|
`Gem: ${report.path}`,
|
|
4756
|
-
`Source version: ${report.
|
|
5324
|
+
`Source version: ${report.sourceRevisionId ?? "unknown"}`,
|
|
4757
5325
|
"Local changes since .uru/source-lock.json:",
|
|
4758
5326
|
...localLines.length === 0 ? [" clean"] : localLines.map((line) => ` ${line}`),
|
|
4759
5327
|
"Remote diff:",
|
|
@@ -4780,18 +5348,11 @@ function dirtyPullErrorMessage(status) {
|
|
|
4780
5348
|
Run \`uru status\` to inspect them, save or push your changes, or re-run \`uru pull --force\` to replace local files with server source.
|
|
4781
5349
|
${preview}${moreLine}`;
|
|
4782
5350
|
}
|
|
4783
|
-
async function
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
};
|
|
4789
|
-
} catch (error) {
|
|
4790
|
-
return {
|
|
4791
|
-
available: false,
|
|
4792
|
-
error: error instanceof Error ? error.message : "Remote operation failed"
|
|
4793
|
-
};
|
|
4794
|
-
}
|
|
5351
|
+
async function remoteSourceOperation(ctx, operationId, params) {
|
|
5352
|
+
return {
|
|
5353
|
+
available: true,
|
|
5354
|
+
result: unwrapToolResult(await executeOperation(ctx, operationId, params))
|
|
5355
|
+
};
|
|
4795
5356
|
}
|
|
4796
5357
|
function formatRemoteSummary(remote) {
|
|
4797
5358
|
if (!remote.available) {
|
|
@@ -4897,17 +5458,17 @@ function safeProjectPath(path) {
|
|
|
4897
5458
|
}
|
|
4898
5459
|
return parts.join("/");
|
|
4899
5460
|
}
|
|
4900
|
-
function
|
|
5461
|
+
function findSourceRevisionId(value) {
|
|
4901
5462
|
let found;
|
|
4902
5463
|
visitJson(value, (candidate) => {
|
|
4903
5464
|
if (!isJsonObject2(candidate)) {
|
|
4904
5465
|
return;
|
|
4905
5466
|
}
|
|
4906
5467
|
const valueAtKey = stringField(candidate, [
|
|
4907
|
-
"
|
|
4908
|
-
"
|
|
4909
|
-
"
|
|
4910
|
-
"
|
|
5468
|
+
"currentSourceRevisionId",
|
|
5469
|
+
"current_source_revision_id",
|
|
5470
|
+
"sourceRevisionId",
|
|
5471
|
+
"source_revision_id"
|
|
4911
5472
|
]);
|
|
4912
5473
|
if (valueAtKey !== undefined) {
|
|
4913
5474
|
found = valueAtKey;
|
|
@@ -4932,11 +5493,12 @@ function visitJson(value, visitor) {
|
|
|
4932
5493
|
}
|
|
4933
5494
|
function isConflictLikeError(error) {
|
|
4934
5495
|
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
4935
|
-
return message.includes("409") || message.includes("conflict") || message.includes("stale") || message.includes("
|
|
5496
|
+
return message.includes("409") || message.includes("conflict") || message.includes("stale") || message.includes("base_source_revision") || message.includes("current_source_revision") || message.includes("version mismatch");
|
|
4936
5497
|
}
|
|
4937
5498
|
|
|
4938
5499
|
// src/token-env-secret-commands.ts
|
|
4939
5500
|
import { spawn as spawn2 } from "node:child_process";
|
|
5501
|
+
import { StringDecoder } from "node:string_decoder";
|
|
4940
5502
|
import { chmod, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
|
|
4941
5503
|
import { dirname as dirname5, resolve as resolve5 } from "node:path";
|
|
4942
5504
|
async function dispatchTokens(ctx, sub, args, linkedProject) {
|
|
@@ -5188,48 +5750,69 @@ function unquoteEnvValue(value) {
|
|
|
5188
5750
|
}
|
|
5189
5751
|
async function runEnvCommand(ctx, command, envVars) {
|
|
5190
5752
|
const executable = requireCommandValue(command[0], "env run command is required");
|
|
5191
|
-
const
|
|
5753
|
+
const redactions = Object.values(envVars).filter((value) => value.length > 0);
|
|
5754
|
+
const code = await spawnStreaming(executable, command.slice(1), {
|
|
5192
5755
|
cwd: ctx.cwd,
|
|
5193
|
-
env: { ...process.env, ...envVars }
|
|
5756
|
+
env: { ...process.env, ...envVars },
|
|
5757
|
+
redactions,
|
|
5758
|
+
stdout: ctx.io.stdout,
|
|
5759
|
+
stderr: ctx.io.stderr
|
|
5194
5760
|
});
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
const stderr = redactValues2(result.stderr, redactions);
|
|
5198
|
-
if (stdout.length > 0) {
|
|
5199
|
-
ctx.io.stdout.write(stdout);
|
|
5200
|
-
}
|
|
5201
|
-
if (stderr.length > 0) {
|
|
5202
|
-
ctx.io.stderr.write(stderr);
|
|
5203
|
-
}
|
|
5204
|
-
if (result.code !== 0) {
|
|
5205
|
-
throw new CliError(`env run failed with exit code ${result.code}`);
|
|
5761
|
+
if (code !== 0) {
|
|
5762
|
+
throw new CliError(`env run failed with exit code ${code}`);
|
|
5206
5763
|
}
|
|
5207
5764
|
}
|
|
5208
|
-
async function
|
|
5765
|
+
async function spawnStreaming(command, args, options) {
|
|
5209
5766
|
return new Promise((resolvePromise, reject) => {
|
|
5210
5767
|
const child = spawn2(command, [...args], {
|
|
5211
5768
|
cwd: options.cwd,
|
|
5212
5769
|
env: options.env,
|
|
5213
|
-
stdio: ["
|
|
5770
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
5214
5771
|
});
|
|
5215
|
-
const stdout =
|
|
5216
|
-
const stderr =
|
|
5772
|
+
const stdout = redactingLineWriter(options.stdout, options.redactions);
|
|
5773
|
+
const stderr = redactingLineWriter(options.stderr, options.redactions);
|
|
5217
5774
|
child.stdout?.on("data", (chunk) => {
|
|
5218
|
-
stdout.
|
|
5775
|
+
stdout.write(chunk);
|
|
5219
5776
|
});
|
|
5220
5777
|
child.stderr?.on("data", (chunk) => {
|
|
5221
|
-
stderr.
|
|
5778
|
+
stderr.write(chunk);
|
|
5222
5779
|
});
|
|
5223
5780
|
child.on("error", reject);
|
|
5224
5781
|
child.on("close", (code) => {
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
stderr: stderr.join("")
|
|
5229
|
-
});
|
|
5782
|
+
stdout.end();
|
|
5783
|
+
stderr.end();
|
|
5784
|
+
resolvePromise(code ?? 1);
|
|
5230
5785
|
});
|
|
5231
5786
|
});
|
|
5232
5787
|
}
|
|
5788
|
+
function redactingLineWriter(destination, redactions) {
|
|
5789
|
+
const decoder = new StringDecoder("utf8");
|
|
5790
|
+
let buffered = "";
|
|
5791
|
+
const flushCompleteLines = () => {
|
|
5792
|
+
let newline = buffered.indexOf(`
|
|
5793
|
+
`);
|
|
5794
|
+
while (newline !== -1) {
|
|
5795
|
+
destination.write(redactValues2(buffered.slice(0, newline + 1), redactions));
|
|
5796
|
+
buffered = buffered.slice(newline + 1);
|
|
5797
|
+
newline = buffered.indexOf(`
|
|
5798
|
+
`);
|
|
5799
|
+
}
|
|
5800
|
+
};
|
|
5801
|
+
return {
|
|
5802
|
+
write: (chunk) => {
|
|
5803
|
+
buffered += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5804
|
+
flushCompleteLines();
|
|
5805
|
+
},
|
|
5806
|
+
end: () => {
|
|
5807
|
+
buffered += decoder.end();
|
|
5808
|
+
flushCompleteLines();
|
|
5809
|
+
if (buffered.length > 0) {
|
|
5810
|
+
destination.write(redactValues2(buffered, redactions));
|
|
5811
|
+
buffered = "";
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5233
5816
|
function redactValues2(content, values) {
|
|
5234
5817
|
let redacted = content;
|
|
5235
5818
|
const ordered = [...new Set(values)].filter((value) => value.length > 0).sort((left, right) => right.length - left.length);
|
|
@@ -5376,11 +5959,21 @@ async function runCli(options) {
|
|
|
5376
5959
|
const envGemId = nonEmptyString(options.env?.["URU_GEM_ID"]);
|
|
5377
5960
|
const explicitGemRef = parsed.flags.gem ?? envGemId;
|
|
5378
5961
|
const apiUrl = parsed.flags.apiUrl ?? envApiUrl ?? config.apiUrl ?? DEFAULT_API_URL;
|
|
5379
|
-
const
|
|
5380
|
-
const cwd = parsed.flags.cwd ?? options.env?.["PWD"] ?? process.cwd();
|
|
5962
|
+
const cwd = parsed.flags.cwd ?? process.cwd();
|
|
5381
5963
|
const linkedProjectFromDisk = await readLinkedProject(cwd);
|
|
5382
5964
|
const linkedProject = explicitGemRef === undefined ? linkedProjectFromDisk : linkedProjectWithExplicitGemRef(linkedProjectFromDisk, explicitGemRef);
|
|
5383
5965
|
const workspace = parsed.flags.workspace ?? envWorkspace ?? linkedProject.workspaceId ?? config.workspace;
|
|
5966
|
+
const storedToken = flagToken === undefined && envToken === undefined && shouldResolveStoredToken(parsed.command) ? await resolveStoredToken({
|
|
5967
|
+
credentialStore,
|
|
5968
|
+
store,
|
|
5969
|
+
apiUrl,
|
|
5970
|
+
config,
|
|
5971
|
+
...workspace === undefined ? {} : { workspaceId: workspace },
|
|
5972
|
+
...options.fetch === undefined ? {} : { fetch: options.fetch },
|
|
5973
|
+
...parsed.command[0] === "login" ? { recoverInvalidCredential: true } : {}
|
|
5974
|
+
}) : undefined;
|
|
5975
|
+
const token = flagToken ?? envToken ?? storedToken;
|
|
5976
|
+
const runtimeConfig = await store.read();
|
|
5384
5977
|
const outputFormat = parsed.flags.outputFormat ?? (parsed.flags.json ? "json" : "text");
|
|
5385
5978
|
const callbackHost = parsed.flags.callbackHost ?? "127.0.0.1";
|
|
5386
5979
|
const callbackPort = parseCallbackPort(parsed.flags.callbackPort);
|
|
@@ -5394,7 +5987,7 @@ async function runCli(options) {
|
|
|
5394
5987
|
const ctx = {
|
|
5395
5988
|
io,
|
|
5396
5989
|
store,
|
|
5397
|
-
config,
|
|
5990
|
+
config: runtimeConfig,
|
|
5398
5991
|
credentialStore,
|
|
5399
5992
|
apiUrl,
|
|
5400
5993
|
...token !== undefined ? { token } : {},
|
|
@@ -5500,11 +6093,18 @@ async function dispatchWithImplicitLogin(ctx, command, linkedProject) {
|
|
|
5500
6093
|
}
|
|
5501
6094
|
await login(ctx, { silentSuccess: true });
|
|
5502
6095
|
const nextConfig = await ctx.store.read();
|
|
5503
|
-
const
|
|
6096
|
+
const selectedWorkspace = ctx.workspace ?? nextConfig.workspace;
|
|
6097
|
+
const nextToken = await resolveStoredToken({
|
|
6098
|
+
credentialStore: ctx.credentialStore,
|
|
6099
|
+
store: ctx.store,
|
|
6100
|
+
apiUrl: ctx.apiUrl,
|
|
6101
|
+
config: nextConfig,
|
|
6102
|
+
...selectedWorkspace === undefined ? {} : { workspaceId: selectedWorkspace },
|
|
6103
|
+
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
6104
|
+
});
|
|
5504
6105
|
if (nextToken === undefined) {
|
|
5505
6106
|
throw new CliError("Login completed but no CLI session token was stored. Re-run `uru login` or pass --token.", { code: ExitCode.AuthRequired });
|
|
5506
6107
|
}
|
|
5507
|
-
const selectedWorkspace = ctx.workspace ?? nextConfig.workspace;
|
|
5508
6108
|
const nextCtx = {
|
|
5509
6109
|
...ctx,
|
|
5510
6110
|
config: nextConfig,
|
|
@@ -5514,7 +6114,14 @@ async function dispatchWithImplicitLogin(ctx, command, linkedProject) {
|
|
|
5514
6114
|
await dispatch(nextCtx, command, linkedProject);
|
|
5515
6115
|
}
|
|
5516
6116
|
function shouldStartImplicitLogin(ctx, error) {
|
|
5517
|
-
return error.exitCode === ExitCode.AuthRequired && ctx.
|
|
6117
|
+
return error.exitCode === ExitCode.AuthRequired && !ctx.tokenIsExplicit && !ctx.noInput && !ctx.terminal.ci && ctx.terminal.stdinIsTty && ctx.terminal.stderrIsTty;
|
|
6118
|
+
}
|
|
6119
|
+
function shouldResolveStoredToken(command) {
|
|
6120
|
+
const [head, sub] = command;
|
|
6121
|
+
if (head === "operations" && (sub === "ls" || sub === "list")) {
|
|
6122
|
+
return false;
|
|
6123
|
+
}
|
|
6124
|
+
return !["logout", "link", "unlink", "docs", "help", "complete", "update"].includes(head ?? "");
|
|
5518
6125
|
}
|
|
5519
6126
|
function terminalStdin(stdin) {
|
|
5520
6127
|
return stdin === undefined ? process.stdin : stdin;
|
|
@@ -5526,8 +6133,26 @@ async function dispatch(ctx, command, linkedProject) {
|
|
|
5526
6133
|
return;
|
|
5527
6134
|
}
|
|
5528
6135
|
if (head === "logout") {
|
|
5529
|
-
|
|
5530
|
-
|
|
6136
|
+
let revocationFailure;
|
|
6137
|
+
try {
|
|
6138
|
+
await revokeStoredOAuthLogin({
|
|
6139
|
+
credentialStore: ctx.credentialStore,
|
|
6140
|
+
apiUrl: ctx.apiUrl,
|
|
6141
|
+
config: ctx.config,
|
|
6142
|
+
...ctx.fetch === undefined ? {} : { fetch: ctx.fetch }
|
|
6143
|
+
});
|
|
6144
|
+
} catch (error) {
|
|
6145
|
+
revocationFailure = error;
|
|
6146
|
+
} finally {
|
|
6147
|
+
await ctx.credentialStore.clearToken(ctx.apiUrl);
|
|
6148
|
+
await ctx.store.clear();
|
|
6149
|
+
}
|
|
6150
|
+
if (revocationFailure !== undefined) {
|
|
6151
|
+
throw new CliError("Logged out locally, but server-side credential revocation could not be confirmed.", {
|
|
6152
|
+
errorCode: "oauth_logout_revocation_failed",
|
|
6153
|
+
cause: revocationFailure
|
|
6154
|
+
});
|
|
6155
|
+
}
|
|
5531
6156
|
emit(ctx, { ok: true }, "Logged out");
|
|
5532
6157
|
return;
|
|
5533
6158
|
}
|
|
@@ -5596,16 +6221,6 @@ async function dispatch(ctx, command, linkedProject) {
|
|
|
5596
6221
|
await gemBuild(ctx, command.slice(1), linkedProject);
|
|
5597
6222
|
return;
|
|
5598
6223
|
}
|
|
5599
|
-
if (head === "deploy") {
|
|
5600
|
-
await withWorkspace(ctx);
|
|
5601
|
-
await gemDeploy(ctx, command.slice(1), linkedProject);
|
|
5602
|
-
return;
|
|
5603
|
-
}
|
|
5604
|
-
if (head === "promote") {
|
|
5605
|
-
await withWorkspace(ctx);
|
|
5606
|
-
await gemPromote(ctx, command.slice(1), linkedProject);
|
|
5607
|
-
return;
|
|
5608
|
-
}
|
|
5609
6224
|
if (head === "pull") {
|
|
5610
6225
|
await withWorkspace(ctx);
|
|
5611
6226
|
await gemSourcePull(ctx, command.slice(1), linkedProject);
|
|
@@ -5636,11 +6251,6 @@ async function dispatch(ctx, command, linkedProject) {
|
|
|
5636
6251
|
await gemOpen(ctx, command.slice(1), linkedProject);
|
|
5637
6252
|
return;
|
|
5638
6253
|
}
|
|
5639
|
-
if (head === "rollback") {
|
|
5640
|
-
await withWorkspace(ctx);
|
|
5641
|
-
await gemRollback(ctx, command.slice(1), linkedProject);
|
|
5642
|
-
return;
|
|
5643
|
-
}
|
|
5644
6254
|
if (head === "gems") {
|
|
5645
6255
|
await withWorkspace(ctx);
|
|
5646
6256
|
await dispatchGems(ctx, sub, command.slice(2), linkedProject);
|