@asteroidai/cli 1.9.11 → 1.9.12
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 +284 -271
- package/dist/core.js +273 -268
- package/package.json +1 -1
- package/src/generated/commands.ts +199 -182
- package/src/program.ts +10 -0
- package/src/reference.ts +13 -1
- package/src/run.test.ts +68 -4
- package/src/run.ts +6 -2
- 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/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(
|
package/src/sync/sync.test.ts
CHANGED
|
@@ -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
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
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("
|
|
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
|
-
{
|
|
202
|
+
{
|
|
203
|
+
workflowId: WF,
|
|
204
|
+
request: get(`/agents/v2/workflows/${WF}/versions/${HEAD}/files`),
|
|
205
|
+
dir: "v3",
|
|
206
|
+
},
|
|
177
207
|
);
|
|
178
208
|
|
|
179
|
-
expect(
|
|
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,
|
|
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
|
-
|
|
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
|
|
41
|
-
const base =
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
133
|
-
|
|
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
|
|
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,
|