@asteroidai/cli 1.9.11 → 1.9.27
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/dist/asteroid.js +675 -328
- package/dist/core.js +287 -270
- package/package.json +1 -1
- package/src/account.ts +287 -10
- package/src/auth/credentials.ts +45 -7
- package/src/auth/store.ts +115 -7
- package/src/generated/commands.ts +199 -182
- package/src/program.ts +13 -1
- package/src/reference.ts +25 -1
- package/src/run.test.ts +289 -5
- package/src/run.ts +14 -4
- package/src/sync/commands.ts +0 -48
- package/src/sync/run.ts +60 -26
- package/src/sync/sync.test.ts +55 -16
- package/src/sync/workflow-pull.ts +21 -18
- package/src/sync/workflow-push.ts +6 -0
- package/src/types.ts +2 -0
- package/src/ui.ts +5 -0
package/src/run.test.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync } from "node:fs";
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
|
|
6
6
|
import { CredentialStore } from "./auth/store";
|
|
7
|
+
import { WorkosError } from "./auth/workos";
|
|
7
8
|
import { ALL_COMMANDS } from "./commands";
|
|
8
9
|
import { readEnv, saveConfig } from "./config";
|
|
9
10
|
import { renderReference } from "./reference";
|
|
@@ -323,8 +324,8 @@ describe("terminal safety", () => {
|
|
|
323
324
|
});
|
|
324
325
|
});
|
|
325
326
|
|
|
326
|
-
describe("
|
|
327
|
-
it("
|
|
327
|
+
describe("files --out", () => {
|
|
328
|
+
it("writes a pinned version into the folder it is given", async () => {
|
|
328
329
|
const h = harness({ ASTEROID_API_KEY: "sk_test" }, () =>
|
|
329
330
|
json({
|
|
330
331
|
rev: 1,
|
|
@@ -344,7 +345,7 @@ describe("workflow pull", () => {
|
|
|
344
345
|
const dir = join(h.configDir, "pulled");
|
|
345
346
|
|
|
346
347
|
const code = await run(
|
|
347
|
-
["workflow", "
|
|
348
|
+
["workflow", "version", "pull", "w1", "v-2", "--out", dir],
|
|
348
349
|
h.deps,
|
|
349
350
|
);
|
|
350
351
|
|
|
@@ -356,6 +357,289 @@ describe("workflow pull", () => {
|
|
|
356
357
|
});
|
|
357
358
|
});
|
|
358
359
|
|
|
360
|
+
describe("workflow pull and push", () => {
|
|
361
|
+
const tree = {
|
|
362
|
+
rev: 1,
|
|
363
|
+
version: 2,
|
|
364
|
+
versionId: "v-2",
|
|
365
|
+
workflowName: "Fill form",
|
|
366
|
+
files: [
|
|
367
|
+
{ path: "settings.yaml", kind: "structural", size: 4, content: "a: 1" },
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
it("pulls into ./<workflow-name>/head and pushes from there with no arguments", async () => {
|
|
372
|
+
const h = harness({ ASTEROID_API_KEY: "sk_test" }, (call) =>
|
|
373
|
+
call.init.method === "PATCH" ? json({}) : json({ ...tree, rev: 2 }),
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(await run(["workflow", "pull", "w1"], h.deps)).toBe(0);
|
|
377
|
+
const head = join(h.configDir, "Fill_form", "head");
|
|
378
|
+
expect(readFileSync(join(head, "settings.yaml"), "utf8")).toBe("a: 1");
|
|
379
|
+
|
|
380
|
+
writeFileSync(join(head, "settings.yaml"), "a: 2");
|
|
381
|
+
const code = await run(["workflow", "push"], { ...h.deps, cwd: head });
|
|
382
|
+
|
|
383
|
+
expect(code).toBe(0);
|
|
384
|
+
const patch = h.calls.find((call) => call.init.method === "PATCH")!;
|
|
385
|
+
expect(patch.url).toBe(
|
|
386
|
+
"https://odyssey.asteroid.ai/agents/v2/workflows/w1/head/files",
|
|
387
|
+
);
|
|
388
|
+
expect(JSON.parse(String(patch.init.body))).toMatchObject({ baseRev: 2 });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("pushes the folder --from names, taking the workflow from its manifest", async () => {
|
|
392
|
+
const h = harness({ ASTEROID_API_KEY: "sk_test" }, (call) =>
|
|
393
|
+
call.init.method === "PATCH" ? json({}) : json({ ...tree, rev: 2 }),
|
|
394
|
+
);
|
|
395
|
+
await run(["workflow", "pull", "w9"], h.deps);
|
|
396
|
+
const head = join(h.configDir, "Fill_form", "head");
|
|
397
|
+
writeFileSync(join(head, "settings.yaml"), "a: 3");
|
|
398
|
+
|
|
399
|
+
const code = await run(["workflow", "push", "--from", head], h.deps);
|
|
400
|
+
|
|
401
|
+
expect(code).toBe(0);
|
|
402
|
+
const patch = h.calls.find((call) => call.init.method === "PATCH")!;
|
|
403
|
+
expect(patch.url).toBe(
|
|
404
|
+
"https://odyssey.asteroid.ai/agents/v2/workflows/w9/head/files",
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("workflow pull with a filter", () => {
|
|
410
|
+
it("refuses a partial tree, which would delete the files it leaves out", async () => {
|
|
411
|
+
const h = harness({ ASTEROID_API_KEY: "sk_test" }, () => json({}));
|
|
412
|
+
|
|
413
|
+
const code = await run(
|
|
414
|
+
["workflow", "pull", "w1", "--paths", "a.md"],
|
|
415
|
+
h.deps,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(code).toBe(2);
|
|
419
|
+
expect(h.calls).toEqual([]);
|
|
420
|
+
expect(h.err.join("")).toContain("cannot take --paths");
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("staff access", () => {
|
|
425
|
+
const own = { workosId: "org_OWN", id: "uuid-own", name: "Asteroid" };
|
|
426
|
+
const signIn = jwt({ exp: Date.now() / 1000 + 300, org_id: "org_OWN" });
|
|
427
|
+
const minted = {
|
|
428
|
+
token: "staff-token-1",
|
|
429
|
+
grantId: "grant-1",
|
|
430
|
+
organizationId: "uuid-acme",
|
|
431
|
+
organizationName: "Acme",
|
|
432
|
+
workosOrganizationId: "org_ACME",
|
|
433
|
+
reason: "SUP-412",
|
|
434
|
+
expiresAt: new Date(Date.now() + 8 * 3600_000).toISOString(),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
async function signedIn(
|
|
438
|
+
respond: (call: Call) => Response,
|
|
439
|
+
email = "ed@asteroid.ai",
|
|
440
|
+
) {
|
|
441
|
+
const h = harness({}, respond);
|
|
442
|
+
h.deps.env.workosApiUrl = "https://workos.test";
|
|
443
|
+
const store = new CredentialStore(h.deps.env);
|
|
444
|
+
await store.writeLogin({ refreshToken: "r1", email });
|
|
445
|
+
store.cacheToken("org_OWN", {
|
|
446
|
+
accessToken: signIn,
|
|
447
|
+
expiresAt: Date.now() + 300_000,
|
|
448
|
+
});
|
|
449
|
+
saveConfig(h.deps.env, { org: own, orgs: [own] });
|
|
450
|
+
return { ...h, store };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const auth = (call: Call) =>
|
|
454
|
+
(call.init.headers as Record<string, string>).Authorization;
|
|
455
|
+
const config = (dir: string) =>
|
|
456
|
+
JSON.parse(readFileSync(join(dir, "config.json"), "utf8"));
|
|
457
|
+
|
|
458
|
+
it("mints a token with the sign-in, then acts in the org with it", async () => {
|
|
459
|
+
const h = await signedIn((call) => {
|
|
460
|
+
if (call.url.endsWith("/users/v2/staff-tokens")) return json(minted, 201);
|
|
461
|
+
return json({ id: "w1", name: "New" }, 201);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(await run(["use", "uuid-acme", "--reason", "SUP-412"], h.deps)).toBe(
|
|
465
|
+
0,
|
|
466
|
+
);
|
|
467
|
+
const mint = h.calls[0];
|
|
468
|
+
expect(mint.init.method).toBe("POST");
|
|
469
|
+
expect(auth(mint)).toBe(`Bearer ${signIn}`);
|
|
470
|
+
expect(JSON.parse(String(mint.init.body))).toEqual({
|
|
471
|
+
organization: "uuid-acme",
|
|
472
|
+
reason: "SUP-412",
|
|
473
|
+
});
|
|
474
|
+
expect(h.err.join("")).toContain("Staff access to Acme (uuid-acme)");
|
|
475
|
+
expect(h.err.join("")).toContain("audited");
|
|
476
|
+
expect(config(h.configDir).org).toEqual({
|
|
477
|
+
workosId: "org_ACME",
|
|
478
|
+
id: "uuid-acme",
|
|
479
|
+
name: "Acme",
|
|
480
|
+
});
|
|
481
|
+
expect(config(h.configDir).orgs).toEqual([own]);
|
|
482
|
+
|
|
483
|
+
expect(await run(["workflow", "create", "--name", "New"], h.deps)).toBe(0);
|
|
484
|
+
expect(auth(h.calls.at(-1)!)).toBe("Bearer staff-token-1");
|
|
485
|
+
expect(h.err.join("")).toContain(
|
|
486
|
+
"Acting in Acme (uuid-acme) with staff access (SUP-412).",
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("asks for a reason before it mints anything", async () => {
|
|
491
|
+
const h = await signedIn(() => json(minted, 201));
|
|
492
|
+
expect(await run(["use", "uuid-acme"], h.deps)).toBe(2);
|
|
493
|
+
expect(h.err.join("")).toContain("--reason <why>");
|
|
494
|
+
expect(h.calls).toHaveLength(0);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("shows a customer no staff hint", async () => {
|
|
498
|
+
const h = await signedIn(
|
|
499
|
+
(call) =>
|
|
500
|
+
json(
|
|
501
|
+
{
|
|
502
|
+
error: "organization_membership_not_found",
|
|
503
|
+
error_description: "not a member",
|
|
504
|
+
},
|
|
505
|
+
400,
|
|
506
|
+
),
|
|
507
|
+
"pat@customer.com",
|
|
508
|
+
);
|
|
509
|
+
expect(await run(["use", "uuid-acme"], h.deps)).toBe(2);
|
|
510
|
+
expect(await run(["use", "org_ACME"], h.deps)).toBe(2);
|
|
511
|
+
expect(await run(["workflow", "list", "--org", "nope"], h.deps)).toBe(1);
|
|
512
|
+
const err = h.err.join("");
|
|
513
|
+
expect(err).toContain("you are not a member of org_ACME.");
|
|
514
|
+
expect(err).toContain("unknown organization nope.");
|
|
515
|
+
expect(err).not.toMatch(/staff|--reason|audited/);
|
|
516
|
+
expect(h.calls.some((c) => c.url.includes("staff-tokens"))).toBe(false);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("takes the staff path when the refresh says you are not a member", async () => {
|
|
520
|
+
const h = await signedIn((call) => {
|
|
521
|
+
if (call.url.includes("workos"))
|
|
522
|
+
return json(
|
|
523
|
+
{
|
|
524
|
+
error: "organization_membership_not_found",
|
|
525
|
+
error_description: "not a member",
|
|
526
|
+
},
|
|
527
|
+
400,
|
|
528
|
+
);
|
|
529
|
+
return json(minted, 201);
|
|
530
|
+
});
|
|
531
|
+
expect(await run(["use", "org_ACME"], h.deps)).toBe(2);
|
|
532
|
+
expect(h.err.join("")).toContain("you are not a member of org_ACME");
|
|
533
|
+
|
|
534
|
+
expect(await run(["use", "org_ACME", "--reason", "SUP-412"], h.deps)).toBe(
|
|
535
|
+
0,
|
|
536
|
+
);
|
|
537
|
+
expect(h.calls.at(-1)!.url).toContain("/users/v2/staff-tokens");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("tells a non-staff person that staff access is for staff", async () => {
|
|
541
|
+
const h = await signedIn(() => json({ error: "forbidden" }, 403));
|
|
542
|
+
expect(await run(["use", "uuid-acme", "--reason", "x"], h.deps)).toBe(1);
|
|
543
|
+
expect(h.err.join("")).toContain("staff access is for Asteroid staff only");
|
|
544
|
+
expect(await h.store.staffTokens()).toEqual([]);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("uses a held staff token for one command with --org", async () => {
|
|
548
|
+
const h = await signedIn(() => json({ items: [] }));
|
|
549
|
+
await h.store.saveStaffToken({
|
|
550
|
+
...minted,
|
|
551
|
+
org: { workosId: "org_ACME", id: "uuid-acme", name: "Acme" },
|
|
552
|
+
expiresAt: Date.now() + 3600_000,
|
|
553
|
+
});
|
|
554
|
+
expect(await run(["workflow", "list", "--org", "Acme"], h.deps)).toBe(0);
|
|
555
|
+
expect(auth(h.calls[0])).toBe("Bearer staff-token-1");
|
|
556
|
+
expect(await run(["workflow", "list"], h.deps)).toBe(0);
|
|
557
|
+
expect(auth(h.calls[1])).toBe(`Bearer ${signIn}`);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// resolveAuth treats a token within 30 s of its expiry as expired.
|
|
561
|
+
it("refuses a staff token in its last 30 s and says how to renew it", async () => {
|
|
562
|
+
const h = await signedIn(() => json({ items: [] }));
|
|
563
|
+
await h.store.saveStaffToken({
|
|
564
|
+
...minted,
|
|
565
|
+
org: { workosId: "org_ACME", id: "uuid-acme", name: "Acme" },
|
|
566
|
+
expiresAt: Date.now() + 10_000,
|
|
567
|
+
});
|
|
568
|
+
expect(await run(["workflow", "list", "--org", "org_ACME"], h.deps)).toBe(
|
|
569
|
+
1,
|
|
570
|
+
);
|
|
571
|
+
expect(h.err.join("")).toContain("has expired");
|
|
572
|
+
expect(h.calls).toHaveLength(0);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it("revokes every staff token on --exit and goes back to your org", async () => {
|
|
576
|
+
const h = await signedIn((call) => {
|
|
577
|
+
if (call.url.endsWith("/users/v2/staff-tokens")) return json(minted, 201);
|
|
578
|
+
return new Response(null, { status: 204 });
|
|
579
|
+
});
|
|
580
|
+
expect(await run(["use", "uuid-acme", "--reason", "SUP-412"], h.deps)).toBe(
|
|
581
|
+
0,
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
expect(await run(["use", "--exit"], h.deps)).toBe(0);
|
|
585
|
+
const revoke = h.calls.at(-1)!;
|
|
586
|
+
expect(revoke.init.method).toBe("DELETE");
|
|
587
|
+
expect(revoke.url).toBe(
|
|
588
|
+
"https://odyssey.asteroid.ai/users/v2/staff-tokens/grant-1",
|
|
589
|
+
);
|
|
590
|
+
expect(auth(revoke)).toBe(`Bearer ${signIn}`);
|
|
591
|
+
expect(await h.store.staffTokens()).toEqual([]);
|
|
592
|
+
expect(config(h.configDir).org).toEqual(own);
|
|
593
|
+
expect(h.err.join("")).toContain("Now using Asteroid (uuid-own)");
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("keeps a staff token on --exit when the revoke fails", async () => {
|
|
597
|
+
const h = await signedIn(() => json({ message: "down" }, 500));
|
|
598
|
+
await h.store.saveStaffToken({
|
|
599
|
+
...minted,
|
|
600
|
+
org: { workosId: "org_ACME", id: "uuid-acme", name: "Acme" },
|
|
601
|
+
expiresAt: Date.now() + 3600_000,
|
|
602
|
+
});
|
|
603
|
+
expect(await run(["use", "--exit"], h.deps)).toBe(1);
|
|
604
|
+
expect(await h.store.staffTokens()).toHaveLength(1);
|
|
605
|
+
expect(h.err.join("")).toContain("Run `asteroid use --exit` again");
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it("revokes a minted token it could not save", async () => {
|
|
609
|
+
const h = await signedIn((call) => {
|
|
610
|
+
if (call.url.endsWith("/users/v2/staff-tokens")) return json(minted, 201);
|
|
611
|
+
return new Response(null, { status: 204 });
|
|
612
|
+
});
|
|
613
|
+
const save = vi
|
|
614
|
+
.spyOn(CredentialStore.prototype, "saveStaffToken")
|
|
615
|
+
.mockRejectedValue(new WorkosError("keychain", "keychain locked"));
|
|
616
|
+
try {
|
|
617
|
+
expect(
|
|
618
|
+
await run(["use", "uuid-acme", "--reason", "SUP-412"], h.deps),
|
|
619
|
+
).toBe(1);
|
|
620
|
+
} finally {
|
|
621
|
+
save.mockRestore();
|
|
622
|
+
}
|
|
623
|
+
const revoke = h.calls.at(-1)!;
|
|
624
|
+
expect(revoke.init.method).toBe("DELETE");
|
|
625
|
+
expect(revoke.url).toContain("/users/v2/staff-tokens/grant-1");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it("refuses an option as the reason", async () => {
|
|
629
|
+
const h = await signedIn(() => json(minted, 201));
|
|
630
|
+
expect(await run(["use", "uuid-acme", "--reason", "--exit"], h.deps)).toBe(
|
|
631
|
+
2,
|
|
632
|
+
);
|
|
633
|
+
expect(h.calls).toHaveLength(0);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("rejects an org with --exit", async () => {
|
|
637
|
+
const h = await signedIn(() => json({}));
|
|
638
|
+
expect(await run(["use", "uuid-acme", "--exit"], h.deps)).toBe(2);
|
|
639
|
+
expect(h.calls).toHaveLength(0);
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
359
643
|
describe("reference", () => {
|
|
360
644
|
it("matches the generated agent-docs page", () => {
|
|
361
645
|
const page = readFileSync(
|
package/src/run.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { clean, render } from "./output";
|
|
|
17
17
|
import { parseInvocation } from "./program";
|
|
18
18
|
import { BUILTINS } from "./reference";
|
|
19
19
|
import { buildRequest } from "./request";
|
|
20
|
-
import { runSync } from "./sync/run";
|
|
20
|
+
import { runsLocally, runSync } from "./sync/run";
|
|
21
21
|
import type { Remote } from "./sync/types";
|
|
22
22
|
import type { CliCommand } from "./types";
|
|
23
23
|
import type { Ui } from "./ui";
|
|
@@ -62,7 +62,10 @@ async function runBuiltin(
|
|
|
62
62
|
"run `asteroid complete <shell>` to print a completion script",
|
|
63
63
|
);
|
|
64
64
|
case "use":
|
|
65
|
-
return use(deps, parsed.positionals[0]
|
|
65
|
+
return use(deps, parsed.positionals[0], {
|
|
66
|
+
reason: parsed.flags.get("reason")?.[0],
|
|
67
|
+
exit: parsed.flags.has("exit"),
|
|
68
|
+
});
|
|
66
69
|
default:
|
|
67
70
|
return whoami(deps, parsed.org, parsed.json);
|
|
68
71
|
}
|
|
@@ -103,7 +106,10 @@ function announce(command: CliCommand, auth: Auth, deps: Deps) {
|
|
|
103
106
|
: auth.org
|
|
104
107
|
? orgLabel(auth.org)
|
|
105
108
|
: "your default organization";
|
|
106
|
-
|
|
109
|
+
const staff = auth.staff
|
|
110
|
+
? ` with staff access (${clean(auth.staff.reason)})`
|
|
111
|
+
: "";
|
|
112
|
+
deps.stderr(`Acting in ${org}${staff}.\n`);
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
async function runCommand(
|
|
@@ -189,6 +195,10 @@ export async function run(argv: string[], deps: Deps): Promise<number> {
|
|
|
189
195
|
builtins: BUILTINS,
|
|
190
196
|
version: VERSION,
|
|
191
197
|
description: DESCRIPTION,
|
|
198
|
+
defaultArg: (command, arg) =>
|
|
199
|
+
command.files === "push" && arg.name === "workflowId" ? "" : undefined,
|
|
200
|
+
defaultArgNote:
|
|
201
|
+
"<workflow-id> defaults to the workflow the pushed folder was pulled from.",
|
|
192
202
|
});
|
|
193
203
|
if (invocation.kind === "exit") {
|
|
194
204
|
if (invocation.stdout) deps.stdout(invocation.stdout);
|
|
@@ -198,7 +208,7 @@ export async function run(argv: string[], deps: Deps): Promise<number> {
|
|
|
198
208
|
if (invocation.kind === "builtin") {
|
|
199
209
|
return await runBuiltin(invocation.name, invocation.parsed, deps);
|
|
200
210
|
}
|
|
201
|
-
if (invocation.command.
|
|
211
|
+
if (runsLocally(invocation.command, invocation.parsed)) {
|
|
202
212
|
const auth = await authFor(invocation.parsed, deps);
|
|
203
213
|
announce(invocation.command, auth, deps);
|
|
204
214
|
return await runSync(invocation.command, invocation.parsed, {
|
package/src/sync/commands.ts
CHANGED
|
@@ -9,12 +9,6 @@ function option(
|
|
|
9
9
|
return { name, flag: name, type, required: false, description, ...extra };
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const DIR = option(
|
|
13
|
-
"dir",
|
|
14
|
-
"string",
|
|
15
|
-
"Write here instead of `<workflow-name>/...` under the current directory.",
|
|
16
|
-
);
|
|
17
|
-
|
|
18
12
|
const local = {
|
|
19
13
|
method: "GET",
|
|
20
14
|
path: "",
|
|
@@ -24,48 +18,6 @@ const local = {
|
|
|
24
18
|
} as const;
|
|
25
19
|
|
|
26
20
|
export const SYNC_COMMANDS: CliCommand[] = [
|
|
27
|
-
{
|
|
28
|
-
...local,
|
|
29
|
-
command: "workflow pull",
|
|
30
|
-
tool: "workflow_pull",
|
|
31
|
-
title: "Pull a workflow to disk",
|
|
32
|
-
description:
|
|
33
|
-
"Write a workflow's editable head to `<workflow-name>/head/` as files: settings, per-node instructions and scripts, and the workflow's own files. Pass --version-id for a pinned version, written read-only to `<workflow-name>/.versions/`. Edit the head files, then run `asteroid workflow push` from that folder.",
|
|
34
|
-
readOnly: true,
|
|
35
|
-
args: [
|
|
36
|
-
{
|
|
37
|
-
name: "workflowId",
|
|
38
|
-
flag: "workflow-id",
|
|
39
|
-
type: "string",
|
|
40
|
-
required: true,
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
query: [
|
|
44
|
-
option(
|
|
45
|
-
"version-id",
|
|
46
|
-
"string",
|
|
47
|
-
"A workflow version ID. Omit for the editable head.",
|
|
48
|
-
),
|
|
49
|
-
DIR,
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
...local,
|
|
54
|
-
command: "workflow push",
|
|
55
|
-
tool: "workflow_push",
|
|
56
|
-
title: "Push local edits to a workflow head",
|
|
57
|
-
description:
|
|
58
|
-
"Send the edits in a pulled head folder to the workflow's editable head: changed and new files are written, removed files are deleted. Fails if the head changed since the pull; pull again and reapply. Push does not publish.",
|
|
59
|
-
readOnly: false,
|
|
60
|
-
args: [],
|
|
61
|
-
query: [
|
|
62
|
-
option(
|
|
63
|
-
"dir",
|
|
64
|
-
"string",
|
|
65
|
-
"The pulled head folder. Defaults to the current directory.",
|
|
66
|
-
),
|
|
67
|
-
],
|
|
68
|
-
},
|
|
69
21
|
{
|
|
70
22
|
...local,
|
|
71
23
|
command: "execution pull",
|
package/src/sync/run.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Parsed, UsageError } from "../args";
|
|
2
|
+
import { buildRequest, type InputReader } from "../request";
|
|
2
3
|
import type { CliCommand } from "../types";
|
|
3
4
|
import {
|
|
4
5
|
type ExecutionPullOptions,
|
|
@@ -22,6 +23,7 @@ export interface SyncDeps {
|
|
|
22
23
|
files?: LocalFiles;
|
|
23
24
|
stdout: (text: string) => void;
|
|
24
25
|
root?: string;
|
|
26
|
+
io?: InputReader;
|
|
25
27
|
workflowPull?: Partial<WorkflowPullOptions>;
|
|
26
28
|
executionPull?: Partial<ExecutionPullOptions>;
|
|
27
29
|
}
|
|
@@ -52,39 +54,71 @@ function executionOptions(parsed: Parsed): Omit<ExecutionPullOptions, "root"> {
|
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function withoutFlag(parsed: Parsed, name: string): Parsed {
|
|
58
|
+
const flags = new Map(parsed.flags);
|
|
59
|
+
flags.delete(name);
|
|
60
|
+
return { ...parsed, flags };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function runsLocally(command: CliCommand, parsed: Parsed): boolean {
|
|
64
|
+
if (command.local) return true;
|
|
65
|
+
if (command.files === "pull") return !parsed.json;
|
|
66
|
+
if (command.files === "push") return parsed.body === undefined;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const PARTIAL = ["paths", "variants"];
|
|
71
|
+
|
|
72
|
+
function rejectPartial(parsed: Parsed) {
|
|
73
|
+
const used = PARTIAL.filter((name) => parsed.flags.has(name));
|
|
74
|
+
if (used.length === 0) return;
|
|
75
|
+
throw new UsageError(
|
|
76
|
+
`pull writes the whole tree, so it cannot take ${used.map((name) => `--${name}`).join(" or ")}. Add --json to list those files instead.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function pull(
|
|
81
|
+
command: CliCommand,
|
|
82
|
+
parsed: Parsed,
|
|
83
|
+
sync: Sync,
|
|
84
|
+
deps: SyncDeps,
|
|
85
|
+
) {
|
|
86
|
+
rejectPartial(parsed);
|
|
87
|
+
const result = await pullWorkflow(sync, {
|
|
88
|
+
workflowId: parsed.positionals[0],
|
|
89
|
+
request: await buildRequest(command, withoutFlag(parsed, "out"), {
|
|
90
|
+
io: deps.io,
|
|
91
|
+
}),
|
|
92
|
+
dir: flag(parsed, "out"),
|
|
93
|
+
root: deps.root ?? "",
|
|
94
|
+
...deps.workflowPull,
|
|
95
|
+
});
|
|
96
|
+
return { result, text: formatWorkflowPull(result) };
|
|
97
|
+
}
|
|
98
|
+
|
|
55
99
|
async function execute(
|
|
56
100
|
command: CliCommand,
|
|
57
101
|
parsed: Parsed,
|
|
58
102
|
sync: Sync,
|
|
59
103
|
deps: SyncDeps,
|
|
60
104
|
) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
...deps.workflowPull,
|
|
70
|
-
});
|
|
71
|
-
return { result, text: formatWorkflowPull(result) };
|
|
72
|
-
}
|
|
73
|
-
case "workflow push": {
|
|
74
|
-
const result = await pushWorkflow(sync, flag(parsed, "dir") ?? ".");
|
|
75
|
-
return { result, text: formatWorkflowPush(result) };
|
|
76
|
-
}
|
|
77
|
-
case "execution pull": {
|
|
78
|
-
const result = await pullExecution(sync, {
|
|
79
|
-
...executionOptions(parsed),
|
|
80
|
-
root,
|
|
81
|
-
...deps.executionPull,
|
|
82
|
-
});
|
|
83
|
-
return { result, text: formatExecutionPull(result) };
|
|
84
|
-
}
|
|
85
|
-
default:
|
|
86
|
-
throw new UsageError(`unknown command ${command.command}`);
|
|
105
|
+
if (command.files === "pull") return pull(command, parsed, sync, deps);
|
|
106
|
+
if (command.files === "push") {
|
|
107
|
+
const result = await pushWorkflow(
|
|
108
|
+
sync,
|
|
109
|
+
flag(parsed, "from") ?? ".",
|
|
110
|
+
parsed.positionals[0] || undefined,
|
|
111
|
+
);
|
|
112
|
+
return { result, text: formatWorkflowPush(result) };
|
|
87
113
|
}
|
|
114
|
+
if (command.command !== "execution pull")
|
|
115
|
+
throw new UsageError(`unknown command ${command.command}`);
|
|
116
|
+
const result = await pullExecution(sync, {
|
|
117
|
+
...executionOptions(parsed),
|
|
118
|
+
root: deps.root ?? "",
|
|
119
|
+
...deps.executionPull,
|
|
120
|
+
});
|
|
121
|
+
return { result, text: formatExecutionPull(result) };
|
|
88
122
|
}
|
|
89
123
|
|
|
90
124
|
export async function runSync(
|