@uru-intelligence/cli 0.3.53 → 0.3.54
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 +27 -5
- package/dist/uru.mjs +734 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,12 @@ bun src/index.ts whoami --json
|
|
|
13
13
|
bun src/index.ts switch <workspace-id>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The CLI is registry-backed. Dedicated Gem/Library/Dataset
|
|
16
|
+
The CLI is registry-backed. Dedicated Gem/Library/Dataset/Automation commands
|
|
17
|
+
are thin adapters over the same platform tools used by MCP and the web UI;
|
|
18
|
+
broader workspace, prompt/persona/context, sharing, web/file, business SQL,
|
|
19
|
+
secret/env/token operations remain exposed through the generated operation
|
|
20
|
+
registry while dedicated UX commands continue to mature. The CLI does **not**
|
|
21
|
+
reimplement Gem lifecycle sequencing client-side.
|
|
17
22
|
|
|
18
23
|
## Current commands
|
|
19
24
|
|
|
@@ -49,11 +54,21 @@ bun src/index.ts gems write 'library/Golden Gems/My Gem.gem' main.html --file ./
|
|
|
49
54
|
bun src/index.ts library ls library
|
|
50
55
|
bun src/index.ts library search revenue
|
|
51
56
|
bun src/index.ts library mkdir 'library/Golden Gems'
|
|
52
|
-
bun src/index.ts datasets query revenue_rows --limit 50 --json
|
|
57
|
+
bun src/index.ts datasets query revenue_rows --limit 50 --response-format concise --json
|
|
53
58
|
bun src/index.ts datasets get --params-json '{"dataset_id":"revenue_rows"}'
|
|
54
59
|
bun src/index.ts datasets rows-upsert --params-json '{"dataset_id":"revenue_rows","rows":[{"name":"Acme"}]}'
|
|
55
60
|
bun src/index.ts datasets manage-create --params-json '{"title":"Revenue Rows","data_schema":{"properties":[]}}'
|
|
56
61
|
|
|
62
|
+
# Automation helpers over the platform tool surface.
|
|
63
|
+
bun src/index.ts automations ls --status active --view summary
|
|
64
|
+
bun src/index.ts automations get <automation-id>
|
|
65
|
+
bun src/index.ts automations create "Daily revenue digest" --status draft
|
|
66
|
+
bun src/index.ts automations enable <automation-id>
|
|
67
|
+
bun src/index.ts automations trigger <automation-id> --input-message "Run now"
|
|
68
|
+
bun src/index.ts automations runs ls <automation-id> --scope workspace --limit 20
|
|
69
|
+
bun src/index.ts automations runs tail <run-id> --level warn
|
|
70
|
+
bun src/index.ts automations runs cancel <run-id> --yes
|
|
71
|
+
|
|
57
72
|
# Raw platform-tool escape hatch for diagnostics and parity.
|
|
58
73
|
bun src/index.ts tools ls
|
|
59
74
|
bun src/index.ts tools schema gem_control_plane --json
|
|
@@ -81,8 +96,9 @@ Gem version commands create immutable, build-backed versions through
|
|
|
81
96
|
`gem_control_plane` `op=version_upload`, then deploy those version ids through
|
|
82
97
|
`op=version_deploy`. Provider-internal flags such as `--runtime-provider` are
|
|
83
98
|
not accepted: runtime routing is compiled server-side from the Gem contract.
|
|
84
|
-
|
|
85
|
-
|
|
99
|
+
Non-100 rollout percentages are accepted only with `--skip-promote` to stage
|
|
100
|
+
rollout intent; live promote/rollback remains fail-closed until weighted link
|
|
101
|
+
routing is enabled.
|
|
86
102
|
|
|
87
103
|
The scaffold defaults to the production API (`https://api.uruintelligence.com`).
|
|
88
104
|
Use `--api-url`, `URU_API_BASE`, or `URU_API_URL` to point it at staging or a local backend:
|
|
@@ -110,6 +126,8 @@ Headless environments can use device login or explicit token/API-key paths:
|
|
|
110
126
|
- linked project state: `.uru/project.json` with `{workspaceId, gemId, libraryPath}`
|
|
111
127
|
- no credentials are written to `.uru/project.json`
|
|
112
128
|
- Gem-scoped deploy tokens for CI use `uru tokens create|ls|revoke`
|
|
129
|
+
- `uru tokens create` requires `--name` plus exactly one lifetime flag:
|
|
130
|
+
`--expiry <duration>` or `--expires-at <iso timestamp>`
|
|
113
131
|
- created tokens default to the server-enforced `gem:deploy` scope
|
|
114
132
|
- custom `--scope`/`--scopes` values must include `gem:deploy` before the token
|
|
115
133
|
can operate Gem deploy/query/control routes
|
|
@@ -158,4 +176,8 @@ bun --filter=@uru-intelligence/cli run smoke:binaries # dry-run matrix
|
|
|
158
176
|
```
|
|
159
177
|
|
|
160
178
|
The updater command is intentionally conservative: `uru update` prints the exact
|
|
161
|
-
`npm install -g @uru-intelligence/cli@latest` command instead of self-mutating
|
|
179
|
+
`npm install -g @uru-intelligence/cli@latest` command instead of self-mutating
|
|
180
|
+
the installation. Interactive text-mode commands also perform a bounded
|
|
181
|
+
once-per-24h npm version check and write a single stderr notice when a newer CLI
|
|
182
|
+
is available. The notifier is disabled for JSON/streaming output, CI,
|
|
183
|
+
`NO_UPDATE_NOTIFIER`, and `URU_NO_UPDATE_NOTIFIER`.
|
package/dist/uru.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
6
6
|
|
|
7
7
|
// src/types.ts
|
|
8
8
|
var DEFAULT_API_URL = "https://api.uruintelligence.com";
|
|
9
|
-
var CLI_VERSION = "0.3.
|
|
9
|
+
var CLI_VERSION = "0.3.54";
|
|
10
10
|
var ExitCode = {
|
|
11
11
|
Ok: 0,
|
|
12
12
|
Failure: 1,
|
|
@@ -202,6 +202,7 @@ function buildOperationGroups(op) {
|
|
|
202
202
|
op("gem.fs_read", "gems", "Read a file from a Gem project directory.", "gem_query", "fs_read", "read"),
|
|
203
203
|
op("gem.fs_write", "gems", "Write a file inside a Gem project directory.", "gem_control_plane", "fs_write", "write"),
|
|
204
204
|
op("gem.fs_batch_write", "gems", "Write multiple Gem files atomically through the server source layer.", "gem_control_plane", "fs_batch_write", "write"),
|
|
205
|
+
op("gem.fs_sync", "gems", "Synchronize Gem files and deletions atomically through the server source layer.", "gem_control_plane", "fs_sync", "write"),
|
|
205
206
|
op("gem.fs_patch", "gems", "Apply a patch to Gem source files.", "gem_control_plane", "fs_patch", "write"),
|
|
206
207
|
op("gem.fs_mkdir", "gems", "Create a directory inside a Gem project.", "gem_control_plane", "fs_mkdir", "write"),
|
|
207
208
|
op("gem.fs_rm", "gems", "Remove a file or directory from a Gem project.", "gem_control_plane", "fs_rm", "destructive"),
|
|
@@ -257,11 +258,12 @@ function buildOperationGroups(op) {
|
|
|
257
258
|
const sharingOperations = [
|
|
258
259
|
op("sharing.share", "sharing", "Share a Library resource with a user, team, or workspace.", "library_collab", "share", "write"),
|
|
259
260
|
op("sharing.unshare", "sharing", "Remove a Library resource grant.", "library_collab", "unshare", "destructive"),
|
|
260
|
-
op("sharing.permissions_list", "sharing", "List permissions for a Library resource.", "
|
|
261
|
+
op("sharing.permissions_list", "sharing", "List permissions for a Library resource.", "sharing_query", "list_permissions", "read"),
|
|
262
|
+
op("sharing.shares_list", "sharing", "List active share grants for a Library resource.", "sharing_query", "list_shares", "read"),
|
|
261
263
|
op("sharing.permissions_set", "sharing", "Set permissions for a Library resource.", "library_collab", "set_permissions", "write"),
|
|
262
264
|
op("sharing.settings_set", "sharing", "Set share settings for a Library resource.", "library_collab", "set_share_settings", "write"),
|
|
263
265
|
op("sharing.transfer_ownership", "sharing", "Transfer ownership for a Library resource.", "library_collab", "transfer_ownership", "destructive"),
|
|
264
|
-
op("sharing.versions_list", "sharing", "List Library resource versions.", "
|
|
266
|
+
op("sharing.versions_list", "sharing", "List Library resource versions.", "sharing_query", "list_versions", "read"),
|
|
265
267
|
op("sharing.version_restore", "sharing", "Restore a Library resource version.", "library_collab", "restore_version", "destructive"),
|
|
266
268
|
op("sharing.comment", "sharing", "Comment on a Library resource.", "library_collab", "comment", "write"),
|
|
267
269
|
op("sharing.comment_resolve", "sharing", "Resolve a Library resource comment.", "library_collab", "resolve_comment", "write"),
|
|
@@ -475,6 +477,10 @@ var internalOnlyPlatformToolIds = new Set([
|
|
|
475
477
|
"TYLER_SQL",
|
|
476
478
|
"UPSERT_DATASET_ROWS"
|
|
477
479
|
]);
|
|
480
|
+
// ../../packages/platform-operations/src/external_company_mcp_policy.ts
|
|
481
|
+
var externalCompanyMcpDeniedToolIds = new Set([
|
|
482
|
+
"MAGUIRE_WRITE_SQL"
|
|
483
|
+
]);
|
|
478
484
|
// ../../packages/platform-operations/src/index.ts
|
|
479
485
|
var ALL_SURFACES = [
|
|
480
486
|
"cli",
|
|
@@ -621,6 +627,12 @@ class UruApiClient {
|
|
|
621
627
|
body: { parameters }
|
|
622
628
|
});
|
|
623
629
|
}
|
|
630
|
+
async streamTool(name, parameters, onEvent) {
|
|
631
|
+
return this.requestStream(`/api/platform/tools/${encodeURIComponent(name)}/execute`, {
|
|
632
|
+
method: "POST",
|
|
633
|
+
body: { parameters }
|
|
634
|
+
}, onEvent);
|
|
635
|
+
}
|
|
624
636
|
async api(path, options) {
|
|
625
637
|
return this.request(path, options);
|
|
626
638
|
}
|
|
@@ -644,6 +656,30 @@ class UruApiClient {
|
|
|
644
656
|
}
|
|
645
657
|
return payload;
|
|
646
658
|
}
|
|
659
|
+
async requestStream(path, options, onEvent) {
|
|
660
|
+
const init = {
|
|
661
|
+
method: options.method,
|
|
662
|
+
...this.signal === undefined ? {} : { signal: this.signal },
|
|
663
|
+
headers: {
|
|
664
|
+
Authorization: `Bearer ${this.token}`,
|
|
665
|
+
Accept: "application/x-ndjson, application/json",
|
|
666
|
+
...this.workspaceId === undefined ? {} : { "X-Uru-Workspace-Id": this.workspaceId },
|
|
667
|
+
...options.body === undefined ? {} : { "Content-Type": "application/json" }
|
|
668
|
+
},
|
|
669
|
+
...options.body === undefined ? {} : { body: JSON.stringify(options.body) }
|
|
670
|
+
};
|
|
671
|
+
const response = await this.fetchImpl(`${this.apiUrl}${path}`, init);
|
|
672
|
+
if (response.ok && response.body !== null && isNdjsonResponse(response.headers.get("content-type"))) {
|
|
673
|
+
await readNdjsonStream(response.body, onEvent);
|
|
674
|
+
return { streamed: true };
|
|
675
|
+
}
|
|
676
|
+
const text = await response.text();
|
|
677
|
+
const payload = parseResponseBody(text);
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
throw apiError(response.status, payload, options.body);
|
|
680
|
+
}
|
|
681
|
+
return { streamed: false, payload };
|
|
682
|
+
}
|
|
647
683
|
}
|
|
648
684
|
function normalizeApiUrl(value) {
|
|
649
685
|
const trimmed = value.trim();
|
|
@@ -659,6 +695,52 @@ function parseResponseBody(text) {
|
|
|
659
695
|
}
|
|
660
696
|
return parsed;
|
|
661
697
|
}
|
|
698
|
+
function isNdjsonResponse(contentType) {
|
|
699
|
+
const normalized = String(contentType ?? "").toLowerCase();
|
|
700
|
+
return normalized.includes("application/x-ndjson") || normalized.includes("application/ndjson") || normalized.includes("text/x-ndjson");
|
|
701
|
+
}
|
|
702
|
+
async function readNdjsonStream(body, onEvent) {
|
|
703
|
+
const reader = body.getReader();
|
|
704
|
+
const decoder = new TextDecoder;
|
|
705
|
+
let buffered = "";
|
|
706
|
+
try {
|
|
707
|
+
while (true) {
|
|
708
|
+
const { done, value } = await reader.read();
|
|
709
|
+
if (done) {
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
buffered += decoder.decode(value, { stream: true });
|
|
713
|
+
buffered = emitCompleteNdjsonLines(buffered, onEvent);
|
|
714
|
+
}
|
|
715
|
+
buffered += decoder.decode();
|
|
716
|
+
emitNdjsonLine(buffered, onEvent);
|
|
717
|
+
} finally {
|
|
718
|
+
reader.releaseLock();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function emitCompleteNdjsonLines(buffered, onEvent) {
|
|
722
|
+
let start = 0;
|
|
723
|
+
while (true) {
|
|
724
|
+
const newline = buffered.indexOf(`
|
|
725
|
+
`, start);
|
|
726
|
+
if (newline === -1) {
|
|
727
|
+
return buffered.slice(start);
|
|
728
|
+
}
|
|
729
|
+
emitNdjsonLine(buffered.slice(start, newline), onEvent);
|
|
730
|
+
start = newline + 1;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function emitNdjsonLine(line, onEvent) {
|
|
734
|
+
const trimmed = line.trim();
|
|
735
|
+
if (trimmed === "") {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const parsed = JSON.parse(trimmed);
|
|
739
|
+
if (!isJsonObject(parsed)) {
|
|
740
|
+
throw new CliError("API returned a non-object stream event");
|
|
741
|
+
}
|
|
742
|
+
onEvent(parsed);
|
|
743
|
+
}
|
|
662
744
|
function apiError(status, payload, requestBody) {
|
|
663
745
|
const redactions = collectSensitiveValues(requestBody);
|
|
664
746
|
const summary = errorMessage(payload) ?? `Uru API request failed with HTTP ${status}`;
|
|
@@ -1005,6 +1087,15 @@ function writeStreamJson(io, event, value) {
|
|
|
1005
1087
|
io.stdout.write(`${JSON.stringify({ event, ...payload })}
|
|
1006
1088
|
`);
|
|
1007
1089
|
}
|
|
1090
|
+
function writeStreamJsonEvent(io, value) {
|
|
1091
|
+
const payload = objectPayload(value);
|
|
1092
|
+
if (typeof payload["event"] !== "string" || payload["event"].trim() === "") {
|
|
1093
|
+
writeStreamJson(io, "event", payload);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
io.stdout.write(`${JSON.stringify(payload)}
|
|
1097
|
+
`);
|
|
1098
|
+
}
|
|
1008
1099
|
function writeText(io, value) {
|
|
1009
1100
|
io.stdout.write(value.endsWith(`
|
|
1010
1101
|
`) ? value : `${value}
|
|
@@ -1075,13 +1166,13 @@ GEMS
|
|
|
1075
1166
|
uru build [library/name.gem] --dry-run --outdir Build/validate and export build manifest
|
|
1076
1167
|
uru deploy [library/name.gem] [--prod] [--dry-run] Server-side build/deploy/publish
|
|
1077
1168
|
uru deploy [library/name.gem] --skip-promote Deploy without promoting the published link
|
|
1078
|
-
uru promote <version-id> [library/name.gem] Promote a Gem version
|
|
1169
|
+
uru promote <version-id> [library/name.gem] Promote a staged Gem version/deployment
|
|
1079
1170
|
uru status [library/name.gem] Local source status versus source lock
|
|
1080
1171
|
uru logs [library/name.gem] [--limit 100] [--follow]
|
|
1081
1172
|
Runtime logs
|
|
1082
1173
|
uru open [library/name.gem] Print first published URL
|
|
1083
|
-
uru rollback <version-id> [library/name.gem] --yes
|
|
1084
|
-
Roll back a
|
|
1174
|
+
uru rollback [<version-id>] [library/name.gem] --yes
|
|
1175
|
+
Roll back a live Gem link
|
|
1085
1176
|
uru gems inspect|validate|build|status [library/name.gem]
|
|
1086
1177
|
uru gems versions upload [library/name.gem] Create an immutable Gem version
|
|
1087
1178
|
uru gems versions deploy <version-id> [library/name.gem]
|
|
@@ -1094,19 +1185,22 @@ LIBRARY / DATASETS / TOOLS
|
|
|
1094
1185
|
uru library search <query> Search Library items
|
|
1095
1186
|
uru library mkdir <path> Create a Library folder
|
|
1096
1187
|
# Prompts, personas, contexts: use typed Library paths such as library/Prompts/Foo.prompt.md
|
|
1097
|
-
uru datasets query <dataset-slug> [--limit 50] Query dataset rows
|
|
1188
|
+
uru datasets query <dataset-slug> [--limit 50] Query dataset rows; add --response-format concise for compact output
|
|
1189
|
+
uru automations ls|get|create|enable|disable Manage automations without raw JSON parameters
|
|
1190
|
+
uru automations trigger <id> Start a manual automation run
|
|
1191
|
+
uru automations runs ls|tail <run-id> Inspect automation runs and logs
|
|
1098
1192
|
uru secrets put <name> [library/name.gem] --value-stdin Store Gem secret without shell history
|
|
1099
1193
|
uru secrets ls [library/name.gem] List Gem secret metadata only
|
|
1100
1194
|
uru secrets rm <name> [library/name.gem] --yes Remove a Gem secret value
|
|
1101
1195
|
uru env pull [library/name.gem] Write a local metadata-only .env template
|
|
1102
1196
|
uru env run -- <cmd> Run with local env values and redact output
|
|
1103
|
-
uru tokens create [library/name.gem] --expiry 30d Create a Gem-scoped deploy token
|
|
1197
|
+
uru tokens create [library/name.gem] --name CI --expiry 30d Create a Gem-scoped deploy token
|
|
1104
1198
|
uru tokens ls [library/name.gem] List Gem token metadata only
|
|
1105
1199
|
uru tokens revoke <token-id> --yes Revoke a Gem token immediately
|
|
1106
1200
|
uru tools ls|schema <name>|run <name> Full platform-tool parity
|
|
1107
1201
|
uru operations ls [--family gems] Print generated operation registry
|
|
1108
1202
|
uru operations run <operation-id> --params-json Run any registry-backed operation
|
|
1109
|
-
uru workspace|sharing|
|
|
1203
|
+
uru workspace|sharing|web <op> Registry families for full-platform parity
|
|
1110
1204
|
uru mcp config|install Print/write MCP client config
|
|
1111
1205
|
uru complete bash|zsh|fish Print shell completions
|
|
1112
1206
|
uru update Print the self-update command
|
|
@@ -1180,8 +1274,6 @@ qualified operation form.
|
|
|
1180
1274
|
var REGISTRY_FAMILY_COMMANDS = {
|
|
1181
1275
|
workspace: "workspace",
|
|
1182
1276
|
sharing: "sharing",
|
|
1183
|
-
automations: "automations",
|
|
1184
|
-
automation: "automations",
|
|
1185
1277
|
web: "web",
|
|
1186
1278
|
files: "files",
|
|
1187
1279
|
file: "files",
|
|
@@ -1242,13 +1334,16 @@ deployment that can be promoted after checks; --prod cannot be combined with
|
|
|
1242
1334
|
--no-publish.
|
|
1243
1335
|
`,
|
|
1244
1336
|
promote: `Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
|
|
1337
|
+
uru promote --deployment-id <deployment-id> [library/name.gem] [--link-id <published-link-id>] [--no-wait]
|
|
1245
1338
|
|
|
1246
|
-
|
|
1247
|
-
is
|
|
1339
|
+
Promote an already-staged immutable Gem version or ready deployment. With
|
|
1340
|
+
--link-id, this is a published-link pointer flip after checks pass. If --link-id
|
|
1341
|
+
is omitted, Uru promotes through the default clean Gem link.
|
|
1248
1342
|
`,
|
|
1249
|
-
pull: `Usage: uru pull [library/name.gem]
|
|
1343
|
+
pull: `Usage: uru pull [library/name.gem] [--force]
|
|
1250
1344
|
|
|
1251
1345
|
Download Gem source into the current directory and write .uru/source-lock.json.
|
|
1346
|
+
Refuses to overwrite dirty local files unless --force is supplied.
|
|
1252
1347
|
`,
|
|
1253
1348
|
status: `Usage: uru status [library/name.gem] [--json]
|
|
1254
1349
|
|
|
@@ -1275,11 +1370,12 @@ before the command to emit NDJSON events.
|
|
|
1275
1370
|
|
|
1276
1371
|
Print the first available hosted/published Gem URL.
|
|
1277
1372
|
`,
|
|
1278
|
-
rollback: `Usage: uru rollback <version-id> [library/name.gem] [--link-id <published-link-id>] --yes
|
|
1373
|
+
rollback: `Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes
|
|
1279
1374
|
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1375
|
+
Rollback retargets a live link to an already-materialized older Gem version or
|
|
1376
|
+
deployment after server-side rollback preflight passes. Requires --yes because it
|
|
1377
|
+
mutates live traffic. If --link-id is omitted, Uru rolls back through the active
|
|
1378
|
+
Primary Gem link.
|
|
1283
1379
|
`,
|
|
1284
1380
|
gems: `Usage: uru gems <inspect|validate|build|status|read|write|versions|checks> [...]
|
|
1285
1381
|
|
|
@@ -1290,8 +1386,9 @@ same platform operations used by MCP and the web UI.
|
|
|
1290
1386
|
uru gems versions deploy <version-id> [library/name.gem] [--pct 100]
|
|
1291
1387
|
|
|
1292
1388
|
Create immutable Gem versions and deploy them through the Gem control plane.
|
|
1293
|
-
Upload accepts --message, --tag/--tags, and --preview-alias.
|
|
1294
|
-
|
|
1389
|
+
Upload accepts --message, --tag/--tags, and --preview-alias. Non-100
|
|
1390
|
+
percentages are accepted only with --skip-promote to stage rollout intent; live
|
|
1391
|
+
promote/rollback requires 100 until weighted link routing is enabled.
|
|
1295
1392
|
`,
|
|
1296
1393
|
"gems checks": `Usage: uru gems checks <version-id> [library/name.gem] [--deployment-id <id>]
|
|
1297
1394
|
uru gems checks run browser|security [library/name.gem] [--deployment-id <id>] [--blocking]
|
|
@@ -1306,9 +1403,24 @@ the check a release gate; security also accepts --deep.
|
|
|
1306
1403
|
|
|
1307
1404
|
Operate on Library items through platform tools.
|
|
1308
1405
|
`,
|
|
1309
|
-
datasets: `Usage: uru datasets query <dataset-slug> [--limit 50] [--offset 0]
|
|
1406
|
+
datasets: `Usage: uru datasets query <dataset-slug> [--limit 50] [--offset 0] [--response-format detailed|concise]
|
|
1407
|
+
|
|
1408
|
+
Query dataset rows through the platform dataset operation. Use concise for id plus selected/display fields while keeping server totals and aggregates.
|
|
1409
|
+
`,
|
|
1410
|
+
automations: `Usage: uru automations ls [--status draft|active|paused|archived] [--limit 100] [--view summary|detailed]
|
|
1411
|
+
uru automations get <automation-id>
|
|
1412
|
+
uru automations create <name> [--description <text>] [--status draft|active|paused]
|
|
1413
|
+
uru automations enable <automation-id>
|
|
1414
|
+
uru automations disable <automation-id>
|
|
1415
|
+
uru automations trigger <automation-id> [--input-message <text>] [--idempotency-key <key>]
|
|
1416
|
+
uru automations runs ls [automation-id] [--scope personal|workspace] [--limit 100]
|
|
1417
|
+
uru automations runs tail <run-id> [--limit 1000] [--level debug|info|warn|error]
|
|
1418
|
+
uru automations runs debug|timeline <run-id>
|
|
1419
|
+
uru automations runs cancel <run-id> --yes
|
|
1310
1420
|
|
|
1311
|
-
|
|
1421
|
+
Manage automations through first-class CLI commands backed by the shared
|
|
1422
|
+
operation registry. Reads default to compact summary views; use --view detailed
|
|
1423
|
+
only when you need full graph/config payloads.
|
|
1312
1424
|
`,
|
|
1313
1425
|
tokens: `Usage: uru tokens create [library/name.gem] --expiry <duration> --name <name>
|
|
1314
1426
|
uru tokens ls [library/name.gem]
|
|
@@ -1412,6 +1524,19 @@ async function emitTool(ctx, operationId, params, options = {}) {
|
|
|
1412
1524
|
operation: options.streamStart["operation"] ?? operationId,
|
|
1413
1525
|
phase: "requesting"
|
|
1414
1526
|
});
|
|
1527
|
+
const streamed = await executeOperationStream(ctx, operationId, params, (event) => {
|
|
1528
|
+
writeStreamJsonEvent(ctx.io, event);
|
|
1529
|
+
});
|
|
1530
|
+
if (streamed.streamed) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const unwrapped2 = unwrapToolResult(streamed.payload ?? null);
|
|
1534
|
+
const output2 = options.transform?.(unwrapped2) ?? unwrapped2;
|
|
1535
|
+
writeStreamJson(ctx.io, "result", output2);
|
|
1536
|
+
writeStreamJson(ctx.io, "done", {
|
|
1537
|
+
operation: options.streamStart["operation"] ?? operationId
|
|
1538
|
+
});
|
|
1539
|
+
return;
|
|
1415
1540
|
}
|
|
1416
1541
|
const result = await executeOperation(ctx, operationId, params);
|
|
1417
1542
|
const unwrapped = unwrapToolResult(result);
|
|
@@ -1429,6 +1554,17 @@ async function emitTool(ctx, operationId, params, options = {}) {
|
|
|
1429
1554
|
writeText(ctx.io, options.textExtractor?.(output) ?? JSON.stringify(output, null, 2));
|
|
1430
1555
|
}
|
|
1431
1556
|
}
|
|
1557
|
+
async function executeOperationStream(ctx, operationId, params, onEvent) {
|
|
1558
|
+
const operation = getPlatformOperation(operationId);
|
|
1559
|
+
if (operation === undefined) {
|
|
1560
|
+
throw new CliError(`Unknown operation registry id: ${operationId}`);
|
|
1561
|
+
}
|
|
1562
|
+
const toolParams = { ...params };
|
|
1563
|
+
if (operation.backendOp !== undefined) {
|
|
1564
|
+
toolParams["op"] = operation.backendOp;
|
|
1565
|
+
}
|
|
1566
|
+
return client(ctx).streamTool(operation.backendToolName, toolParams, onEvent);
|
|
1567
|
+
}
|
|
1432
1568
|
async function executeOperation(ctx, operationId, params) {
|
|
1433
1569
|
const operation = getPlatformOperation(operationId);
|
|
1434
1570
|
if (operation === undefined) {
|
|
@@ -1474,7 +1610,7 @@ function tokenGemRefParams(flags, linkedProject, options = {}) {
|
|
|
1474
1610
|
}
|
|
1475
1611
|
const explicitGem = flags.values["--gem"];
|
|
1476
1612
|
if (explicitGem !== undefined) {
|
|
1477
|
-
return { gem_id: explicitGem };
|
|
1613
|
+
return explicitGem.startsWith("library/") ? { path: explicitGem } : { gem_id: explicitGem };
|
|
1478
1614
|
}
|
|
1479
1615
|
const positional = flags.positionals[0];
|
|
1480
1616
|
if (positional !== undefined) {
|
|
@@ -1624,6 +1760,18 @@ function sanitizeConfig(value) {
|
|
|
1624
1760
|
if (value["insecureStorage"] === true) {
|
|
1625
1761
|
config.insecureStorage = true;
|
|
1626
1762
|
}
|
|
1763
|
+
if (isRecord(value["update"])) {
|
|
1764
|
+
const update = {};
|
|
1765
|
+
for (const key of ["lastCheckedAt", "latestVersion"]) {
|
|
1766
|
+
const item = value["update"][key];
|
|
1767
|
+
if (typeof item === "string" && item.trim() !== "") {
|
|
1768
|
+
update[key] = item;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (Object.keys(update).length > 0) {
|
|
1772
|
+
config.update = update;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1627
1775
|
return config;
|
|
1628
1776
|
}
|
|
1629
1777
|
function isNodeError(error) {
|
|
@@ -2008,10 +2156,15 @@ function credentialStoreUnavailableError(backend, message, error) {
|
|
|
2008
2156
|
var CLI_DOCS_URL = "https://docs.uruintelligence.com/cli/errors";
|
|
2009
2157
|
var ERROR_TIPS = {
|
|
2010
2158
|
auth_required: "Run `uru login`, pass `--token`, or set `URU_TOKEN` before retrying.",
|
|
2159
|
+
interactive_login_required: "Run `uru login` before this command, use `uru login --device` on remote shells, or pass `--token`/`URU_TOKEN` in CI.",
|
|
2011
2160
|
conflict: "Run `uru status` or `uru pull` to inspect remote changes; use `--force` only when you intentionally want to overwrite.",
|
|
2012
2161
|
api_request_failed: "Check the request parameters, workspace context, and server response before retrying.",
|
|
2013
2162
|
keychain_access_failed: "Unlock the OS credential store, or retry with `--insecure-storage` only if plaintext local storage is acceptable.",
|
|
2014
2163
|
keychain_unavailable: "Install or unlock the OS credential helper, or retry with `--insecure-storage` only if plaintext local storage is acceptable.",
|
|
2164
|
+
oauth_state_mismatch: "Restart `uru login`; this usually means the browser callback came from an old or mismatched login tab.",
|
|
2165
|
+
oauth_authorization_failed: "Restart `uru login` and approve access in the browser, or use `uru login --device` if the browser cannot reach the callback.",
|
|
2166
|
+
oauth_missing_code: "Restart `uru login`; if the browser callback keeps failing, retry with `uru login --device`.",
|
|
2167
|
+
oauth_request_failed: "Check the API URL and network access, then retry `uru login`; use `uru login --device` from SSH/headless environments.",
|
|
2015
2168
|
oauth_device_failed: "Restart `uru login --device` and complete authorization in the browser before the code expires.",
|
|
2016
2169
|
oauth_device_timeout: "Run `uru login --device` again and complete browser authorization before the timeout.",
|
|
2017
2170
|
cancelled: "The command was cancelled; rerun it only if you still want the operation to continue."
|
|
@@ -2257,6 +2410,120 @@ function terminalWidth(stream, env) {
|
|
|
2257
2410
|
return 80;
|
|
2258
2411
|
}
|
|
2259
2412
|
|
|
2413
|
+
// src/update-notifier.ts
|
|
2414
|
+
var PACKAGE_NAME = "@uru-intelligence/cli";
|
|
2415
|
+
var REGISTRY_LATEST_URL = "https://registry.npmjs.org/@uru-intelligence%2Fcli/latest";
|
|
2416
|
+
var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
2417
|
+
var UPDATE_CHECK_TIMEOUT_MS = 750;
|
|
2418
|
+
async function maybeNotifyCliUpdate(ctx, options = {}) {
|
|
2419
|
+
if (!shouldCheckForUpdate(ctx, options.env ?? process.env)) {
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
const now = options.now?.() ?? Date.now();
|
|
2423
|
+
if (!isUpdateCheckDue(ctx.config, now)) {
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
const checkedConfig = withUpdateCheckTimestamp(ctx.config, now);
|
|
2427
|
+
await writeConfigBestEffort(ctx, checkedConfig);
|
|
2428
|
+
const latestVersion = await fetchLatestVersion({
|
|
2429
|
+
fetchImpl: ctx.fetch ?? fetch,
|
|
2430
|
+
parentSignal: ctx.abortSignal
|
|
2431
|
+
});
|
|
2432
|
+
if (latestVersion === undefined) {
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
await writeConfigBestEffort(ctx, {
|
|
2436
|
+
...checkedConfig,
|
|
2437
|
+
update: {
|
|
2438
|
+
...checkedConfig.update,
|
|
2439
|
+
latestVersion
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
if (compareVersions(latestVersion, CLI_VERSION) <= 0) {
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
ctx.io.stderr.write(`A newer Uru CLI is available: ${CLI_VERSION} → ${latestVersion}. ` + `Run npm install -g ${PACKAGE_NAME}@latest
|
|
2446
|
+
`);
|
|
2447
|
+
}
|
|
2448
|
+
function shouldCheckForUpdate(ctx, env) {
|
|
2449
|
+
return ctx.outputFormat === "text" && ctx.terminal.stderrIsTty && !ctx.terminal.ci && env["NO_UPDATE_NOTIFIER"] === undefined && env["URU_NO_UPDATE_NOTIFIER"] === undefined;
|
|
2450
|
+
}
|
|
2451
|
+
function isUpdateCheckDue(config, now) {
|
|
2452
|
+
const raw = config.update?.lastCheckedAt;
|
|
2453
|
+
if (raw === undefined) {
|
|
2454
|
+
return true;
|
|
2455
|
+
}
|
|
2456
|
+
const lastCheckedAt = Date.parse(raw);
|
|
2457
|
+
return !Number.isFinite(lastCheckedAt) || now - lastCheckedAt >= UPDATE_CHECK_INTERVAL_MS;
|
|
2458
|
+
}
|
|
2459
|
+
function withUpdateCheckTimestamp(config, now) {
|
|
2460
|
+
return {
|
|
2461
|
+
...config,
|
|
2462
|
+
update: {
|
|
2463
|
+
...config.update,
|
|
2464
|
+
lastCheckedAt: new Date(now).toISOString()
|
|
2465
|
+
}
|
|
2466
|
+
};
|
|
2467
|
+
}
|
|
2468
|
+
async function fetchLatestVersion(args) {
|
|
2469
|
+
const controller = new AbortController;
|
|
2470
|
+
const timeout = setTimeout(() => {
|
|
2471
|
+
controller.abort();
|
|
2472
|
+
}, UPDATE_CHECK_TIMEOUT_MS);
|
|
2473
|
+
timeout.unref?.();
|
|
2474
|
+
const abortFromParent = () => {
|
|
2475
|
+
controller.abort(args.parentSignal.reason);
|
|
2476
|
+
};
|
|
2477
|
+
if (args.parentSignal.aborted) {
|
|
2478
|
+
abortFromParent();
|
|
2479
|
+
} else {
|
|
2480
|
+
args.parentSignal.addEventListener("abort", abortFromParent, { once: true });
|
|
2481
|
+
}
|
|
2482
|
+
try {
|
|
2483
|
+
const response = await args.fetchImpl(REGISTRY_LATEST_URL, {
|
|
2484
|
+
headers: { Accept: "application/json" },
|
|
2485
|
+
signal: controller.signal
|
|
2486
|
+
});
|
|
2487
|
+
if (!response.ok) {
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
const payload = await response.json();
|
|
2491
|
+
if (!isRecord2(payload)) {
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
const version = payload["version"];
|
|
2495
|
+
return typeof version === "string" && version.trim() !== "" ? version.trim() : undefined;
|
|
2496
|
+
} catch {
|
|
2497
|
+
return;
|
|
2498
|
+
} finally {
|
|
2499
|
+
clearTimeout(timeout);
|
|
2500
|
+
args.parentSignal.removeEventListener("abort", abortFromParent);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
async function writeConfigBestEffort(ctx, config) {
|
|
2504
|
+
try {
|
|
2505
|
+
await ctx.store.write(config);
|
|
2506
|
+
} catch {}
|
|
2507
|
+
}
|
|
2508
|
+
function compareVersions(left, right) {
|
|
2509
|
+
const leftParts = versionParts(left);
|
|
2510
|
+
const rightParts = versionParts(right);
|
|
2511
|
+
const width = Math.max(leftParts.length, rightParts.length);
|
|
2512
|
+
for (let index = 0;index < width; index += 1) {
|
|
2513
|
+
const delta = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
2514
|
+
if (delta !== 0) {
|
|
2515
|
+
return delta;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
return 0;
|
|
2519
|
+
}
|
|
2520
|
+
function versionParts(version) {
|
|
2521
|
+
return version.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10)).filter((part) => Number.isFinite(part));
|
|
2522
|
+
}
|
|
2523
|
+
function isRecord2(value) {
|
|
2524
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2260
2527
|
// src/auth-commands.ts
|
|
2261
2528
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
2262
2529
|
import { createServer } from "node:http";
|
|
@@ -2633,14 +2900,19 @@ async function resolveFlagToken(flags, stdin) {
|
|
|
2633
2900
|
return;
|
|
2634
2901
|
}
|
|
2635
2902
|
async function login(ctx, options = {}) {
|
|
2636
|
-
const
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2903
|
+
const existingToken = ctx.token === undefined || ctx.token.trim() === "" ? undefined : ctx.token;
|
|
2904
|
+
let token = existingToken ?? await obtainLoginSessionToken(ctx);
|
|
2905
|
+
let result;
|
|
2906
|
+
try {
|
|
2907
|
+
result = await validateSessionToken(ctx, token);
|
|
2908
|
+
} catch (error) {
|
|
2909
|
+
const storedTokenIsStale = existingToken !== undefined && !ctx.tokenIsExplicit && isAuthRequiredError(error);
|
|
2910
|
+
if (!storedTokenIsStale) {
|
|
2911
|
+
throw error;
|
|
2912
|
+
}
|
|
2913
|
+
token = await obtainLoginSessionToken(ctx);
|
|
2914
|
+
result = await validateSessionToken(ctx, token);
|
|
2915
|
+
}
|
|
2644
2916
|
const selectedWorkspace = selectWorkspace(result, ctx.workspace);
|
|
2645
2917
|
await persistLoginToken(ctx, token, selectedWorkspace);
|
|
2646
2918
|
if (options.silentSuccess === true) {
|
|
@@ -2661,6 +2933,18 @@ async function login(ctx, options = {}) {
|
|
|
2661
2933
|
writeText(ctx.io, ctx.insecureStorage ? "Logged in (token stored in local config)" : "Logged in");
|
|
2662
2934
|
}
|
|
2663
2935
|
}
|
|
2936
|
+
async function validateSessionToken(ctx, token) {
|
|
2937
|
+
const loginClient = new UruApiClient({
|
|
2938
|
+
apiUrl: ctx.apiUrl,
|
|
2939
|
+
token,
|
|
2940
|
+
...ctx.workspace !== undefined ? { workspaceId: ctx.workspace } : {},
|
|
2941
|
+
...ctx.fetch !== undefined ? { fetch: ctx.fetch } : {}
|
|
2942
|
+
});
|
|
2943
|
+
return loginClient.validateToken();
|
|
2944
|
+
}
|
|
2945
|
+
function isAuthRequiredError(error) {
|
|
2946
|
+
return error instanceof CliError && error.exitCode === ExitCode.AuthRequired;
|
|
2947
|
+
}
|
|
2664
2948
|
async function readAllStdin(input) {
|
|
2665
2949
|
const chunks = [];
|
|
2666
2950
|
for await (const chunk of input) {
|
|
@@ -2905,8 +3189,6 @@ import { dirname as dirname3 } from "node:path";
|
|
|
2905
3189
|
var GENERIC_FAMILY_COMMANDS = {
|
|
2906
3190
|
workspace: "workspace",
|
|
2907
3191
|
sharing: "sharing",
|
|
2908
|
-
automations: "automations",
|
|
2909
|
-
automation: "automations",
|
|
2910
3192
|
web: "web",
|
|
2911
3193
|
files: "files",
|
|
2912
3194
|
file: "files",
|
|
@@ -2942,6 +3224,7 @@ var TOP_LEVEL_COMMANDS = [
|
|
|
2942
3224
|
"open",
|
|
2943
3225
|
"rollback",
|
|
2944
3226
|
"gems",
|
|
3227
|
+
"automations",
|
|
2945
3228
|
"library",
|
|
2946
3229
|
"datasets",
|
|
2947
3230
|
"tokens",
|
|
@@ -3139,13 +3422,154 @@ async function dispatchDatasets(ctx, sub, args) {
|
|
|
3139
3422
|
return;
|
|
3140
3423
|
}
|
|
3141
3424
|
const flags = parseLocalFlags(args);
|
|
3142
|
-
const datasetId = requireCommandValue(flags.positionals[0], "Usage: uru datasets query <dataset-slug> [--limit 50]");
|
|
3143
|
-
|
|
3425
|
+
const datasetId = requireCommandValue(flags.positionals[0], "Usage: uru datasets query <dataset-slug> [--limit 50] [--response-format detailed|concise]");
|
|
3426
|
+
const params = {
|
|
3144
3427
|
dataset_id: datasetId,
|
|
3145
3428
|
datasetId,
|
|
3146
3429
|
limit: integerFlag(flags, "--limit", 50),
|
|
3147
3430
|
offset: integerFlag(flags, "--offset", 0)
|
|
3148
|
-
}
|
|
3431
|
+
};
|
|
3432
|
+
const responseFormat = flags.values["--response-format"];
|
|
3433
|
+
if (responseFormat !== undefined) {
|
|
3434
|
+
if (responseFormat !== "detailed" && responseFormat !== "concise") {
|
|
3435
|
+
throw new CliError('--response-format must be "detailed" or "concise"');
|
|
3436
|
+
}
|
|
3437
|
+
params["response_format"] = responseFormat;
|
|
3438
|
+
}
|
|
3439
|
+
await emitTool(ctx, "dataset.query", params);
|
|
3440
|
+
}
|
|
3441
|
+
async function dispatchAutomations(ctx, sub, args) {
|
|
3442
|
+
if (sub === "ls" || sub === "list") {
|
|
3443
|
+
const flags = parseLocalFlags(args);
|
|
3444
|
+
const params = {
|
|
3445
|
+
limit: integerFlag(flags, "--limit", 100)
|
|
3446
|
+
};
|
|
3447
|
+
copyOptionalFlag(flags, params, "--status", "status_filter");
|
|
3448
|
+
copyOptionalFlag(flags, params, "--parent-folder-id", "parent_folder_id");
|
|
3449
|
+
copyOptionalFlag(flags, params, "--folder-id", "parent_folder_id");
|
|
3450
|
+
copyAutomationView(flags, params);
|
|
3451
|
+
await emitTool(ctx, "automation.list", params);
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
if (sub === "get" || sub === "inspect") {
|
|
3455
|
+
const flags = parseLocalFlags(args);
|
|
3456
|
+
await emitTool(ctx, "automation.get", {
|
|
3457
|
+
automation_id: requireCommandValue(flags.positionals[0], "Usage: uru automations get <automation-id>")
|
|
3458
|
+
});
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
if (sub === "create") {
|
|
3462
|
+
const flags = parseLocalFlags(args);
|
|
3463
|
+
const params = {
|
|
3464
|
+
name: requireCommandValue(flags.positionals[0], "Usage: uru automations create <name> [--description <text>] [--status draft|active|paused] [--graph-json {...}]")
|
|
3465
|
+
};
|
|
3466
|
+
copyOptionalFlag(flags, params, "--description", "description");
|
|
3467
|
+
copyOptionalFlag(flags, params, "--status", "status");
|
|
3468
|
+
copyOptionalFlag(flags, params, "--parent-folder-id", "parent_folder_id");
|
|
3469
|
+
copyOptionalFlag(flags, params, "--folder-id", "parent_folder_id");
|
|
3470
|
+
const graphJson = flags.values["--graph-json"];
|
|
3471
|
+
if (graphJson !== undefined) {
|
|
3472
|
+
params["graph_json"] = parseParamsJson(graphJson, "--graph-json");
|
|
3473
|
+
}
|
|
3474
|
+
await emitTool(ctx, "automation.create", params);
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
if (sub === "enable" || sub === "disable") {
|
|
3478
|
+
const flags = parseLocalFlags(args);
|
|
3479
|
+
await emitTool(ctx, "automation.update", {
|
|
3480
|
+
automation_id: requireCommandValue(flags.positionals[0], `Usage: uru automations ${sub} <automation-id>`),
|
|
3481
|
+
status: sub === "enable" ? "active" : "paused"
|
|
3482
|
+
});
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
if (sub === "trigger" || sub === "run" || sub === "start") {
|
|
3486
|
+
const flags = parseLocalFlags(args);
|
|
3487
|
+
const params = {
|
|
3488
|
+
automation_id: requireCommandValue(flags.values["--automation-id"] ?? flags.positionals[0], "Usage: uru automations trigger <automation-id> [--input-message <text>] [--idempotency-key <key>]")
|
|
3489
|
+
};
|
|
3490
|
+
copyOptionalFlag(flags, params, "--input-message", "input_message");
|
|
3491
|
+
copyOptionalFlag(flags, params, "--idempotency-key", "idempotency_key");
|
|
3492
|
+
await emitTool(ctx, "automation.run_start", params);
|
|
3493
|
+
return;
|
|
3494
|
+
}
|
|
3495
|
+
if (sub === "runs") {
|
|
3496
|
+
await dispatchAutomationRuns(ctx, args[0], args.slice(1));
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
throw new CliError("Usage: uru automations ls|get|create|enable|disable|trigger|runs");
|
|
3500
|
+
}
|
|
3501
|
+
async function dispatchAutomationRuns(ctx, sub, args) {
|
|
3502
|
+
if (sub === "ls" || sub === "list") {
|
|
3503
|
+
const flags = parseLocalFlags(args);
|
|
3504
|
+
const params = {
|
|
3505
|
+
limit: integerFlag(flags, "--limit", 100)
|
|
3506
|
+
};
|
|
3507
|
+
copyOptionalFlag(flags, params, "--automation-id", "automation_id");
|
|
3508
|
+
copyOptionalFlag(flags, params, "--scope", "scope");
|
|
3509
|
+
copyOptionalFlag(flags, params, "--cursor", "cursor");
|
|
3510
|
+
copyOptionalFlag(flags, params, "--include", "include");
|
|
3511
|
+
if (params["automation_id"] === undefined && flags.positionals[0] !== undefined) {
|
|
3512
|
+
params["automation_id"] = flags.positionals[0];
|
|
3513
|
+
}
|
|
3514
|
+
await emitTool(ctx, "automation.runs_list", params);
|
|
3515
|
+
return;
|
|
3516
|
+
}
|
|
3517
|
+
if (sub === "tail" || sub === "logs") {
|
|
3518
|
+
const flags = parseLocalFlags(args);
|
|
3519
|
+
const params = {
|
|
3520
|
+
run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs tail <run-id> [--limit 1000] [--level debug|info|warn|error]"),
|
|
3521
|
+
limit: integerFlag(flags, "--limit", 1000)
|
|
3522
|
+
};
|
|
3523
|
+
copyOptionalFlag(flags, params, "--level", "level");
|
|
3524
|
+
await emitTool(ctx, "automation.run_logs", params);
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
if (sub === "debug") {
|
|
3528
|
+
const flags = parseLocalFlags(args);
|
|
3529
|
+
const params = {
|
|
3530
|
+
run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs debug <run-id> [--log-limit 100] [--transcript-limit 50]"),
|
|
3531
|
+
log_limit: integerFlag(flags, "--log-limit", 100),
|
|
3532
|
+
transcript_limit: integerFlag(flags, "--transcript-limit", 50)
|
|
3533
|
+
};
|
|
3534
|
+
copyOptionalFlag(flags, params, "--log-level", "log_level");
|
|
3535
|
+
await emitTool(ctx, "automation.run_debug", params);
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
if (sub === "timeline") {
|
|
3539
|
+
const flags = parseLocalFlags(args);
|
|
3540
|
+
await emitTool(ctx, "automation.run_timeline", {
|
|
3541
|
+
run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs timeline <run-id> [--limit 5000]"),
|
|
3542
|
+
limit: integerFlag(flags, "--limit", 5000)
|
|
3543
|
+
});
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (sub === "cancel") {
|
|
3547
|
+
if (!ctx.yes) {
|
|
3548
|
+
throw new CliError("Cancelling an automation run requires --yes because it mutates execution state.");
|
|
3549
|
+
}
|
|
3550
|
+
const flags = parseLocalFlags(args);
|
|
3551
|
+
await emitTool(ctx, "automation.run_cancel", {
|
|
3552
|
+
run_id: requireCommandValue(flags.values["--run-id"] ?? flags.positionals[0], "Usage: uru automations runs cancel <run-id> --yes")
|
|
3553
|
+
});
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
throw new CliError("Usage: uru automations runs ls|tail|debug|timeline|cancel");
|
|
3557
|
+
}
|
|
3558
|
+
function copyOptionalFlag(flags, target, flagName, paramName) {
|
|
3559
|
+
const value = flags.values[flagName];
|
|
3560
|
+
if (value !== undefined) {
|
|
3561
|
+
target[paramName] = value;
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
function copyAutomationView(flags, target) {
|
|
3565
|
+
const value = flags.values["--view"];
|
|
3566
|
+
if (value === undefined) {
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
if (value !== "summary" && value !== "detailed") {
|
|
3570
|
+
throw new CliError('--view must be "summary" or "detailed"');
|
|
3571
|
+
}
|
|
3572
|
+
target["view"] = value;
|
|
3149
3573
|
}
|
|
3150
3574
|
function mcpServerConfig(args) {
|
|
3151
3575
|
const apiKey = requireCommandValue(args.apiKey, "MCP config requires --api-key, --token, URU_TOKEN, or a logged-in CLI token.");
|
|
@@ -3353,10 +3777,10 @@ async function gemBuild(ctx, args, linkedProject) {
|
|
|
3353
3777
|
}
|
|
3354
3778
|
async function gemPromote(ctx, args, linkedProject) {
|
|
3355
3779
|
const flags = parseLocalFlags(args);
|
|
3356
|
-
assertAllowedLocalFlags(flags, ["--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
|
|
3780
|
+
assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--link-id", "--no-wait", "--path", "--pct"], "uru promote");
|
|
3357
3781
|
await deployVersionToPublishedLink(ctx, flags, linkedProject, {
|
|
3358
3782
|
operation: "promote",
|
|
3359
|
-
usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>]"
|
|
3783
|
+
usage: "Usage: uru promote <version-id> [library/name.gem] [--link-id <published-link-id>] | uru promote --deployment-id <deployment-id> [library/name.gem]"
|
|
3360
3784
|
});
|
|
3361
3785
|
}
|
|
3362
3786
|
async function gemChecks(ctx, args, linkedProject) {
|
|
@@ -3366,10 +3790,11 @@ async function gemChecks(ctx, args, linkedProject) {
|
|
|
3366
3790
|
return;
|
|
3367
3791
|
}
|
|
3368
3792
|
assertAllowedLocalFlags(flags, ["--deployment-id", "--gem", "--path"], "uru gems checks");
|
|
3793
|
+
const explicitDeploymentId = flags.values["--deployment-id"];
|
|
3794
|
+
const gemPathArgs = explicitDeploymentId === undefined ? flags.positionals.slice(1) : flags.positionals;
|
|
3369
3795
|
const params = {
|
|
3370
|
-
path: gemPathFromArgs(
|
|
3796
|
+
path: gemPathFromArgs(gemPathArgs, linkedProject)
|
|
3371
3797
|
};
|
|
3372
|
-
const explicitDeploymentId = flags.values["--deployment-id"];
|
|
3373
3798
|
if (explicitDeploymentId !== undefined) {
|
|
3374
3799
|
params["deployment_id"] = explicitDeploymentId;
|
|
3375
3800
|
} else if (flags.positionals[0] !== undefined) {
|
|
@@ -3576,13 +4001,21 @@ async function gemOpen(ctx, args, linkedProject) {
|
|
|
3576
4001
|
}
|
|
3577
4002
|
async function gemRollback(ctx, args, linkedProject) {
|
|
3578
4003
|
const flags = parseLocalFlags(args);
|
|
3579
|
-
assertAllowedLocalFlags(flags, [
|
|
4004
|
+
assertAllowedLocalFlags(flags, [
|
|
4005
|
+
"--deployment-id",
|
|
4006
|
+
"--gem",
|
|
4007
|
+
"--link-id",
|
|
4008
|
+
"--no-wait",
|
|
4009
|
+
"--path",
|
|
4010
|
+
"--pct",
|
|
4011
|
+
"--yes"
|
|
4012
|
+
], "uru rollback");
|
|
3580
4013
|
if (!ctx.yes && !flags.booleans.has("--yes")) {
|
|
3581
4014
|
throw new CliError("Rollback requires --yes to confirm the link pointer change.");
|
|
3582
4015
|
}
|
|
3583
4016
|
await deployVersionToPublishedLink(ctx, flags, linkedProject, {
|
|
3584
4017
|
operation: "rollback",
|
|
3585
|
-
usage: "Usage: uru rollback <version-id> [library/name.gem] [--link-id <published-link-id>] --yes"
|
|
4018
|
+
usage: "Usage: uru rollback [<version-id>] [library/name.gem] [--deployment-id <deployment-id>] [--link-id <published-link-id>] --yes"
|
|
3586
4019
|
});
|
|
3587
4020
|
}
|
|
3588
4021
|
async function gemWrite(ctx, args) {
|
|
@@ -3619,12 +4052,15 @@ async function dispatchGemVersions(ctx, args, linkedProject) {
|
|
|
3619
4052
|
const gemPath = gemPathFromArgs(flags.positionals.slice(1), linkedProject);
|
|
3620
4053
|
const pct = flags.values["--pct"] ?? trafficPercent;
|
|
3621
4054
|
const trafficPercentValue = pct === undefined || pct === "" ? 100 : parseTrafficPercent(pct);
|
|
3622
|
-
|
|
4055
|
+
const publish = !flags.booleans.has("--skip-promote");
|
|
4056
|
+
if (publish && trafficPercentValue !== 100) {
|
|
4057
|
+
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.");
|
|
4058
|
+
}
|
|
3623
4059
|
const params = {
|
|
3624
4060
|
path: gemPath,
|
|
3625
4061
|
version_id: requireCommandValue(versionId, "Usage: uru gems versions deploy <version-id> [library/name.gem] [--pct 100] [--skip-promote]"),
|
|
3626
4062
|
traffic_percent: trafficPercentValue,
|
|
3627
|
-
publish
|
|
4063
|
+
publish
|
|
3628
4064
|
};
|
|
3629
4065
|
copyStringFlag(flags, params, "--link-id", "link_id");
|
|
3630
4066
|
if (flags.booleans.has("--no-wait")) {
|
|
@@ -3659,13 +4095,17 @@ async function gemVersionUpload(ctx, args, linkedProject) {
|
|
|
3659
4095
|
}
|
|
3660
4096
|
copyStringFlag(flags, params, "--source-version-id", "source_version_id");
|
|
3661
4097
|
copyStringFlag(flags, params, "--message", "message");
|
|
3662
|
-
|
|
4098
|
+
const previewAlias = flags.values["--preview-alias"];
|
|
4099
|
+
if (previewAlias !== undefined) {
|
|
4100
|
+
params["preview_alias"] = validatePreviewAlias(previewAlias);
|
|
4101
|
+
}
|
|
3663
4102
|
const tags = tagsFromFlags(flags);
|
|
3664
4103
|
if (tags.length > 0) {
|
|
3665
4104
|
params["tags"] = tags;
|
|
3666
4105
|
}
|
|
3667
4106
|
await emitGemTool(ctx, "gem.version_upload", params, {
|
|
3668
|
-
streamStart: { operation: "gem.version_upload", path: gemPath }
|
|
4107
|
+
streamStart: { operation: "gem.version_upload", path: gemPath },
|
|
4108
|
+
textExtractor: extractVersionUploadSummary
|
|
3669
4109
|
});
|
|
3670
4110
|
}
|
|
3671
4111
|
function parseTrafficPercent(raw) {
|
|
@@ -3675,29 +4115,33 @@ function parseTrafficPercent(raw) {
|
|
|
3675
4115
|
}
|
|
3676
4116
|
return value;
|
|
3677
4117
|
}
|
|
3678
|
-
function assertCurrentBackendTrafficSupport(value) {
|
|
3679
|
-
if (value !== 100) {
|
|
3680
|
-
throw new CliError("Gradual percentage Gem deploys require backend traffic-splitting support; omit @pct/--pct or use 100 until split traffic is enabled.");
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
4118
|
async function deployVersionToPublishedLink(ctx, flags, linkedProject, options) {
|
|
3684
|
-
const
|
|
3685
|
-
const
|
|
4119
|
+
const deploymentId = flags.values["--deployment-id"];
|
|
4120
|
+
const firstPositional = flags.positionals[0];
|
|
4121
|
+
const versionId = deploymentId === undefined && firstPositional !== undefined ? firstPositional.startsWith("library/") ? undefined : firstPositional : undefined;
|
|
4122
|
+
if (deploymentId === undefined && versionId === undefined) {
|
|
4123
|
+
throw new CliError(options.usage);
|
|
4124
|
+
}
|
|
4125
|
+
const gemPath = gemPathFromArgs(versionId === undefined ? flags.positionals : flags.positionals.slice(1), linkedProject);
|
|
3686
4126
|
const trafficPercent = parseTrafficPercent(flags.values["--pct"] ?? "100");
|
|
3687
|
-
|
|
4127
|
+
if (trafficPercent !== 100) {
|
|
4128
|
+
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.`);
|
|
4129
|
+
}
|
|
3688
4130
|
const params = {
|
|
3689
|
-
path: gemPath
|
|
3690
|
-
version_id: versionId,
|
|
3691
|
-
traffic_percent: trafficPercent,
|
|
3692
|
-
publish: true,
|
|
3693
|
-
operation: options.operation
|
|
4131
|
+
path: gemPath
|
|
3694
4132
|
};
|
|
4133
|
+
if (deploymentId === undefined) {
|
|
4134
|
+
params["version_id"] = requireCommandValue(versionId, options.usage);
|
|
4135
|
+
} else {
|
|
4136
|
+
params["app_deployment_id"] = deploymentId;
|
|
4137
|
+
}
|
|
3695
4138
|
copyStringFlag(flags, params, "--link-id", "link_id");
|
|
3696
4139
|
if (flags.booleans.has("--no-wait")) {
|
|
3697
4140
|
params["wait"] = false;
|
|
3698
4141
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
4142
|
+
const operation = `gem.${options.operation}`;
|
|
4143
|
+
await emitGemTool(ctx, operation, params, {
|
|
4144
|
+
streamStart: { operation, path: gemPath },
|
|
3701
4145
|
textExtractor: extractDeploymentUrl
|
|
3702
4146
|
});
|
|
3703
4147
|
}
|
|
@@ -3708,6 +4152,13 @@ function tagsFromFlags(flags) {
|
|
|
3708
4152
|
}
|
|
3709
4153
|
return raw.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
|
|
3710
4154
|
}
|
|
4155
|
+
function validatePreviewAlias(rawAlias) {
|
|
4156
|
+
const alias = rawAlias.trim();
|
|
4157
|
+
if (alias.length === 0 || alias.length > 63 || !/^[a-z][a-z0-9-]*[a-z0-9]$/u.test(alias)) {
|
|
4158
|
+
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.");
|
|
4159
|
+
}
|
|
4160
|
+
return alias;
|
|
4161
|
+
}
|
|
3711
4162
|
function assertAllowedLocalFlags(flags, allowedFlags, commandName) {
|
|
3712
4163
|
const allowed = new Set(allowedFlags);
|
|
3713
4164
|
const unexpected = localFlagNames(flags).filter((flag) => !allowed.has(flag));
|
|
@@ -3727,9 +4178,12 @@ function localFlagNames(flags) {
|
|
|
3727
4178
|
}
|
|
3728
4179
|
async function writeBuildManifest(ctx, outdir, buildResult) {
|
|
3729
4180
|
const manifest = gemBuildManifestFromBuildResult(buildResult);
|
|
4181
|
+
const providerDecision = gemProviderDecisionFromBuildResult(buildResult, manifest);
|
|
3730
4182
|
const absoluteOutdir = resolve2(ctx.cwd, outdir);
|
|
3731
4183
|
await mkdir4(absoluteOutdir, { recursive: true });
|
|
3732
4184
|
await writeFile4(join3(absoluteOutdir, "gem-build-manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4185
|
+
`, { mode: 384 });
|
|
4186
|
+
await writeFile4(join3(absoluteOutdir, "gem-provider-decision.json"), `${JSON.stringify(providerDecision, null, 2)}
|
|
3733
4187
|
`, { mode: 384 });
|
|
3734
4188
|
}
|
|
3735
4189
|
function gemBuildManifestFromBuildResult(buildResult) {
|
|
@@ -3782,6 +4236,80 @@ function normalizeGemBuildManifest(value) {
|
|
|
3782
4236
|
manifest_kind: "gem_build_manifest"
|
|
3783
4237
|
};
|
|
3784
4238
|
}
|
|
4239
|
+
function gemProviderDecisionFromBuildResult(buildResult, manifest) {
|
|
4240
|
+
const root = jsonObjectOrNull(buildResult);
|
|
4241
|
+
const build = jsonObjectOrNull(root?.["build"]);
|
|
4242
|
+
const buildResultDetails = jsonObjectOrNull(build?.["build_result"]);
|
|
4243
|
+
const buildMetadata = jsonObjectOrNull(buildResultDetails?.["metadata"]);
|
|
4244
|
+
const runtimeProfile = firstJsonObject([
|
|
4245
|
+
root?.["runtime_profile"],
|
|
4246
|
+
root?.["runtimeProfile"],
|
|
4247
|
+
build?.["runtime_profile"],
|
|
4248
|
+
build?.["runtimeProfile"],
|
|
4249
|
+
buildResultDetails?.["runtime_profile"],
|
|
4250
|
+
buildResultDetails?.["runtimeProfile"],
|
|
4251
|
+
buildMetadata?.["runtime_profile"],
|
|
4252
|
+
buildMetadata?.["runtimeProfile"]
|
|
4253
|
+
]) ?? {};
|
|
4254
|
+
const providerDecision = firstJsonObject([
|
|
4255
|
+
root?.["provider_decision"],
|
|
4256
|
+
root?.["providerDecision"],
|
|
4257
|
+
build?.["provider_decision"],
|
|
4258
|
+
build?.["providerDecision"],
|
|
4259
|
+
buildResultDetails?.["provider_decision"],
|
|
4260
|
+
buildResultDetails?.["providerDecision"],
|
|
4261
|
+
buildMetadata?.["provider_decision"],
|
|
4262
|
+
buildMetadata?.["providerDecision"]
|
|
4263
|
+
]) ?? {};
|
|
4264
|
+
const runtimeContract = firstJsonObject([
|
|
4265
|
+
root?.["runtime_contract"],
|
|
4266
|
+
root?.["runtimeContract"],
|
|
4267
|
+
build?.["runtime_contract"],
|
|
4268
|
+
build?.["runtimeContract"],
|
|
4269
|
+
buildResultDetails?.["runtime_contract"],
|
|
4270
|
+
buildResultDetails?.["runtimeContract"],
|
|
4271
|
+
buildMetadata?.["runtime_contract"],
|
|
4272
|
+
buildMetadata?.["runtimeContract"],
|
|
4273
|
+
manifest["runtime_contract"],
|
|
4274
|
+
manifest["runtimeContract"]
|
|
4275
|
+
]) ?? {};
|
|
4276
|
+
return {
|
|
4277
|
+
schema: "uru.gem.provider_decision.v1",
|
|
4278
|
+
schema_version: 1,
|
|
4279
|
+
manifest_kind: "gem_provider_decision",
|
|
4280
|
+
provider: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["provider", "runtime_provider", "runtimeProvider", "selected_provider"]),
|
|
4281
|
+
runtime_class: stringFieldFromJsonObjects([providerDecision, runtimeProfile, manifest], ["runtime_class", "runtimeClass", "class", "serving_mode"]),
|
|
4282
|
+
runtime_profile: runtimeProfile,
|
|
4283
|
+
provider_decision: providerDecision,
|
|
4284
|
+
runtime_contract: runtimeContract,
|
|
4285
|
+
manifest_summary: {
|
|
4286
|
+
serving_mode: manifest["serving_mode"] ?? null,
|
|
4287
|
+
worker_compatibility: manifest["worker_compatibility"] ?? null,
|
|
4288
|
+
serverful_compatibility: manifest["serverful_compatibility"] ?? null,
|
|
4289
|
+
runtime_requirements: manifest["runtime_requirements"] ?? null
|
|
4290
|
+
}
|
|
4291
|
+
};
|
|
4292
|
+
}
|
|
4293
|
+
function firstJsonObject(values) {
|
|
4294
|
+
for (const value of values) {
|
|
4295
|
+
const object = jsonObjectOrNull(value);
|
|
4296
|
+
if (object !== null) {
|
|
4297
|
+
return object;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
return null;
|
|
4301
|
+
}
|
|
4302
|
+
function stringFieldFromJsonObjects(objects, keys) {
|
|
4303
|
+
for (const object of objects) {
|
|
4304
|
+
for (const key of keys) {
|
|
4305
|
+
const value = object[key];
|
|
4306
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
4307
|
+
return value.trim();
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
return null;
|
|
4312
|
+
}
|
|
3785
4313
|
function jsonObjectOrNull(value) {
|
|
3786
4314
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
3787
4315
|
return null;
|
|
@@ -3791,6 +4319,48 @@ function jsonObjectOrNull(value) {
|
|
|
3791
4319
|
function extractDeploymentUrl(value) {
|
|
3792
4320
|
return firstUrl(value);
|
|
3793
4321
|
}
|
|
4322
|
+
function extractVersionUploadSummary(value) {
|
|
4323
|
+
const objects = collectJsonObjects(value);
|
|
4324
|
+
const versionId = stringFieldFromJsonObjects(objects, [
|
|
4325
|
+
"version_id",
|
|
4326
|
+
"versionId",
|
|
4327
|
+
"id"
|
|
4328
|
+
]);
|
|
4329
|
+
const previewUrl = stringFieldFromJsonObjects(objects, [
|
|
4330
|
+
"preview_url",
|
|
4331
|
+
"previewUrl",
|
|
4332
|
+
"preview_alias_url",
|
|
4333
|
+
"previewAliasUrl",
|
|
4334
|
+
"url"
|
|
4335
|
+
]) ?? firstUrl(value);
|
|
4336
|
+
const lines = [
|
|
4337
|
+
versionId === null ? null : `Version: ${versionId}`,
|
|
4338
|
+
previewUrl === undefined || previewUrl === null ? null : `Preview: ${previewUrl}`
|
|
4339
|
+
].filter((line) => line !== null);
|
|
4340
|
+
return lines.length === 0 ? undefined : lines.join(`
|
|
4341
|
+
`);
|
|
4342
|
+
}
|
|
4343
|
+
function collectJsonObjects(value) {
|
|
4344
|
+
const objects = [];
|
|
4345
|
+
collectJsonObjectsInto(value, objects);
|
|
4346
|
+
return objects;
|
|
4347
|
+
}
|
|
4348
|
+
function collectJsonObjectsInto(value, objects) {
|
|
4349
|
+
if (Array.isArray(value)) {
|
|
4350
|
+
for (const item of value) {
|
|
4351
|
+
collectJsonObjectsInto(item, objects);
|
|
4352
|
+
}
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
const object = jsonObjectOrNull(value);
|
|
4356
|
+
if (object === null) {
|
|
4357
|
+
return;
|
|
4358
|
+
}
|
|
4359
|
+
objects.push(object);
|
|
4360
|
+
for (const child of Object.values(object)) {
|
|
4361
|
+
collectJsonObjectsInto(child, objects);
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
3794
4364
|
function trimTrailingSlashes(value) {
|
|
3795
4365
|
let end = value.length;
|
|
3796
4366
|
while (end > 0 && value.charCodeAt(end - 1) === 47) {
|
|
@@ -3807,10 +4377,20 @@ function trimLeadingSlashes(value) {
|
|
|
3807
4377
|
}
|
|
3808
4378
|
|
|
3809
4379
|
// src/gem-source-commands.ts
|
|
3810
|
-
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
4380
|
+
import { mkdir as mkdir5, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
|
|
3811
4381
|
import { dirname as dirname4, join as join4, resolve as resolve3 } from "node:path";
|
|
3812
4382
|
async function gemSourcePull(ctx, args, linkedProject) {
|
|
3813
|
-
const
|
|
4383
|
+
const flags = parseLocalFlags(args);
|
|
4384
|
+
const gemPath = gemPathFromArgs(flags.positionals, linkedProject);
|
|
4385
|
+
const force = flags.booleans.has("--force");
|
|
4386
|
+
const previousLock = await readSourceLock(ctx.cwd);
|
|
4387
|
+
const localFilesBeforePull = await collectLocalSourceFiles(ctx.cwd);
|
|
4388
|
+
const localStatusBeforePull = compareSourceLock(previousLock, localFilesBeforePull);
|
|
4389
|
+
if (!force && !localStatusBeforePull.clean) {
|
|
4390
|
+
throw new CliError(dirtyPullErrorMessage(localStatusBeforePull), {
|
|
4391
|
+
code: ExitCode.Conflict
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
3814
4394
|
const tree = unwrapToolResult(await executeOperation(ctx, "gem.fs_tree", {
|
|
3815
4395
|
path: gemPath,
|
|
3816
4396
|
recursive: true
|
|
@@ -3830,6 +4410,7 @@ async function gemSourcePull(ctx, args, linkedProject) {
|
|
|
3830
4410
|
await writePulledSourceFile(ctx.cwd, filePath, content);
|
|
3831
4411
|
files[filePath] = sha256Hex(content);
|
|
3832
4412
|
}
|
|
4413
|
+
const pruned = await pruneRemoteDeletedSourceFiles(ctx.cwd, previousLock, localFilesBeforePull, new Set(filePaths));
|
|
3833
4414
|
const lock = {
|
|
3834
4415
|
...sourceVersionId === undefined ? {} : { sourceVersionId },
|
|
3835
4416
|
files,
|
|
@@ -3840,8 +4421,10 @@ async function gemSourcePull(ctx, args, linkedProject) {
|
|
|
3840
4421
|
ok: true,
|
|
3841
4422
|
path: gemPath,
|
|
3842
4423
|
files: filePaths,
|
|
4424
|
+
prunedFiles: pruned.deleted,
|
|
4425
|
+
preservedFiles: pruned.preserved,
|
|
3843
4426
|
...sourceVersionId === undefined ? {} : { sourceVersionId }
|
|
3844
|
-
},
|
|
4427
|
+
}, formatPullSummary(gemPath, filePaths.length, pruned));
|
|
3845
4428
|
}
|
|
3846
4429
|
async function gemSourceStatus(ctx, args, linkedProject) {
|
|
3847
4430
|
const flags = parseLocalFlags(args);
|
|
@@ -3901,24 +4484,16 @@ async function gemSourcePush(ctx, args, linkedProject) {
|
|
|
3901
4484
|
...lock.sourceVersionId === undefined ? {} : { sourceVersionId: lock.sourceVersionId }
|
|
3902
4485
|
};
|
|
3903
4486
|
try {
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
}
|
|
3913
|
-
|
|
3914
|
-
const writeResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_batch_write", {
|
|
3915
|
-
path: gemPath,
|
|
3916
|
-
files: sortedFiles.map(localFileBatchWritePayload),
|
|
3917
|
-
...sourceMutationCasParams(sourceCas, force),
|
|
3918
|
-
change_summary: force ? "Force-push local Gem source via Uru CLI." : "Push local Gem source via Uru CLI."
|
|
3919
|
-
}));
|
|
3920
|
-
sourceCas = nextSourceMutationCas(sourceCas, writeResult);
|
|
3921
|
-
}
|
|
4487
|
+
const syncResult = unwrapToolResult(await executeOperation(ctx, "gem.fs_sync", {
|
|
4488
|
+
path: gemPath,
|
|
4489
|
+
files: sortedFiles.map(localFileBatchWritePayload),
|
|
4490
|
+
delete_paths: localStatus.deleted.map((deletedPath) => ({
|
|
4491
|
+
path: deletedPath
|
|
4492
|
+
})),
|
|
4493
|
+
...sourceMutationCasParams(sourceCas, force),
|
|
4494
|
+
change_summary: force ? "Force-push local Gem source via Uru CLI." : "Push local Gem source via Uru CLI."
|
|
4495
|
+
}));
|
|
4496
|
+
sourceCas = nextSourceMutationCas(sourceCas, syncResult);
|
|
3922
4497
|
} catch (error) {
|
|
3923
4498
|
if (isConflictLikeError(error)) {
|
|
3924
4499
|
throw new CliError(error instanceof Error ? error.message : "Gem source conflict", { code: ExitCode.Conflict, cause: error });
|
|
@@ -3945,6 +4520,44 @@ function localFileBatchWritePayload(file) {
|
|
|
3945
4520
|
content_base64: file.contentBase64
|
|
3946
4521
|
};
|
|
3947
4522
|
}
|
|
4523
|
+
async function pruneRemoteDeletedSourceFiles(cwd, previousLock, localFilesBeforePull, remotePaths) {
|
|
4524
|
+
const deleted = [];
|
|
4525
|
+
const preserved = [];
|
|
4526
|
+
const lockedFiles = Object.entries(previousLock.files ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
4527
|
+
for (const [projectPath2, lockedHash] of lockedFiles) {
|
|
4528
|
+
if (remotePaths.has(projectPath2)) {
|
|
4529
|
+
continue;
|
|
4530
|
+
}
|
|
4531
|
+
const localFile = localFilesBeforePull[projectPath2];
|
|
4532
|
+
if (localFile === undefined) {
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
if (localFile.sha256 !== lockedHash) {
|
|
4536
|
+
preserved.push(projectPath2);
|
|
4537
|
+
continue;
|
|
4538
|
+
}
|
|
4539
|
+
const safePath = safeProjectPath(projectPath2);
|
|
4540
|
+
if (safePath === null) {
|
|
4541
|
+
preserved.push(projectPath2);
|
|
4542
|
+
continue;
|
|
4543
|
+
}
|
|
4544
|
+
await rm3(join4(resolve3(cwd), ...safePath.split("/")), { force: true });
|
|
4545
|
+
deleted.push(projectPath2);
|
|
4546
|
+
}
|
|
4547
|
+
return { deleted, preserved };
|
|
4548
|
+
}
|
|
4549
|
+
function formatPullSummary(gemPath, writtenCount, pruned) {
|
|
4550
|
+
const parts = [
|
|
4551
|
+
`Pulled ${writtenCount} file${writtenCount === 1 ? "" : "s"} from ${gemPath}`
|
|
4552
|
+
];
|
|
4553
|
+
if (pruned.deleted.length > 0) {
|
|
4554
|
+
parts.push(`pruned ${pruned.deleted.length} remote-deleted file${pruned.deleted.length === 1 ? "" : "s"}`);
|
|
4555
|
+
}
|
|
4556
|
+
if (pruned.preserved.length > 0) {
|
|
4557
|
+
parts.push(`preserved ${pruned.preserved.length} locally changed remote-deleted file${pruned.preserved.length === 1 ? "" : "s"}`);
|
|
4558
|
+
}
|
|
4559
|
+
return parts.join("; ");
|
|
4560
|
+
}
|
|
3948
4561
|
function sourceMutationCasParams(cas, force) {
|
|
3949
4562
|
if (force || cas.sourceVersionId === undefined) {
|
|
3950
4563
|
return { force: true };
|
|
@@ -4036,6 +4649,17 @@ function formatLocalStatusEntries(status) {
|
|
|
4036
4649
|
...status.deleted.map((path) => `D ${path}`)
|
|
4037
4650
|
];
|
|
4038
4651
|
}
|
|
4652
|
+
function dirtyPullErrorMessage(status) {
|
|
4653
|
+
const entries = formatLocalStatusEntries(status);
|
|
4654
|
+
const preview = entries.slice(0, 10).map((entry) => ` ${entry}`).join(`
|
|
4655
|
+
`);
|
|
4656
|
+
const moreCount = Math.max(0, entries.length - 10);
|
|
4657
|
+
const moreLine = moreCount > 0 ? `
|
|
4658
|
+
...and ${moreCount} more local change(s)` : "";
|
|
4659
|
+
return `Refusing to pull because local Gem source has uncommitted changes that could be overwritten.
|
|
4660
|
+
Run \`uru status\` to inspect them, save or push your changes, or re-run \`uru pull --force\` to replace local files with server source.
|
|
4661
|
+
${preview}${moreLine}`;
|
|
4662
|
+
}
|
|
4039
4663
|
async function tryRemoteSourceOperation(ctx, operationId, params) {
|
|
4040
4664
|
try {
|
|
4041
4665
|
return {
|
|
@@ -4198,15 +4822,29 @@ import { dirname as dirname5, resolve as resolve4 } from "node:path";
|
|
|
4198
4822
|
async function dispatchTokens(ctx, sub, args, linkedProject) {
|
|
4199
4823
|
if (sub === "create") {
|
|
4200
4824
|
const flags = parseLocalFlags(args);
|
|
4825
|
+
const usage = "Usage: uru tokens create [library/name.gem] [--gem <id>] --name <name> (--expiry 30d|--expires-at <iso>)";
|
|
4826
|
+
const name = requireCommandValue(flags.values["--name"], usage);
|
|
4827
|
+
const expiry = flags.values["--expiry"];
|
|
4828
|
+
const expiresAt = flags.values["--expires-at"];
|
|
4829
|
+
if (expiry === undefined && expiresAt === undefined) {
|
|
4830
|
+
throw new CliError("Gem deploy token creation requires --expiry <duration> or --expires-at <iso timestamp>.");
|
|
4831
|
+
}
|
|
4832
|
+
if (expiry !== undefined && expiresAt !== undefined) {
|
|
4833
|
+
throw new CliError("Pass only one token lifetime flag: --expiry or --expires-at.");
|
|
4834
|
+
}
|
|
4201
4835
|
const params = {
|
|
4202
4836
|
...tokenGemRefParams(flags, linkedProject, {
|
|
4203
4837
|
required: true,
|
|
4204
|
-
usage
|
|
4205
|
-
})
|
|
4838
|
+
usage
|
|
4839
|
+
}),
|
|
4840
|
+
name
|
|
4206
4841
|
};
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4842
|
+
if (expiry !== undefined) {
|
|
4843
|
+
params["expiry"] = expiry;
|
|
4844
|
+
}
|
|
4845
|
+
if (expiresAt !== undefined) {
|
|
4846
|
+
params["expires_at"] = expiresAt;
|
|
4847
|
+
}
|
|
4210
4848
|
const scopes = tokenScopes(flags);
|
|
4211
4849
|
if (scopes.length > 0) {
|
|
4212
4850
|
params["scopes"] = scopes;
|
|
@@ -4640,6 +5278,7 @@ async function runCli(options) {
|
|
|
4640
5278
|
credentialStore,
|
|
4641
5279
|
apiUrl,
|
|
4642
5280
|
...token !== undefined ? { token } : {},
|
|
5281
|
+
tokenIsExplicit: flagToken !== undefined || envToken !== undefined,
|
|
4643
5282
|
...workspace !== undefined ? { workspace } : {},
|
|
4644
5283
|
cwd,
|
|
4645
5284
|
json: outputFormat === "json",
|
|
@@ -4661,6 +5300,9 @@ async function runCli(options) {
|
|
|
4661
5300
|
};
|
|
4662
5301
|
throwIfCancelled(cancellation.signal);
|
|
4663
5302
|
await dispatchWithImplicitLogin(ctx, parsed.command, linkedProject);
|
|
5303
|
+
await maybeNotifyCliUpdate(ctx, {
|
|
5304
|
+
...options.env === undefined ? {} : { env: options.env }
|
|
5305
|
+
});
|
|
4664
5306
|
return ExitCode.Ok;
|
|
4665
5307
|
} catch (error) {
|
|
4666
5308
|
const cliError = normalizeError(error);
|
|
@@ -4894,6 +5536,11 @@ async function dispatch(ctx, command, linkedProject) {
|
|
|
4894
5536
|
await dispatchDatasets(ctx, sub, command.slice(2));
|
|
4895
5537
|
return;
|
|
4896
5538
|
}
|
|
5539
|
+
if (head === "automations") {
|
|
5540
|
+
await withWorkspace(ctx);
|
|
5541
|
+
await dispatchAutomations(ctx, sub, command.slice(2));
|
|
5542
|
+
return;
|
|
5543
|
+
}
|
|
4897
5544
|
if (head === "tokens" || head === "token") {
|
|
4898
5545
|
await withWorkspace(ctx);
|
|
4899
5546
|
await dispatchTokens(ctx, sub, command.slice(2), linkedProject);
|