@asteroidai/cli 1.9.4 → 1.9.11
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 +1127 -307
- package/dist/core.js +861 -52
- package/package.json +3 -2
- package/src/commands.ts +13 -0
- package/src/core.ts +31 -0
- package/src/main.ts +2 -2
- package/src/program.test.ts +19 -0
- package/src/program.ts +15 -1
- package/src/run.test.ts +35 -2
- package/src/run.ts +53 -19
- package/src/sync/activities.ts +80 -0
- package/src/sync/commands.ts +111 -0
- package/src/sync/execution-pull.ts +273 -0
- package/src/sync/node-files.ts +47 -0
- package/src/sync/paths.ts +45 -0
- package/src/sync/run.ts +102 -0
- package/src/sync/sync.test.ts +463 -0
- package/src/sync/types.ts +58 -0
- package/src/sync/workflow-pull.ts +182 -0
- package/src/sync/workflow-push.ts +155 -0
- package/src/types.ts +2 -0
package/dist/core.js
CHANGED
|
@@ -4555,16 +4555,16 @@ function isScalar(value2) {
|
|
|
4555
4555
|
return value2 === null || ["string", "number", "boolean"].includes(typeof value2);
|
|
4556
4556
|
}
|
|
4557
4557
|
function indent(text2) {
|
|
4558
|
-
return text2.split("\n").map((
|
|
4558
|
+
return text2.split("\n").map((line2) => ` ${line2}`).join("\n");
|
|
4559
4559
|
}
|
|
4560
4560
|
function text(value2) {
|
|
4561
4561
|
return clean(isScalar(value2) ? String(value2) : JSON.stringify(value2));
|
|
4562
4562
|
}
|
|
4563
4563
|
function cell(value2, options = {}) {
|
|
4564
4564
|
if (value2 === null || value2 === void 0) return "";
|
|
4565
|
-
const
|
|
4566
|
-
if (options.full ||
|
|
4567
|
-
return `${
|
|
4565
|
+
const line2 = text(value2).replace(/\s+/g, " ");
|
|
4566
|
+
if (options.full || line2.length <= MAX_CELL) return line2;
|
|
4567
|
+
return `${line2.slice(0, MAX_CELL - 1)}\u2026`;
|
|
4568
4568
|
}
|
|
4569
4569
|
function columns(rows, options) {
|
|
4570
4570
|
const scalarKeys = [];
|
|
@@ -4612,8 +4612,8 @@ function table(input, options = {}) {
|
|
|
4612
4612
|
const widths = keys.map(
|
|
4613
4613
|
(key, i) => Math.max(key.length, ...cells.map((r) => r[i].length))
|
|
4614
4614
|
);
|
|
4615
|
-
const
|
|
4616
|
-
return [
|
|
4615
|
+
const line2 = (values) => values.map((v, i) => v.padEnd(widths[i])).join(" ").trimEnd();
|
|
4616
|
+
return [line2(keys.map((k) => k.toUpperCase())), ...cells.map(line2)].join(
|
|
4617
4617
|
"\n"
|
|
4618
4618
|
);
|
|
4619
4619
|
}
|
|
@@ -4727,12 +4727,12 @@ var DOTTED = /^--([a-z0-9-]+\.[a-z0-9.-]+)(?:=(.*))?$/;
|
|
|
4727
4727
|
function collect(value2, previous) {
|
|
4728
4728
|
return [...previous ?? [], value2];
|
|
4729
4729
|
}
|
|
4730
|
-
function optionText(
|
|
4731
|
-
const text2 =
|
|
4730
|
+
function optionText(option2) {
|
|
4731
|
+
const text2 = option2.description?.replace(/\s+/g, " ").trim() ?? "";
|
|
4732
4732
|
const parts = [text2 && !/[.!?]$/.test(text2) ? `${text2}.` : text2];
|
|
4733
|
-
if (
|
|
4734
|
-
if (
|
|
4735
|
-
if (
|
|
4733
|
+
if (option2.enum) parts.push(`One of: ${option2.enum.join(", ")}.`);
|
|
4734
|
+
if (option2.type.endsWith("[]")) parts.push("Repeatable.");
|
|
4735
|
+
if (option2.required) parts.push("Required.");
|
|
4736
4736
|
return parts.filter(Boolean).join(" ");
|
|
4737
4737
|
}
|
|
4738
4738
|
function splitDotted(argv) {
|
|
@@ -4759,18 +4759,20 @@ function group(parent, name, groups, path) {
|
|
|
4759
4759
|
return created;
|
|
4760
4760
|
}
|
|
4761
4761
|
function leafOptions(leaf, command) {
|
|
4762
|
-
for (const
|
|
4763
|
-
if (
|
|
4764
|
-
leaf.option(`--${
|
|
4765
|
-
leaf.option(`--no-${
|
|
4762
|
+
for (const option2 of [...command.query, ...command.body?.fields ?? []]) {
|
|
4763
|
+
if (option2.type === "boolean") {
|
|
4764
|
+
leaf.option(`--${option2.flag}`, optionText(option2));
|
|
4765
|
+
leaf.option(`--no-${option2.flag}`, `Set ${option2.flag} to false.`);
|
|
4766
4766
|
continue;
|
|
4767
4767
|
}
|
|
4768
|
-
const placeholder =
|
|
4768
|
+
const placeholder = option2.enum ? option2.enum.join("|") : option2.type.replace("[]", "");
|
|
4769
4769
|
leaf.option(
|
|
4770
|
-
`--${
|
|
4771
|
-
optionText(
|
|
4770
|
+
`--${option2.flag} <${placeholder}>`,
|
|
4771
|
+
optionText(option2),
|
|
4772
4772
|
collect
|
|
4773
4773
|
);
|
|
4774
|
+
if (option2.negatable)
|
|
4775
|
+
leaf.option(`--no-${option2.flag}`, `Skip ${option2.flag}.`);
|
|
4774
4776
|
}
|
|
4775
4777
|
for (const file of command.body?.files ?? []) {
|
|
4776
4778
|
leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
|
|
@@ -4780,11 +4782,11 @@ function toParsed(leaf, positionals, dotted) {
|
|
|
4780
4782
|
const globals = leaf.optsWithGlobals();
|
|
4781
4783
|
const own = leaf.opts();
|
|
4782
4784
|
const flags = new Map(dotted);
|
|
4783
|
-
for (const
|
|
4784
|
-
const
|
|
4785
|
-
const value2 = own[
|
|
4786
|
-
if (!
|
|
4787
|
-
flags.set(
|
|
4785
|
+
for (const option2 of leaf.options) {
|
|
4786
|
+
const flag2 = option2.long?.replace(/^--(no-)?/, "");
|
|
4787
|
+
const value2 = own[option2.attributeName()];
|
|
4788
|
+
if (!flag2 || value2 === void 0 || flags.has(flag2)) continue;
|
|
4789
|
+
flags.set(flag2, Array.isArray(value2) ? value2.map(String) : [String(value2)]);
|
|
4788
4790
|
}
|
|
4789
4791
|
return {
|
|
4790
4792
|
positionals,
|
|
@@ -4797,7 +4799,11 @@ function toParsed(leaf, positionals, dotted) {
|
|
|
4797
4799
|
}
|
|
4798
4800
|
function defineCommands(root, options, dotted = /* @__PURE__ */ new Map(), onInvocation = () => void 0) {
|
|
4799
4801
|
if (options.description) root.description(options.description);
|
|
4800
|
-
if (options.version)
|
|
4802
|
+
if (options.version)
|
|
4803
|
+
root.addHelpText(
|
|
4804
|
+
"after",
|
|
4805
|
+
"\nRun `asteroid --version` to print the CLI version."
|
|
4806
|
+
);
|
|
4801
4807
|
for (const [flags, text2] of GLOBAL_FLAGS) {
|
|
4802
4808
|
if (!flags.startsWith("-h")) root.option(flags, text2);
|
|
4803
4809
|
}
|
|
@@ -4859,8 +4865,8 @@ ${notes.join("\n")}`);
|
|
|
4859
4865
|
);
|
|
4860
4866
|
}
|
|
4861
4867
|
const unknownDotted = [...dotted.keys()].filter(
|
|
4862
|
-
(
|
|
4863
|
-
(f) =>
|
|
4868
|
+
(flag2) => !(command.body?.fields ?? []).some(
|
|
4869
|
+
(f) => flag2.startsWith(`${f.flag}.`)
|
|
4864
4870
|
)
|
|
4865
4871
|
);
|
|
4866
4872
|
if (unknownDotted.length > 0) {
|
|
@@ -4881,6 +4887,15 @@ async function parseInvocation(argv, options) {
|
|
|
4881
4887
|
let stdout = "";
|
|
4882
4888
|
let stderr = "";
|
|
4883
4889
|
let result;
|
|
4890
|
+
if (options.version && (argv[0] === "-v" || argv[0] === "--version")) {
|
|
4891
|
+
return {
|
|
4892
|
+
kind: "exit",
|
|
4893
|
+
code: 0,
|
|
4894
|
+
stdout: `${options.version}
|
|
4895
|
+
`,
|
|
4896
|
+
stderr: ""
|
|
4897
|
+
};
|
|
4898
|
+
}
|
|
4884
4899
|
const { argv: rest, dotted } = splitDotted(argv);
|
|
4885
4900
|
const root = new Command("asteroid").exitOverride().helpCommand(false).configureOutput({
|
|
4886
4901
|
writeOut: (text2) => stdout += text2,
|
|
@@ -4915,34 +4930,34 @@ import { basename } from "node:path";
|
|
|
4915
4930
|
function camel(segment) {
|
|
4916
4931
|
return segment.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
4917
4932
|
}
|
|
4918
|
-
function scalar(
|
|
4919
|
-
const type =
|
|
4920
|
-
if (
|
|
4933
|
+
function scalar(option2, raw) {
|
|
4934
|
+
const type = option2.type.replace("[]", "");
|
|
4935
|
+
if (option2.enum && !option2.enum.includes(raw)) {
|
|
4921
4936
|
throw new UsageError(
|
|
4922
|
-
`--${
|
|
4937
|
+
`--${option2.flag} must be one of ${option2.enum.join(", ")}`
|
|
4923
4938
|
);
|
|
4924
4939
|
}
|
|
4925
4940
|
if (type === "integer" || type === "number") {
|
|
4926
4941
|
const value2 = Number(raw);
|
|
4927
4942
|
if (raw.trim() === "" || Number.isNaN(value2) || type === "integer" && !Number.isInteger(value2)) {
|
|
4928
4943
|
throw new UsageError(
|
|
4929
|
-
`--${
|
|
4944
|
+
`--${option2.flag} must be ${type === "integer" ? "an integer" : "a number"}`
|
|
4930
4945
|
);
|
|
4931
4946
|
}
|
|
4932
4947
|
return value2;
|
|
4933
4948
|
}
|
|
4934
4949
|
if (type === "boolean") {
|
|
4935
4950
|
if (raw !== "true" && raw !== "false") {
|
|
4936
|
-
throw new UsageError(`--${
|
|
4951
|
+
throw new UsageError(`--${option2.flag} must be true or false`);
|
|
4937
4952
|
}
|
|
4938
4953
|
return raw === "true";
|
|
4939
4954
|
}
|
|
4940
|
-
if (type === "json") return parseJson(raw, `--${
|
|
4955
|
+
if (type === "json") return parseJson(raw, `--${option2.flag}`);
|
|
4941
4956
|
return raw;
|
|
4942
4957
|
}
|
|
4943
|
-
function value(
|
|
4944
|
-
if (
|
|
4945
|
-
return scalar(
|
|
4958
|
+
function value(option2, raws) {
|
|
4959
|
+
if (option2.type.endsWith("[]")) return raws.map((raw) => scalar(option2, raw));
|
|
4960
|
+
return scalar(option2, raws.at(-1));
|
|
4946
4961
|
}
|
|
4947
4962
|
var nodeReader = {
|
|
4948
4963
|
text: async (path) => readFileSync(path, "utf8"),
|
|
@@ -4954,12 +4969,12 @@ async function readInput(raw, io) {
|
|
|
4954
4969
|
return raw;
|
|
4955
4970
|
}
|
|
4956
4971
|
async function resolveInputs(command, parsed, io) {
|
|
4957
|
-
const jsonFlags = [...command.query, ...command.body?.fields ?? []].filter((
|
|
4972
|
+
const jsonFlags = [...command.query, ...command.body?.fields ?? []].filter((option2) => option2.type === "json").map((option2) => option2.flag);
|
|
4958
4973
|
const flags = new Map(parsed.flags);
|
|
4959
|
-
for (const
|
|
4960
|
-
const raws = flags.get(
|
|
4974
|
+
for (const flag2 of jsonFlags) {
|
|
4975
|
+
const raws = flags.get(flag2);
|
|
4961
4976
|
if (raws)
|
|
4962
|
-
flags.set(
|
|
4977
|
+
flags.set(flag2, await Promise.all(raws.map((raw) => readInput(raw, io))));
|
|
4963
4978
|
}
|
|
4964
4979
|
const body = parsed.body === void 0 ? void 0 : await readInput(parsed.body, io);
|
|
4965
4980
|
return { ...parsed, flags, body };
|
|
@@ -5009,18 +5024,18 @@ async function buildRequest(command, input, context = {}) {
|
|
|
5009
5024
|
});
|
|
5010
5025
|
const flags = new Map(parsed.flags);
|
|
5011
5026
|
const query = new URLSearchParams();
|
|
5012
|
-
for (const
|
|
5013
|
-
const raws = flags.get(
|
|
5014
|
-
flags.delete(
|
|
5027
|
+
for (const option2 of command.query) {
|
|
5028
|
+
const raws = flags.get(option2.flag);
|
|
5029
|
+
flags.delete(option2.flag);
|
|
5015
5030
|
if (!raws) {
|
|
5016
|
-
if (
|
|
5031
|
+
if (option2.required) throw new UsageError(`--${option2.flag} is required`);
|
|
5017
5032
|
continue;
|
|
5018
5033
|
}
|
|
5019
|
-
const parsedValue = value(
|
|
5034
|
+
const parsedValue = value(option2, raws);
|
|
5020
5035
|
const values = Array.isArray(parsedValue) ? parsedValue : [parsedValue];
|
|
5021
|
-
if (
|
|
5022
|
-
query.append(
|
|
5023
|
-
else for (const v of values) query.append(
|
|
5036
|
+
if (option2.explode === false)
|
|
5037
|
+
query.append(option2.name, values.map(String).join(","));
|
|
5038
|
+
else for (const v of values) query.append(option2.name, String(v));
|
|
5024
5039
|
}
|
|
5025
5040
|
const built = { method: command.method, path, query };
|
|
5026
5041
|
const body = command.body;
|
|
@@ -5070,11 +5085,11 @@ async function buildRequest(command, input, context = {}) {
|
|
|
5070
5085
|
flags.delete(field.flag);
|
|
5071
5086
|
json[field.name] = value(field, raws);
|
|
5072
5087
|
}
|
|
5073
|
-
for (const [
|
|
5074
|
-
const [head, ...rest] =
|
|
5088
|
+
for (const [flag2, raws] of [...flags]) {
|
|
5089
|
+
const [head, ...rest] = flag2.split(".");
|
|
5075
5090
|
const field = body.fields.find((f) => f.flag === head);
|
|
5076
5091
|
if (!field || rest.length === 0) continue;
|
|
5077
|
-
flags.delete(
|
|
5092
|
+
flags.delete(flag2);
|
|
5078
5093
|
const current = json[field.name];
|
|
5079
5094
|
json[field.name] = typeof current === "object" && current !== null ? current : {};
|
|
5080
5095
|
setPath(json, [field.name, ...rest.map(camel)], loose(raws.at(-1)));
|
|
@@ -5105,12 +5120,806 @@ function rejectUnknown(flags) {
|
|
|
5105
5120
|
);
|
|
5106
5121
|
}
|
|
5107
5122
|
}
|
|
5123
|
+
|
|
5124
|
+
// src/sync/commands.ts
|
|
5125
|
+
function option(name, type, description, extra = {}) {
|
|
5126
|
+
return { name, flag: name, type, required: false, description, ...extra };
|
|
5127
|
+
}
|
|
5128
|
+
var DIR = option(
|
|
5129
|
+
"dir",
|
|
5130
|
+
"string",
|
|
5131
|
+
"Write here instead of `<workflow-name>/...` under the current directory."
|
|
5132
|
+
);
|
|
5133
|
+
var local = {
|
|
5134
|
+
method: "GET",
|
|
5135
|
+
path: "",
|
|
5136
|
+
binary: false,
|
|
5137
|
+
destructive: false,
|
|
5138
|
+
local: true
|
|
5139
|
+
};
|
|
5140
|
+
var SYNC_COMMANDS = [
|
|
5141
|
+
{
|
|
5142
|
+
...local,
|
|
5143
|
+
command: "workflow pull",
|
|
5144
|
+
tool: "workflow_pull",
|
|
5145
|
+
title: "Pull a workflow to disk",
|
|
5146
|
+
description: "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.",
|
|
5147
|
+
readOnly: true,
|
|
5148
|
+
args: [
|
|
5149
|
+
{
|
|
5150
|
+
name: "workflowId",
|
|
5151
|
+
flag: "workflow-id",
|
|
5152
|
+
type: "string",
|
|
5153
|
+
required: true
|
|
5154
|
+
}
|
|
5155
|
+
],
|
|
5156
|
+
query: [
|
|
5157
|
+
option(
|
|
5158
|
+
"version-id",
|
|
5159
|
+
"string",
|
|
5160
|
+
"A workflow version ID. Omit for the editable head."
|
|
5161
|
+
),
|
|
5162
|
+
DIR
|
|
5163
|
+
]
|
|
5164
|
+
},
|
|
5165
|
+
{
|
|
5166
|
+
...local,
|
|
5167
|
+
command: "workflow push",
|
|
5168
|
+
tool: "workflow_push",
|
|
5169
|
+
title: "Push local edits to a workflow head",
|
|
5170
|
+
description: "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.",
|
|
5171
|
+
readOnly: false,
|
|
5172
|
+
args: [],
|
|
5173
|
+
query: [
|
|
5174
|
+
option(
|
|
5175
|
+
"dir",
|
|
5176
|
+
"string",
|
|
5177
|
+
"The pulled head folder. Defaults to the current directory."
|
|
5178
|
+
)
|
|
5179
|
+
]
|
|
5180
|
+
},
|
|
5181
|
+
{
|
|
5182
|
+
...local,
|
|
5183
|
+
command: "execution pull",
|
|
5184
|
+
tool: "execution_pull",
|
|
5185
|
+
title: "Pull an execution to disk",
|
|
5186
|
+
description: "Write an execution to `<workflow-name>/executions/<id>/`: summary.json, activities.md (the step-by-step log), and the files the run produced.",
|
|
5187
|
+
readOnly: true,
|
|
5188
|
+
args: [
|
|
5189
|
+
{
|
|
5190
|
+
name: "executionId",
|
|
5191
|
+
flag: "execution-id",
|
|
5192
|
+
type: "string",
|
|
5193
|
+
required: true
|
|
5194
|
+
}
|
|
5195
|
+
],
|
|
5196
|
+
query: [
|
|
5197
|
+
option(
|
|
5198
|
+
"files",
|
|
5199
|
+
"string",
|
|
5200
|
+
"Only download files whose path matches this glob. --no-files skips downloads.",
|
|
5201
|
+
{
|
|
5202
|
+
negatable: true
|
|
5203
|
+
}
|
|
5204
|
+
),
|
|
5205
|
+
option(
|
|
5206
|
+
"summary-only",
|
|
5207
|
+
"boolean",
|
|
5208
|
+
"Write summary.json only: no activities, no files."
|
|
5209
|
+
),
|
|
5210
|
+
option(
|
|
5211
|
+
"max-file-size",
|
|
5212
|
+
"integer",
|
|
5213
|
+
"Skip files larger than this many bytes."
|
|
5214
|
+
),
|
|
5215
|
+
option(
|
|
5216
|
+
"dir",
|
|
5217
|
+
"string",
|
|
5218
|
+
"Write here instead of `<workflow-name>/executions/<id>/` under the current directory."
|
|
5219
|
+
)
|
|
5220
|
+
]
|
|
5221
|
+
}
|
|
5222
|
+
];
|
|
5223
|
+
|
|
5224
|
+
// src/commands.ts
|
|
5225
|
+
function withSyncCommands(commands) {
|
|
5226
|
+
return [...commands, ...SYNC_COMMANDS].sort(
|
|
5227
|
+
(a, b) => a.command.localeCompare(b.command)
|
|
5228
|
+
);
|
|
5229
|
+
}
|
|
5230
|
+
var ALL_COMMANDS = withSyncCommands(COMMANDS);
|
|
5231
|
+
|
|
5232
|
+
// src/sync/activities.ts
|
|
5233
|
+
function indentJson(data) {
|
|
5234
|
+
return JSON.stringify(data, null, 2).split("\n").map((line2) => ` ${line2}`).join("\n");
|
|
5235
|
+
}
|
|
5236
|
+
function action(label, payload) {
|
|
5237
|
+
const info = payload.info;
|
|
5238
|
+
const detail = info && Object.keys(info).length > 0 ? `
|
|
5239
|
+
info:
|
|
5240
|
+
${indentJson(info)}` : "";
|
|
5241
|
+
return `[${label}: ${payload.actionName}]:${detail}
|
|
5242
|
+
`;
|
|
5243
|
+
}
|
|
5244
|
+
function line(payload) {
|
|
5245
|
+
switch (payload.activityType) {
|
|
5246
|
+
case "reasoning":
|
|
5247
|
+
return `[reasoning]:
|
|
5248
|
+
${payload.reasoning}
|
|
5249
|
+
`;
|
|
5250
|
+
case "generic":
|
|
5251
|
+
return `[message]:
|
|
5252
|
+
${payload.message}
|
|
5253
|
+
`;
|
|
5254
|
+
case "user_message_received":
|
|
5255
|
+
return `[user_message]:
|
|
5256
|
+
${payload.message}
|
|
5257
|
+
`;
|
|
5258
|
+
case "action_started":
|
|
5259
|
+
return action("action_started", payload);
|
|
5260
|
+
case "action_completed":
|
|
5261
|
+
return action("action_completed", payload);
|
|
5262
|
+
case "action_failed":
|
|
5263
|
+
return `[action_failed: ${payload.actionName}]:
|
|
5264
|
+
failure: ${payload.message}
|
|
5265
|
+
`;
|
|
5266
|
+
case "compaction_performed":
|
|
5267
|
+
return `[compaction]: summary_tokens: ${payload.summaryTokens}, duration_ms: ${payload.durationMs}
|
|
5268
|
+
`;
|
|
5269
|
+
case "compaction_started":
|
|
5270
|
+
return "[compaction_started]\n";
|
|
5271
|
+
default: {
|
|
5272
|
+
const summary2 = payload.display?.summaryText;
|
|
5273
|
+
return summary2 ? `[${payload.activityType}]: ${summary2}
|
|
5274
|
+
` : `[${payload.activityType}]
|
|
5275
|
+
`;
|
|
5276
|
+
}
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
function formatActivitiesMarkdown(activities, status, createdAt) {
|
|
5280
|
+
const lines = [
|
|
5281
|
+
"# Execution Activities\n",
|
|
5282
|
+
`Status: ${status}`,
|
|
5283
|
+
`Started: ${createdAt}`
|
|
5284
|
+
];
|
|
5285
|
+
let currentTurn = "";
|
|
5286
|
+
for (const { payload } of activities) {
|
|
5287
|
+
const turnId = payload.display?.turnId ?? "";
|
|
5288
|
+
if (turnId && turnId !== currentTurn) {
|
|
5289
|
+
currentTurn = turnId;
|
|
5290
|
+
lines.push(`
|
|
5291
|
+
---
|
|
5292
|
+
|
|
5293
|
+
## ${turnId}
|
|
5294
|
+
`);
|
|
5295
|
+
} else if (!currentTurn && !turnId) {
|
|
5296
|
+
currentTurn = "_started";
|
|
5297
|
+
lines.push("\n---\n");
|
|
5298
|
+
}
|
|
5299
|
+
lines.push(line(payload));
|
|
5300
|
+
}
|
|
5301
|
+
return lines.join("\n");
|
|
5302
|
+
}
|
|
5303
|
+
|
|
5304
|
+
// ../workflow-files/src/representation/name-charset.ts
|
|
5305
|
+
var NAME_CHARSET_RE = /^[A-Za-z0-9 !#$%&'()+,.;=@^`{}[\]~-]+$/;
|
|
5306
|
+
var WINDOWS_RESERVED = /* @__PURE__ */ new Set([
|
|
5307
|
+
"con",
|
|
5308
|
+
"prn",
|
|
5309
|
+
"aux",
|
|
5310
|
+
"nul",
|
|
5311
|
+
...Array.from({ length: 9 }, (_, i) => `com${i + 1}`),
|
|
5312
|
+
...Array.from({ length: 9 }, (_, i) => `lpt${i + 1}`)
|
|
5313
|
+
]);
|
|
5314
|
+
function isNameCharsetConforming(name) {
|
|
5315
|
+
return NAME_CHARSET_RE.test(name);
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5318
|
+
// ../workflow-files/src/representation/types.ts
|
|
5319
|
+
function normalizeNodeName(name) {
|
|
5320
|
+
let result = name.toLowerCase();
|
|
5321
|
+
result = result.replace(/[^a-z0-9]+/g, "_");
|
|
5322
|
+
result = result.replace(/^_+|_+$/g, "");
|
|
5323
|
+
if (result.length > 80) {
|
|
5324
|
+
result = result.slice(0, 80).replace(/_+$/, "");
|
|
5325
|
+
}
|
|
5326
|
+
if (result === "") {
|
|
5327
|
+
throw new Error(
|
|
5328
|
+
`representation: node name "${name}" produces an empty directory name after normalization`
|
|
5329
|
+
);
|
|
5330
|
+
}
|
|
5331
|
+
return result;
|
|
5332
|
+
}
|
|
5333
|
+
function nodeDirName(name) {
|
|
5334
|
+
if (isNameCharsetConforming(name)) {
|
|
5335
|
+
return name.replaceAll(" ", "_");
|
|
5336
|
+
}
|
|
5337
|
+
return normalizeNodeName(name);
|
|
5338
|
+
}
|
|
5339
|
+
function agentDirName(name) {
|
|
5340
|
+
try {
|
|
5341
|
+
return nodeDirName(name);
|
|
5342
|
+
} catch {
|
|
5343
|
+
return "agent";
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
function executionDirName(executionId) {
|
|
5347
|
+
return executionId.slice(0, 8);
|
|
5348
|
+
}
|
|
5349
|
+
function workflowDirName(workflowId, version, parentId, parentVersion) {
|
|
5350
|
+
const short = workflowId.slice(0, 8);
|
|
5351
|
+
if (version !== void 0 && version !== null) {
|
|
5352
|
+
return `v${version}_${short}`;
|
|
5353
|
+
}
|
|
5354
|
+
if (parentId) {
|
|
5355
|
+
if (parentVersion !== void 0 && parentVersion !== null) {
|
|
5356
|
+
return `draft_${short}_from_v${parentVersion}`;
|
|
5357
|
+
}
|
|
5358
|
+
return `draft_${short}_from_draft_${parentId.slice(0, 8)}`;
|
|
5359
|
+
}
|
|
5360
|
+
return `draft_${short}`;
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
// src/sync/paths.ts
|
|
5364
|
+
import { createHash } from "node:crypto";
|
|
5365
|
+
function join(...parts) {
|
|
5366
|
+
return parts.filter((part) => part !== "").join("/").replace(/\/{2,}/g, "/");
|
|
5367
|
+
}
|
|
5368
|
+
function workflowRoot(root, workflowName) {
|
|
5369
|
+
return join(root, agentDirName(workflowName));
|
|
5370
|
+
}
|
|
5371
|
+
function isContained(path) {
|
|
5372
|
+
if (path === "" || path.startsWith("/") || path.includes("\\")) return false;
|
|
5373
|
+
return path.split("/").every((segment) => segment !== ".." && segment !== "");
|
|
5374
|
+
}
|
|
5375
|
+
function sha256(bytes) {
|
|
5376
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
5377
|
+
}
|
|
5378
|
+
function matchesGlob(path, pattern) {
|
|
5379
|
+
let source = "";
|
|
5380
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
5381
|
+
const ch = pattern[i];
|
|
5382
|
+
if (ch === "*" && pattern[i + 1] === "*" && pattern[i + 2] === "/") {
|
|
5383
|
+
source += "(?:.*/)?";
|
|
5384
|
+
i += 2;
|
|
5385
|
+
} else if (ch === "*" && pattern[i + 1] === "*") {
|
|
5386
|
+
source += ".*";
|
|
5387
|
+
i += 1;
|
|
5388
|
+
} else if (ch === "*") source += "[^/]*";
|
|
5389
|
+
else if (ch === "?") source += "[^/]";
|
|
5390
|
+
else source += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
5391
|
+
}
|
|
5392
|
+
return new RegExp(`^${source}$`).test(path);
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5395
|
+
// src/sync/types.ts
|
|
5396
|
+
var MANIFEST = ".asteroid.json";
|
|
5397
|
+
function get(path, query = {}) {
|
|
5398
|
+
return { method: "GET", path, query: new URLSearchParams(query) };
|
|
5399
|
+
}
|
|
5400
|
+
function statusOf(error) {
|
|
5401
|
+
const status = error?.status;
|
|
5402
|
+
return typeof status === "number" ? status : void 0;
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
// src/sync/execution-pull.ts
|
|
5406
|
+
var DIRECTORIES = ["workspace", "downloads", "shared"];
|
|
5407
|
+
function relativeFilePath(file) {
|
|
5408
|
+
const at = file.filePath.indexOf(`/${file.directory}/`);
|
|
5409
|
+
if (at === -1)
|
|
5410
|
+
return `${file.directory}/${file.filePath.split("/").pop() ?? "unknown"}`;
|
|
5411
|
+
return file.filePath.slice(at + 1).replace(/^workspace\/\.execution-context\//, "");
|
|
5412
|
+
}
|
|
5413
|
+
function executionDir(options, execution) {
|
|
5414
|
+
const chosen = options.dir?.(execution);
|
|
5415
|
+
if (chosen) return chosen;
|
|
5416
|
+
const root = workflowRoot(
|
|
5417
|
+
options.root,
|
|
5418
|
+
execution.workflowName ?? execution.workflowId
|
|
5419
|
+
);
|
|
5420
|
+
return join(root, "executions", executionDirName(execution.id));
|
|
5421
|
+
}
|
|
5422
|
+
async function writeText(sync, path, text2) {
|
|
5423
|
+
await sync.files.write(path, Buffer.from(text2, "utf8"));
|
|
5424
|
+
}
|
|
5425
|
+
async function writeActivities(sync, dir, execution) {
|
|
5426
|
+
try {
|
|
5427
|
+
const activities = await sync.remote.json(
|
|
5428
|
+
get(`/agents/v2/executions/${execution.id}/activities`, { order: "asc" })
|
|
5429
|
+
);
|
|
5430
|
+
if (activities.length === 0) return false;
|
|
5431
|
+
const markdown = formatActivitiesMarkdown(
|
|
5432
|
+
activities,
|
|
5433
|
+
execution.status,
|
|
5434
|
+
execution.createdAt
|
|
5435
|
+
);
|
|
5436
|
+
await writeText(sync, join(dir, "activities.md"), markdown);
|
|
5437
|
+
return true;
|
|
5438
|
+
} catch {
|
|
5439
|
+
return false;
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
async function listFiles(sync, executionId) {
|
|
5443
|
+
const listing = await sync.remote.json(
|
|
5444
|
+
get(`/agents/v2/executions/${executionId}/files`)
|
|
5445
|
+
);
|
|
5446
|
+
return DIRECTORIES.flatMap((name) => listing.directories[name] ?? []);
|
|
5447
|
+
}
|
|
5448
|
+
async function download(sync, executionId, file) {
|
|
5449
|
+
const path = `/agents/v2/executions/${executionId}/files/${file.id}/signed-url`;
|
|
5450
|
+
const { url } = await sync.remote.json(get(path));
|
|
5451
|
+
return sync.remote.download(url);
|
|
5452
|
+
}
|
|
5453
|
+
function selectFiles(files, options, skipped) {
|
|
5454
|
+
return files.filter((file) => {
|
|
5455
|
+
const path = relativeFilePath(file);
|
|
5456
|
+
if (!isContained(path)) skipped.push(`${path} (unsafe path)`);
|
|
5457
|
+
else if (options.glob !== void 0 && !matchesGlob(path, options.glob))
|
|
5458
|
+
skipped.push(`${path} (excluded by --files)`);
|
|
5459
|
+
else if (options.maxFileBytes !== void 0 && file.fileSize > options.maxFileBytes)
|
|
5460
|
+
skipped.push(`${path} (${file.fileSize} bytes, over the size limit)`);
|
|
5461
|
+
else return true;
|
|
5462
|
+
return false;
|
|
5463
|
+
});
|
|
5464
|
+
}
|
|
5465
|
+
async function pullFiles(sync, dir, options) {
|
|
5466
|
+
const skipped = [];
|
|
5467
|
+
const failed = [];
|
|
5468
|
+
let files;
|
|
5469
|
+
try {
|
|
5470
|
+
files = await listFiles(sync, options.executionId);
|
|
5471
|
+
} catch (error) {
|
|
5472
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5473
|
+
return {
|
|
5474
|
+
downloaded: 0,
|
|
5475
|
+
candidates: 0,
|
|
5476
|
+
skipped,
|
|
5477
|
+
failed: [`file listing (${reason})`]
|
|
5478
|
+
};
|
|
5479
|
+
}
|
|
5480
|
+
let downloaded = 0;
|
|
5481
|
+
await Promise.all(
|
|
5482
|
+
selectFiles(files, options, skipped).map(async (file) => {
|
|
5483
|
+
try {
|
|
5484
|
+
const bytes = await download(sync, options.executionId, file);
|
|
5485
|
+
await sync.files.write(join(dir, relativeFilePath(file)), bytes);
|
|
5486
|
+
downloaded++;
|
|
5487
|
+
} catch (error) {
|
|
5488
|
+
failed.push(
|
|
5489
|
+
`${file.fileName} (${error instanceof Error ? error.message : String(error)})`
|
|
5490
|
+
);
|
|
5491
|
+
}
|
|
5492
|
+
})
|
|
5493
|
+
);
|
|
5494
|
+
return { downloaded, candidates: files.length, skipped, failed };
|
|
5495
|
+
}
|
|
5496
|
+
async function pullExecution(sync, options) {
|
|
5497
|
+
const execution = await sync.remote.json(
|
|
5498
|
+
get(`/agents/v2/executions/${options.executionId}`)
|
|
5499
|
+
);
|
|
5500
|
+
const dir = executionDir(options, execution);
|
|
5501
|
+
await writeText(
|
|
5502
|
+
sync,
|
|
5503
|
+
join(dir, "summary.json"),
|
|
5504
|
+
JSON.stringify(execution, null, 2)
|
|
5505
|
+
);
|
|
5506
|
+
const result = {
|
|
5507
|
+
execution,
|
|
5508
|
+
dir,
|
|
5509
|
+
written: ["summary.json"],
|
|
5510
|
+
downloaded: 0,
|
|
5511
|
+
candidates: 0,
|
|
5512
|
+
skipped: [],
|
|
5513
|
+
failed: []
|
|
5514
|
+
};
|
|
5515
|
+
if (execution.purgedAt) return result;
|
|
5516
|
+
if (options.activities !== false && await writeActivities(sync, dir, execution))
|
|
5517
|
+
result.written.push("activities.md");
|
|
5518
|
+
if (options.files === false) {
|
|
5519
|
+
return {
|
|
5520
|
+
...result,
|
|
5521
|
+
skippedBy: options.activities === false ? "--summary-only" : "--no-files"
|
|
5522
|
+
};
|
|
5523
|
+
}
|
|
5524
|
+
return { ...result, ...await pullFiles(sync, dir, options) };
|
|
5525
|
+
}
|
|
5526
|
+
function filesLine(result) {
|
|
5527
|
+
const parts = [...result.written];
|
|
5528
|
+
if (result.downloaded > 0) parts.push(`${result.downloaded} downloaded`);
|
|
5529
|
+
const counted = `Files: ${result.written.length + result.downloaded} written (${parts.join(", ")})`;
|
|
5530
|
+
if (result.skippedBy)
|
|
5531
|
+
return `${counted}; downloads skipped (${result.skippedBy})`;
|
|
5532
|
+
if (result.candidates === 0) return `${counted}; no execution files matched`;
|
|
5533
|
+
return counted;
|
|
5534
|
+
}
|
|
5535
|
+
function failures(execution) {
|
|
5536
|
+
const list = execution.scriptFailures ?? [];
|
|
5537
|
+
if (list.length === 0) return null;
|
|
5538
|
+
const lines = list.map(
|
|
5539
|
+
(f) => ` - ${f.nodeName ?? "(unknown node)"}: ${(f.error ?? "").split("\n")[0]}`
|
|
5540
|
+
);
|
|
5541
|
+
return `Script failures: ${list.length}
|
|
5542
|
+
${lines.join("\n")}`;
|
|
5543
|
+
}
|
|
5544
|
+
function purged(result) {
|
|
5545
|
+
const { execution } = result;
|
|
5546
|
+
return [
|
|
5547
|
+
`Execution: ${execution.id}`,
|
|
5548
|
+
`Status: ${execution.status}`,
|
|
5549
|
+
`Purged: ${execution.purgedAt}`,
|
|
5550
|
+
"This run was retention-purged. Activities, files, inputs and result are gone. Do not diagnose it.",
|
|
5551
|
+
`Written to: ${result.dir}/`
|
|
5552
|
+
].join("\n");
|
|
5553
|
+
}
|
|
5554
|
+
function formatExecutionPull(result) {
|
|
5555
|
+
const { execution } = result;
|
|
5556
|
+
if (execution.purgedAt) return purged(result);
|
|
5557
|
+
const outcome = execution.executionResult;
|
|
5558
|
+
return [
|
|
5559
|
+
`Execution: ${execution.id}`,
|
|
5560
|
+
`Status: ${execution.status}`,
|
|
5561
|
+
`Workflow: ${execution.workflowName ?? execution.workflowId}`,
|
|
5562
|
+
execution.workflowVersionId ? `Workflow version: ${execution.workflowVersionId}` : null,
|
|
5563
|
+
execution.inputs ? `Inputs: ${JSON.stringify(execution.inputs)}` : null,
|
|
5564
|
+
outcome ? `Result: ${outcome.outcome}: ${outcome.reasoning}` : null,
|
|
5565
|
+
`Created: ${execution.createdAt}`,
|
|
5566
|
+
execution.terminalAt ? `Completed: ${execution.terminalAt}` : null,
|
|
5567
|
+
execution.duration != null ? `Duration: ${execution.duration.toFixed(1)}s` : null,
|
|
5568
|
+
failures(execution),
|
|
5569
|
+
filesLine(result),
|
|
5570
|
+
result.skipped.length > 0 ? `Skipped:
|
|
5571
|
+
${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
|
|
5572
|
+
result.failed.length > 0 ? `Failed: ${result.failed.join(", ")}` : null,
|
|
5573
|
+
`Written to: ${result.dir}/`
|
|
5574
|
+
].filter(Boolean).join("\n");
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
// src/sync/node-files.ts
|
|
5578
|
+
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5579
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
5580
|
+
function missing(error) {
|
|
5581
|
+
return error.code === "ENOENT";
|
|
5582
|
+
}
|
|
5583
|
+
async function walk(root, dir, out) {
|
|
5584
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
5585
|
+
const path = resolve(dir, entry.name);
|
|
5586
|
+
if (entry.isDirectory()) await walk(root, path, out);
|
|
5587
|
+
else if (entry.isFile())
|
|
5588
|
+
out.push(relative(root, path).split(sep).join("/"));
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
function nodeFiles(cwd) {
|
|
5592
|
+
const at = (path) => resolve(cwd, path);
|
|
5593
|
+
return {
|
|
5594
|
+
async read(path) {
|
|
5595
|
+
try {
|
|
5596
|
+
return new Uint8Array(await readFile(at(path)));
|
|
5597
|
+
} catch (error) {
|
|
5598
|
+
if (missing(error)) return void 0;
|
|
5599
|
+
throw error;
|
|
5600
|
+
}
|
|
5601
|
+
},
|
|
5602
|
+
async write(path, bytes) {
|
|
5603
|
+
await mkdir(dirname(at(path)), { recursive: true });
|
|
5604
|
+
await writeFile(at(path), bytes);
|
|
5605
|
+
},
|
|
5606
|
+
async remove(path) {
|
|
5607
|
+
await rm(at(path), { force: true });
|
|
5608
|
+
},
|
|
5609
|
+
async list(dir) {
|
|
5610
|
+
const out = [];
|
|
5611
|
+
try {
|
|
5612
|
+
await walk(at(dir), at(dir), out);
|
|
5613
|
+
} catch (error) {
|
|
5614
|
+
if (!missing(error)) throw error;
|
|
5615
|
+
}
|
|
5616
|
+
return out.sort();
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
// src/sync/workflow-pull.ts
|
|
5622
|
+
var CONCURRENCY = 16;
|
|
5623
|
+
function treeRequest(options) {
|
|
5624
|
+
const base = `/agents/v2/workflows/${options.workflowId}`;
|
|
5625
|
+
const path = options.versionId ? `${base}/versions/${options.versionId}/files` : `${base}/head/files`;
|
|
5626
|
+
return get(path, { contents: "all" });
|
|
5627
|
+
}
|
|
5628
|
+
function pullDir(options, tree) {
|
|
5629
|
+
if (options.dir) return options.dir;
|
|
5630
|
+
const root = workflowRoot(options.root, tree.workflowName);
|
|
5631
|
+
if (!options.versionId) return join(root, "head");
|
|
5632
|
+
return join(root, ".versions", workflowDirName(tree.versionId, tree.version));
|
|
5633
|
+
}
|
|
5634
|
+
async function agentFileBytes(sync, workflowId, tree, entry) {
|
|
5635
|
+
const path = `/agents/v2/workflows/${workflowId}/versions/${tree.versionId}/files/${entry.fileId}/signed-url`;
|
|
5636
|
+
const { url } = await sync.remote.json(get(path));
|
|
5637
|
+
return sync.remote.download(url);
|
|
5638
|
+
}
|
|
5639
|
+
async function entryBytes(sync, workflowId, tree, entry) {
|
|
5640
|
+
if (entry.contentBase64 !== void 0)
|
|
5641
|
+
return Buffer.from(entry.contentBase64, "base64");
|
|
5642
|
+
if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
|
|
5643
|
+
if (!entry.fileId) return void 0;
|
|
5644
|
+
return agentFileBytes(sync, workflowId, tree, entry);
|
|
5645
|
+
}
|
|
5646
|
+
async function writeEntries(sync, options, tree, dir) {
|
|
5647
|
+
const hashes = {};
|
|
5648
|
+
const skipped = [];
|
|
5649
|
+
let shared = 0;
|
|
5650
|
+
const wanted = tree.files.filter((entry) => {
|
|
5651
|
+
if (!isContained(entry.path)) skipped.push(`${entry.path} (unsafe path)`);
|
|
5652
|
+
else if (options.maxFileBytes !== void 0 && entry.size > options.maxFileBytes)
|
|
5653
|
+
skipped.push(`${entry.path} (${entry.size} bytes, over the size limit)`);
|
|
5654
|
+
else return true;
|
|
5655
|
+
return false;
|
|
5656
|
+
});
|
|
5657
|
+
for (let i = 0; i < wanted.length; i += CONCURRENCY) {
|
|
5658
|
+
await Promise.all(
|
|
5659
|
+
wanted.slice(i, i + CONCURRENCY).map(async (entry) => {
|
|
5660
|
+
const bytes = await entryBytes(sync, options.workflowId, tree, entry);
|
|
5661
|
+
if (!bytes) return void skipped.push(`${entry.path} (no content)`);
|
|
5662
|
+
await sync.files.write(join(dir, entry.path), bytes);
|
|
5663
|
+
hashes[entry.path] = sha256(bytes);
|
|
5664
|
+
if (entry.kind === "agent_file") shared++;
|
|
5665
|
+
})
|
|
5666
|
+
);
|
|
5667
|
+
}
|
|
5668
|
+
return { hashes, skipped, shared };
|
|
5669
|
+
}
|
|
5670
|
+
async function removeStale(sync, dir, keep) {
|
|
5671
|
+
for (const path of await sync.files.list(dir)) {
|
|
5672
|
+
if (!keep.has(path) && path !== MANIFEST)
|
|
5673
|
+
await sync.files.remove(join(dir, path));
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
async function writeManifest(sync, dir, manifest) {
|
|
5677
|
+
const text2 = `${JSON.stringify(manifest, null, 2)}
|
|
5678
|
+
`;
|
|
5679
|
+
await sync.files.write(join(dir, MANIFEST), Buffer.from(text2, "utf8"));
|
|
5680
|
+
}
|
|
5681
|
+
async function pullWorkflow(sync, options) {
|
|
5682
|
+
const tree = await sync.remote.json(treeRequest(options));
|
|
5683
|
+
const dir = pullDir(options, tree);
|
|
5684
|
+
const { hashes, skipped, shared } = await writeEntries(
|
|
5685
|
+
sync,
|
|
5686
|
+
options,
|
|
5687
|
+
tree,
|
|
5688
|
+
dir
|
|
5689
|
+
);
|
|
5690
|
+
await removeStale(sync, dir, new Set(Object.keys(hashes)));
|
|
5691
|
+
if (options.manifest !== false) {
|
|
5692
|
+
await writeManifest(sync, dir, {
|
|
5693
|
+
workflowId: options.workflowId,
|
|
5694
|
+
versionId: tree.versionId,
|
|
5695
|
+
head: !options.versionId,
|
|
5696
|
+
rev: tree.rev,
|
|
5697
|
+
files: hashes
|
|
5698
|
+
});
|
|
5699
|
+
}
|
|
5700
|
+
return {
|
|
5701
|
+
workflowId: options.workflowId,
|
|
5702
|
+
workflowName: tree.workflowName,
|
|
5703
|
+
versionId: tree.versionId,
|
|
5704
|
+
version: tree.version,
|
|
5705
|
+
rev: tree.rev,
|
|
5706
|
+
dir,
|
|
5707
|
+
written: Object.keys(hashes).length,
|
|
5708
|
+
shared,
|
|
5709
|
+
skipped
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
function formatWorkflowPull(result) {
|
|
5713
|
+
const definition = result.written - result.shared;
|
|
5714
|
+
const files = result.written === 0 ? "Files: none written" : result.shared === 0 ? `Files: ${result.written} written` : `Files: ${result.written} written (${definition} workflow definition, ${result.shared} shared)`;
|
|
5715
|
+
return [
|
|
5716
|
+
`Workflow: ${result.workflowName} (${result.workflowId})`,
|
|
5717
|
+
`Workflow version: ${result.versionId}${result.version ? ` (v${result.version})` : " (draft)"}`,
|
|
5718
|
+
`Written to: ${result.dir}/`,
|
|
5719
|
+
files,
|
|
5720
|
+
result.skipped.length > 0 ? `Skipped:
|
|
5721
|
+
${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null
|
|
5722
|
+
].filter(Boolean).join("\n");
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
// src/sync/workflow-push.ts
|
|
5726
|
+
async function readManifest(sync, dir) {
|
|
5727
|
+
const bytes = await sync.files.read(join(dir, MANIFEST));
|
|
5728
|
+
if (!bytes) {
|
|
5729
|
+
throw new UsageError(
|
|
5730
|
+
`no pulled workflow in ${dir}. Run \`asteroid workflow pull <workflow-id>\`, then push from the head folder it writes.`
|
|
5731
|
+
);
|
|
5732
|
+
}
|
|
5733
|
+
const manifest = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
5734
|
+
if (!manifest.head) {
|
|
5735
|
+
throw new UsageError(
|
|
5736
|
+
`${dir} holds a pinned workflow version, which is read-only. Push from a pulled head.`
|
|
5737
|
+
);
|
|
5738
|
+
}
|
|
5739
|
+
return manifest;
|
|
5740
|
+
}
|
|
5741
|
+
async function localChanges(sync, dir, manifest) {
|
|
5742
|
+
const local2 = (await sync.files.list(dir)).filter(
|
|
5743
|
+
(path) => path !== MANIFEST
|
|
5744
|
+
);
|
|
5745
|
+
const writes = [];
|
|
5746
|
+
for (const path of local2) {
|
|
5747
|
+
const bytes = await sync.files.read(join(dir, path));
|
|
5748
|
+
if (!bytes) continue;
|
|
5749
|
+
const hash = sha256(bytes);
|
|
5750
|
+
if (manifest.files[path] === hash) continue;
|
|
5751
|
+
writes.push({
|
|
5752
|
+
path,
|
|
5753
|
+
contentBase64: Buffer.from(bytes).toString("base64"),
|
|
5754
|
+
hash
|
|
5755
|
+
});
|
|
5756
|
+
}
|
|
5757
|
+
const present = new Set(local2);
|
|
5758
|
+
const deletes = Object.keys(manifest.files).filter(
|
|
5759
|
+
(path) => !present.has(path)
|
|
5760
|
+
);
|
|
5761
|
+
return { writes, deletes };
|
|
5762
|
+
}
|
|
5763
|
+
async function patch(sync, manifest, writes, deletes) {
|
|
5764
|
+
try {
|
|
5765
|
+
await sync.remote.json({
|
|
5766
|
+
method: "PATCH",
|
|
5767
|
+
path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
|
|
5768
|
+
query: new URLSearchParams(),
|
|
5769
|
+
json: { baseRev: manifest.rev, writes, deletes }
|
|
5770
|
+
});
|
|
5771
|
+
} catch (error) {
|
|
5772
|
+
if (statusOf(error) !== 409) throw error;
|
|
5773
|
+
throw new UsageError(
|
|
5774
|
+
`the workflow head changed since your pull (rev ${manifest.rev}). Copy your edits aside, run \`asteroid workflow pull ${manifest.workflowId}\`, reapply them, then push again.`
|
|
5775
|
+
);
|
|
5776
|
+
}
|
|
5777
|
+
}
|
|
5778
|
+
async function refresh(sync, dir, manifest, pushed) {
|
|
5779
|
+
const tree = await sync.remote.json(
|
|
5780
|
+
get(`/agents/v2/workflows/${manifest.workflowId}/head/files`)
|
|
5781
|
+
);
|
|
5782
|
+
const files = {};
|
|
5783
|
+
for (const entry of tree.files) {
|
|
5784
|
+
if (entry.content !== void 0) {
|
|
5785
|
+
const bytes = Buffer.from(entry.content, "utf8");
|
|
5786
|
+
files[entry.path] = sha256(bytes);
|
|
5787
|
+
if (pushed.get(entry.path) !== files[entry.path])
|
|
5788
|
+
await sync.files.write(join(dir, entry.path), bytes);
|
|
5789
|
+
} else {
|
|
5790
|
+
const local2 = await sync.files.read(join(dir, entry.path));
|
|
5791
|
+
const hash = local2 ? sha256(local2) : pushed.get(entry.path) ?? entry.checksum;
|
|
5792
|
+
if (hash) files[entry.path] = hash;
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
for (const path of await sync.files.list(dir)) {
|
|
5796
|
+
if (path !== MANIFEST && !(path in files))
|
|
5797
|
+
await sync.files.remove(join(dir, path));
|
|
5798
|
+
}
|
|
5799
|
+
const next = { ...manifest, versionId: tree.versionId, rev: tree.rev, files };
|
|
5800
|
+
await writeManifest(sync, dir, next);
|
|
5801
|
+
return next;
|
|
5802
|
+
}
|
|
5803
|
+
async function pushWorkflow(sync, dir) {
|
|
5804
|
+
const manifest = await readManifest(sync, dir);
|
|
5805
|
+
const { writes, deletes } = await localChanges(sync, dir, manifest);
|
|
5806
|
+
const result = {
|
|
5807
|
+
workflowId: manifest.workflowId,
|
|
5808
|
+
dir,
|
|
5809
|
+
written: writes.map((w) => w.path),
|
|
5810
|
+
deleted: deletes
|
|
5811
|
+
};
|
|
5812
|
+
if (writes.length === 0 && deletes.length === 0) {
|
|
5813
|
+
return { ...result, rev: manifest.rev, versionId: manifest.versionId };
|
|
5814
|
+
}
|
|
5815
|
+
await patch(
|
|
5816
|
+
sync,
|
|
5817
|
+
manifest,
|
|
5818
|
+
writes.map(({ path, contentBase64 }) => ({ path, contentBase64 })),
|
|
5819
|
+
deletes
|
|
5820
|
+
);
|
|
5821
|
+
const next = await refresh(
|
|
5822
|
+
sync,
|
|
5823
|
+
dir,
|
|
5824
|
+
manifest,
|
|
5825
|
+
new Map(writes.map((w) => [w.path, w.hash]))
|
|
5826
|
+
);
|
|
5827
|
+
return { ...result, rev: next.rev, versionId: next.versionId };
|
|
5828
|
+
}
|
|
5829
|
+
function formatWorkflowPush(result) {
|
|
5830
|
+
if (result.written.length === 0 && result.deleted.length === 0) {
|
|
5831
|
+
return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).`;
|
|
5832
|
+
}
|
|
5833
|
+
return [
|
|
5834
|
+
`Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.`,
|
|
5835
|
+
...result.written.map((path) => ` + ${path}`),
|
|
5836
|
+
...result.deleted.map((path) => ` - ${path}`),
|
|
5837
|
+
`Head is now rev ${result.rev} (version ${result.versionId}).`
|
|
5838
|
+
].join("\n");
|
|
5839
|
+
}
|
|
5840
|
+
|
|
5841
|
+
// src/sync/run.ts
|
|
5842
|
+
function flag(parsed, name) {
|
|
5843
|
+
return parsed.flags.get(name)?.at(-1);
|
|
5844
|
+
}
|
|
5845
|
+
function integer(parsed, name) {
|
|
5846
|
+
const raw = flag(parsed, name);
|
|
5847
|
+
if (raw === void 0) return void 0;
|
|
5848
|
+
if (!/^\d+$/.test(raw))
|
|
5849
|
+
throw new UsageError(`--${name} must be a whole number`);
|
|
5850
|
+
return Number(raw);
|
|
5851
|
+
}
|
|
5852
|
+
function executionOptions(parsed) {
|
|
5853
|
+
const summaryOnly = flag(parsed, "summary-only") === "true";
|
|
5854
|
+
const glob = flag(parsed, "files");
|
|
5855
|
+
const dir = flag(parsed, "dir");
|
|
5856
|
+
return {
|
|
5857
|
+
executionId: parsed.positionals[0],
|
|
5858
|
+
activities: !summaryOnly,
|
|
5859
|
+
files: !summaryOnly && glob !== "false",
|
|
5860
|
+
glob: glob === "false" ? void 0 : glob,
|
|
5861
|
+
maxFileBytes: integer(parsed, "max-file-size"),
|
|
5862
|
+
dir: dir ? () => dir : void 0
|
|
5863
|
+
};
|
|
5864
|
+
}
|
|
5865
|
+
async function execute(command, parsed, sync, deps) {
|
|
5866
|
+
const root = deps.root ?? "";
|
|
5867
|
+
switch (command.command) {
|
|
5868
|
+
case "workflow pull": {
|
|
5869
|
+
const result = await pullWorkflow(sync, {
|
|
5870
|
+
workflowId: parsed.positionals[0],
|
|
5871
|
+
versionId: flag(parsed, "version-id"),
|
|
5872
|
+
dir: flag(parsed, "dir"),
|
|
5873
|
+
root,
|
|
5874
|
+
...deps.workflowPull
|
|
5875
|
+
});
|
|
5876
|
+
return { result, text: formatWorkflowPull(result) };
|
|
5877
|
+
}
|
|
5878
|
+
case "workflow push": {
|
|
5879
|
+
const result = await pushWorkflow(sync, flag(parsed, "dir") ?? ".");
|
|
5880
|
+
return { result, text: formatWorkflowPush(result) };
|
|
5881
|
+
}
|
|
5882
|
+
case "execution pull": {
|
|
5883
|
+
const result = await pullExecution(sync, {
|
|
5884
|
+
...executionOptions(parsed),
|
|
5885
|
+
root,
|
|
5886
|
+
...deps.executionPull
|
|
5887
|
+
});
|
|
5888
|
+
return { result, text: formatExecutionPull(result) };
|
|
5889
|
+
}
|
|
5890
|
+
default:
|
|
5891
|
+
throw new UsageError(`unknown command ${command.command}`);
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
async function runSync(command, parsed, deps) {
|
|
5895
|
+
const sync = {
|
|
5896
|
+
remote: deps.remote,
|
|
5897
|
+
files: deps.files ?? nodeFiles(deps.cwd)
|
|
5898
|
+
};
|
|
5899
|
+
const { result, text: text2 } = await execute(command, parsed, sync, deps);
|
|
5900
|
+
deps.stdout(`${parsed.json ? JSON.stringify(result, null, 2) : text2}
|
|
5901
|
+
`);
|
|
5902
|
+
return 0;
|
|
5903
|
+
}
|
|
5108
5904
|
export {
|
|
5905
|
+
ALL_COMMANDS,
|
|
5109
5906
|
COMMANDS,
|
|
5907
|
+
MANIFEST,
|
|
5908
|
+
SYNC_COMMANDS,
|
|
5110
5909
|
UsageError,
|
|
5111
5910
|
buildRequest,
|
|
5911
|
+
formatActivitiesMarkdown,
|
|
5912
|
+
formatExecutionPull,
|
|
5913
|
+
formatWorkflowPull,
|
|
5914
|
+
formatWorkflowPush,
|
|
5112
5915
|
leadingWords,
|
|
5113
5916
|
matchCommand,
|
|
5917
|
+
matchesGlob,
|
|
5114
5918
|
parseInvocation,
|
|
5115
|
-
|
|
5919
|
+
pullExecution,
|
|
5920
|
+
pullWorkflow,
|
|
5921
|
+
pushWorkflow,
|
|
5922
|
+
render,
|
|
5923
|
+
runSync,
|
|
5924
|
+
withSyncCommands
|
|
5116
5925
|
};
|