@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.
@@ -6,6 +6,7 @@ import { formatExecutionPull, pullExecution } from "./execution-pull";
6
6
  import { sha256 } from "./paths";
7
7
  import {
8
8
  type FileTree,
9
+ get,
9
10
  type LocalFiles,
10
11
  MANIFEST,
11
12
  type Manifest,
@@ -82,8 +83,16 @@ function tree(overrides: Partial<FileTree> = {}): FileTree {
82
83
  }
83
84
 
84
85
  const headFiles = `GET /agents/v2/workflows/${WF}/head/files`;
86
+ const headRequest = () => get(`/agents/v2/workflows/${WF}/head/files`);
85
87
  const signedBig = `GET /agents/v2/workflows/${WF}/versions/${HEAD}/files/f-big/signed-url`;
86
88
 
89
+ function manifestOf(
90
+ local: ReturnType<typeof memoryFiles>,
91
+ dir: string,
92
+ ): Manifest {
93
+ return JSON.parse(local.text(`${dir}/${MANIFEST}`)!) as Manifest;
94
+ }
95
+
87
96
  describe("pullWorkflow", () => {
88
97
  function pulled(initial: Record<string, string> = {}) {
89
98
  const local = memoryFiles(initial);
@@ -95,10 +104,14 @@ describe("pullWorkflow", () => {
95
104
  return { sync, local, calls, downloads };
96
105
  }
97
106
 
98
- it("writes the head under <workflow-name>/head with every file's bytes", async () => {
107
+ it("writes every file's bytes into the folder", async () => {
99
108
  const { sync, local, downloads } = pulled();
100
109
 
101
- const result = await pullWorkflow(sync, { workflowId: WF, root: "out" });
110
+ const result = await pullWorkflow(sync, {
111
+ workflowId: WF,
112
+ request: headRequest(),
113
+ dir: "out/Fill_Form/head",
114
+ });
102
115
 
103
116
  expect(result.dir).toBe("out/Fill_Form/head");
104
117
  expect(local.text("out/Fill_Form/head/settings.yaml")).toBe(
@@ -114,7 +127,11 @@ describe("pullWorkflow", () => {
114
127
  it("asks for every file's contents", async () => {
115
128
  const { sync, calls } = pulled();
116
129
 
117
- await pullWorkflow(sync, { workflowId: WF, root: "" });
130
+ await pullWorkflow(sync, {
131
+ workflowId: WF,
132
+ request: headRequest(),
133
+ dir: "Fill_Form/head",
134
+ });
118
135
 
119
136
  expect(calls[0].query.get("contents")).toBe("all");
120
137
  });
@@ -122,7 +139,11 @@ describe("pullWorkflow", () => {
122
139
  it("records the rev and each file's hash for a later push", async () => {
123
140
  const { sync, local } = pulled();
124
141
 
125
- await pullWorkflow(sync, { workflowId: WF, root: "" });
142
+ await pullWorkflow(sync, {
143
+ workflowId: WF,
144
+ request: headRequest(),
145
+ dir: "Fill_Form/head",
146
+ });
126
147
 
127
148
  const manifest = JSON.parse(
128
149
  local.text(`Fill_Form/head/${MANIFEST}`)!,
@@ -141,7 +162,11 @@ describe("pullWorkflow", () => {
141
162
  "Fill_Form/head/nodes/Old/instructions.md": "gone",
142
163
  });
143
164
 
144
- await pullWorkflow(sync, { workflowId: WF, root: "" });
165
+ await pullWorkflow(sync, {
166
+ workflowId: WF,
167
+ request: headRequest(),
168
+ dir: "Fill_Form/head",
169
+ });
145
170
 
146
171
  expect(local.store.has("Fill_Form/head/nodes/Old/instructions.md")).toBe(
147
172
  false,
@@ -153,7 +178,8 @@ describe("pullWorkflow", () => {
153
178
 
154
179
  const result = await pullWorkflow(sync, {
155
180
  workflowId: WF,
156
- root: "",
181
+ request: headRequest(),
182
+ dir: "Fill_Form/head",
157
183
  maxFileBytes: 1000,
158
184
  });
159
185
 
@@ -164,7 +190,7 @@ describe("pullWorkflow", () => {
164
190
  ]);
165
191
  });
166
192
 
167
- it("writes a pinned version under .versions/", async () => {
193
+ it("marks a pinned version read-only in its manifest", async () => {
168
194
  const local = memoryFiles();
169
195
  const { remote } = fakeRemote({
170
196
  [`GET /agents/v2/workflows/${WF}/versions/${HEAD}/files`]: () =>
@@ -173,10 +199,14 @@ describe("pullWorkflow", () => {
173
199
 
174
200
  const result = await pullWorkflow(
175
201
  { remote, files: local.files },
176
- { workflowId: WF, versionId: HEAD, root: "" },
202
+ {
203
+ workflowId: WF,
204
+ request: get(`/agents/v2/workflows/${WF}/versions/${HEAD}/files`),
205
+ dir: "v3",
206
+ },
177
207
  );
178
208
 
179
- expect(result.dir).toBe("Fill_Form/.versions/v3_22222222");
209
+ expect(manifestOf(local, "v3").head).toBe(false);
180
210
  });
181
211
 
182
212
  it("refuses an entry whose path leaves the folder", async () => {
@@ -192,7 +222,7 @@ describe("pullWorkflow", () => {
192
222
 
193
223
  const result = await pullWorkflow(
194
224
  { remote, files: local.files },
195
- { workflowId: WF, root: "" },
225
+ { workflowId: WF, request: headRequest(), dir: "Fill_Form/head" },
196
226
  );
197
227
 
198
228
  expect(result.skipped).toEqual(["../escape (unsafe path)"]);
@@ -243,7 +273,7 @@ describe("pushWorkflow", () => {
243
273
  [headFiles]: () => tree({ rev: 8, files: [] }),
244
274
  });
245
275
 
246
- const result = await pushWorkflow({ remote, files: local.files }, dir);
276
+ const result = await pushWorkflow({ remote, files: local.files }, dir, WF);
247
277
 
248
278
  expect(patched).toEqual({
249
279
  baseRev: 7,
@@ -283,7 +313,7 @@ describe("pushWorkflow", () => {
283
313
  }),
284
314
  });
285
315
 
286
- await pushWorkflow({ remote, files: local.files }, dir);
316
+ await pushWorkflow({ remote, files: local.files }, dir, WF);
287
317
 
288
318
  expect(local.text(`${dir}/settings.yaml`)).toBe("start: Login\n");
289
319
  const manifest = JSON.parse(local.text(`${dir}/${MANIFEST}`)!) as Manifest;
@@ -297,7 +327,7 @@ describe("pushWorkflow", () => {
297
327
  const local = pulledHead({});
298
328
  const { remote, calls } = fakeRemote({});
299
329
 
300
- const result = await pushWorkflow({ remote, files: local.files }, dir);
330
+ const result = await pushWorkflow({ remote, files: local.files }, dir, WF);
301
331
 
302
332
  expect(calls).toEqual([]);
303
333
  expect(result).toMatchObject({ written: [], deleted: [], rev: 7 });
@@ -311,7 +341,7 @@ describe("pushWorkflow", () => {
311
341
  },
312
342
  });
313
343
 
314
- const push = pushWorkflow({ remote, files: local.files }, dir);
344
+ const push = pushWorkflow({ remote, files: local.files }, dir, WF);
315
345
 
316
346
  await expect(push).rejects.toThrow(UsageError);
317
347
  await expect(push).rejects.toThrow("changed since your pull");
@@ -320,11 +350,20 @@ describe("pushWorkflow", () => {
320
350
  it("refuses a folder with no pull in it", async () => {
321
351
  const { remote } = fakeRemote({});
322
352
 
323
- const push = pushWorkflow({ remote, files: memoryFiles().files }, dir);
353
+ const push = pushWorkflow({ remote, files: memoryFiles().files }, dir, WF);
324
354
 
325
355
  await expect(push).rejects.toThrow("no pulled workflow");
326
356
  });
327
357
 
358
+ it("refuses a folder pulled from another workflow", async () => {
359
+ const local = pulledHead({});
360
+ const { remote } = fakeRemote({});
361
+
362
+ const push = pushWorkflow({ remote, files: local.files }, dir, "other");
363
+
364
+ await expect(push).rejects.toThrow(`holds workflow ${WF}, not other`);
365
+ });
366
+
328
367
  it("refuses a pinned version", async () => {
329
368
  const manifest = {
330
369
  workflowId: WF,
@@ -338,7 +377,7 @@ describe("pushWorkflow", () => {
338
377
  });
339
378
  const { remote } = fakeRemote({});
340
379
 
341
- const push = pushWorkflow({ remote, files: local.files }, dir);
380
+ const push = pushWorkflow({ remote, files: local.files }, dir, WF);
342
381
 
343
382
  await expect(push).rejects.toThrow("read-only");
344
383
  });
@@ -1,3 +1,4 @@
1
+ import type { BuiltRequest } from "../request";
1
2
  import {
2
3
  isContained,
3
4
  join,
@@ -18,9 +19,9 @@ const CONCURRENCY = 16;
18
19
 
19
20
  export interface WorkflowPullOptions {
20
21
  workflowId: string;
21
- versionId?: string;
22
- root: string;
22
+ request: BuiltRequest;
23
23
  dir?: string;
24
+ root?: string;
24
25
  manifest?: boolean;
25
26
  maxFileBytes?: number;
26
27
  }
@@ -32,24 +33,16 @@ export interface WorkflowPullResult {
32
33
  version?: number;
33
34
  rev: number;
34
35
  dir: string;
36
+ head: boolean;
35
37
  written: number;
36
38
  shared: number;
37
39
  skipped: string[];
38
40
  }
39
41
 
40
- function treeRequest(options: WorkflowPullOptions) {
41
- const base = `/agents/v2/workflows/${options.workflowId}`;
42
- const path = options.versionId
43
- ? `${base}/versions/${options.versionId}/files`
44
- : `${base}/head/files`;
45
- return get(path, { contents: "all" });
46
- }
47
-
48
- export function pullDir(options: WorkflowPullOptions, tree: FileTree): string {
49
- if (options.dir) return options.dir;
50
- const root = workflowRoot(options.root, tree.workflowName);
51
- if (!options.versionId) return join(root, "head");
52
- return join(root, ".versions", workflowDirName(tree.versionId, tree.version));
42
+ export function pullDir(root: string, tree: FileTree, head: boolean): string {
43
+ const base = workflowRoot(root, tree.workflowName);
44
+ if (head) return join(base, "head");
45
+ return join(base, ".versions", workflowDirName(tree.versionId, tree.version));
53
46
  }
54
47
 
55
48
  async function agentFileBytes(
@@ -129,8 +122,14 @@ export async function pullWorkflow(
129
122
  sync: Sync,
130
123
  options: WorkflowPullOptions,
131
124
  ): Promise<WorkflowPullResult> {
132
- const tree = (await sync.remote.json(treeRequest(options))) as FileTree;
133
- const dir = pullDir(options, tree);
125
+ const request = {
126
+ ...options.request,
127
+ query: new URLSearchParams(options.request.query),
128
+ };
129
+ request.query.set("contents", "all");
130
+ const tree = (await sync.remote.json(request)) as FileTree;
131
+ const head = request.path.endsWith("/head/files");
132
+ const dir = options.dir ?? pullDir(options.root ?? "", tree, head);
134
133
  const { hashes, skipped, shared } = await writeEntries(
135
134
  sync,
136
135
  options,
@@ -142,7 +141,7 @@ export async function pullWorkflow(
142
141
  await writeManifest(sync, dir, {
143
142
  workflowId: options.workflowId,
144
143
  versionId: tree.versionId,
145
- head: !options.versionId,
144
+ head,
146
145
  rev: tree.rev,
147
146
  files: hashes,
148
147
  });
@@ -154,6 +153,7 @@ export async function pullWorkflow(
154
153
  version: tree.version,
155
154
  rev: tree.rev,
156
155
  dir,
156
+ head,
157
157
  written: Object.keys(hashes).length,
158
158
  shared,
159
159
  skipped,
@@ -176,6 +176,9 @@ export function formatWorkflowPull(result: WorkflowPullResult): string {
176
176
  result.skipped.length > 0
177
177
  ? `Skipped:\n${result.skipped.map((s) => ` - ${s}`).join("\n")}`
178
178
  : null,
179
+ result.head
180
+ ? `Edit the files, then run \`asteroid workflow push\` from ${result.dir}/.`
181
+ : "This is a pinned version. It is read-only.",
179
182
  ]
180
183
  .filter(Boolean)
181
184
  .join("\n");
@@ -115,8 +115,14 @@ async function refresh(
115
115
  export async function pushWorkflow(
116
116
  sync: Sync,
117
117
  dir: string,
118
+ workflowId?: string,
118
119
  ): Promise<WorkflowPushResult> {
119
120
  const manifest = await readManifest(sync, dir);
121
+ if (workflowId !== undefined && manifest.workflowId !== workflowId) {
122
+ throw new UsageError(
123
+ `${dir} holds workflow ${manifest.workflowId}, not ${workflowId}.`,
124
+ );
125
+ }
120
126
  const { writes, deletes } = await localChanges(sync, dir, manifest);
121
127
  const result = {
122
128
  workflowId: manifest.workflowId,
package/src/types.ts CHANGED
@@ -41,4 +41,6 @@ export interface CliCommand {
41
41
  readOnly: boolean;
42
42
  destructive: boolean;
43
43
  local?: boolean;
44
+ files?: "pull" | "push";
45
+ capability?: "publish" | "run_execution";
44
46
  }
package/src/ui.ts CHANGED
@@ -7,6 +7,7 @@ export interface Choice {
7
7
 
8
8
  export interface Ui {
9
9
  select(message: string, choices: Choice[]): Promise<string | undefined>;
10
+ text(message: string): Promise<string | undefined>;
10
11
  wait<T>(message: string, done: string, work: Promise<T>): Promise<T>;
11
12
  }
12
13
 
@@ -15,6 +16,10 @@ export const clackUi: Ui = {
15
16
  const value = await clack.select({ message, options: choices });
16
17
  return clack.isCancel(value) ? undefined : value;
17
18
  },
19
+ async text(message) {
20
+ const value = await clack.text({ message });
21
+ return clack.isCancel(value) ? undefined : value;
22
+ },
18
23
  async wait(message, done, work) {
19
24
  const spinner = clack.spinner();
20
25
  spinner.start(message);