@asteroidai/cli 1.9.2 → 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 +1140 -316
- package/dist/core.js +869 -59
- 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/output.test.ts +19 -0
- package/src/output.ts +11 -9
- package/src/program.test.ts +19 -0
- package/src/program.ts +15 -1
- package/src/reference.ts +1 -0
- package/src/run.test.ts +55 -2
- package/src/run.ts +62 -21
- 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,17 +4555,18 @@ 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
|
-
function cell(value2) {
|
|
4563
|
+
function cell(value2, options = {}) {
|
|
4564
4564
|
if (value2 === null || value2 === void 0) return "";
|
|
4565
|
-
const
|
|
4566
|
-
|
|
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`;
|
|
4567
4568
|
}
|
|
4568
|
-
function columns(rows) {
|
|
4569
|
+
function columns(rows, options) {
|
|
4569
4570
|
const scalarKeys = [];
|
|
4570
4571
|
for (const row of rows) {
|
|
4571
4572
|
for (const [key, value2] of Object.entries(row)) {
|
|
@@ -4577,7 +4578,7 @@ function columns(rows) {
|
|
|
4577
4578
|
...PREFERRED.filter((key) => scalarKeys.includes(key)),
|
|
4578
4579
|
...scalarKeys.filter((key) => !PREFERRED.includes(key))
|
|
4579
4580
|
];
|
|
4580
|
-
return ordered.slice(0, MAX_COLUMNS);
|
|
4581
|
+
return options.full ? ordered : ordered.slice(0, MAX_COLUMNS);
|
|
4581
4582
|
}
|
|
4582
4583
|
function style(format, value2, options) {
|
|
4583
4584
|
return options.styled ? styleText(format, value2, { validateStream: false }) : value2;
|
|
@@ -4597,9 +4598,9 @@ function flatten(row) {
|
|
|
4597
4598
|
function table(input, options = {}) {
|
|
4598
4599
|
if (input.length === 0) return "No results.";
|
|
4599
4600
|
const rows = input.map(flatten);
|
|
4600
|
-
const keys = columns(rows);
|
|
4601
|
+
const keys = columns(rows, options);
|
|
4601
4602
|
if (keys.length === 0) return rows.map((row) => text(row)).join("\n");
|
|
4602
|
-
const cells = rows.map((row) => keys.map((key) => cell(row[key])));
|
|
4603
|
+
const cells = rows.map((row) => keys.map((key) => cell(row[key], options)));
|
|
4603
4604
|
if (options.styled) {
|
|
4604
4605
|
const grid = new Table({
|
|
4605
4606
|
head: keys.map((key) => style("bold", key, options)),
|
|
@@ -4611,15 +4612,15 @@ function table(input, options = {}) {
|
|
|
4611
4612
|
const widths = keys.map(
|
|
4612
4613
|
(key, i) => Math.max(key.length, ...cells.map((r) => r[i].length))
|
|
4613
4614
|
);
|
|
4614
|
-
const
|
|
4615
|
-
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(
|
|
4616
4617
|
"\n"
|
|
4617
4618
|
);
|
|
4618
4619
|
}
|
|
4619
|
-
function summary(value2) {
|
|
4620
|
+
function summary(value2, options) {
|
|
4620
4621
|
if (Array.isArray(value2))
|
|
4621
4622
|
return `[${value2.length} item${value2.length === 1 ? "" : "s"}]`;
|
|
4622
|
-
if (value2 && typeof value2 === "object") return cell(value2);
|
|
4623
|
+
if (value2 && typeof value2 === "object") return cell(value2, options);
|
|
4623
4624
|
return text(value2);
|
|
4624
4625
|
}
|
|
4625
4626
|
function record(value2, options) {
|
|
@@ -4638,7 +4639,7 @@ ${" ".repeat(width + 2)}`)}`;
|
|
|
4638
4639
|
return `${style("bold", key, options)}:
|
|
4639
4640
|
${indent(record(v, options))}`;
|
|
4640
4641
|
}
|
|
4641
|
-
return `${label(key)} ${isScalar(v) ? text(v) : summary(v)}`;
|
|
4642
|
+
return `${label(key)} ${isScalar(v) ? text(v) : summary(v, options)}`;
|
|
4642
4643
|
}).join("\n");
|
|
4643
4644
|
}
|
|
4644
4645
|
function render(value2, options = {}) {
|
|
@@ -4726,12 +4727,12 @@ var DOTTED = /^--([a-z0-9-]+\.[a-z0-9.-]+)(?:=(.*))?$/;
|
|
|
4726
4727
|
function collect(value2, previous) {
|
|
4727
4728
|
return [...previous ?? [], value2];
|
|
4728
4729
|
}
|
|
4729
|
-
function optionText(
|
|
4730
|
-
const text2 =
|
|
4730
|
+
function optionText(option2) {
|
|
4731
|
+
const text2 = option2.description?.replace(/\s+/g, " ").trim() ?? "";
|
|
4731
4732
|
const parts = [text2 && !/[.!?]$/.test(text2) ? `${text2}.` : text2];
|
|
4732
|
-
if (
|
|
4733
|
-
if (
|
|
4734
|
-
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.");
|
|
4735
4736
|
return parts.filter(Boolean).join(" ");
|
|
4736
4737
|
}
|
|
4737
4738
|
function splitDotted(argv) {
|
|
@@ -4758,18 +4759,20 @@ function group(parent, name, groups, path) {
|
|
|
4758
4759
|
return created;
|
|
4759
4760
|
}
|
|
4760
4761
|
function leafOptions(leaf, command) {
|
|
4761
|
-
for (const
|
|
4762
|
-
if (
|
|
4763
|
-
leaf.option(`--${
|
|
4764
|
-
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.`);
|
|
4765
4766
|
continue;
|
|
4766
4767
|
}
|
|
4767
|
-
const placeholder =
|
|
4768
|
+
const placeholder = option2.enum ? option2.enum.join("|") : option2.type.replace("[]", "");
|
|
4768
4769
|
leaf.option(
|
|
4769
|
-
`--${
|
|
4770
|
-
optionText(
|
|
4770
|
+
`--${option2.flag} <${placeholder}>`,
|
|
4771
|
+
optionText(option2),
|
|
4771
4772
|
collect
|
|
4772
4773
|
);
|
|
4774
|
+
if (option2.negatable)
|
|
4775
|
+
leaf.option(`--no-${option2.flag}`, `Skip ${option2.flag}.`);
|
|
4773
4776
|
}
|
|
4774
4777
|
for (const file of command.body?.files ?? []) {
|
|
4775
4778
|
leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
|
|
@@ -4779,11 +4782,11 @@ function toParsed(leaf, positionals, dotted) {
|
|
|
4779
4782
|
const globals = leaf.optsWithGlobals();
|
|
4780
4783
|
const own = leaf.opts();
|
|
4781
4784
|
const flags = new Map(dotted);
|
|
4782
|
-
for (const
|
|
4783
|
-
const
|
|
4784
|
-
const value2 = own[
|
|
4785
|
-
if (!
|
|
4786
|
-
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)]);
|
|
4787
4790
|
}
|
|
4788
4791
|
return {
|
|
4789
4792
|
positionals,
|
|
@@ -4796,7 +4799,11 @@ function toParsed(leaf, positionals, dotted) {
|
|
|
4796
4799
|
}
|
|
4797
4800
|
function defineCommands(root, options, dotted = /* @__PURE__ */ new Map(), onInvocation = () => void 0) {
|
|
4798
4801
|
if (options.description) root.description(options.description);
|
|
4799
|
-
if (options.version)
|
|
4802
|
+
if (options.version)
|
|
4803
|
+
root.addHelpText(
|
|
4804
|
+
"after",
|
|
4805
|
+
"\nRun `asteroid --version` to print the CLI version."
|
|
4806
|
+
);
|
|
4800
4807
|
for (const [flags, text2] of GLOBAL_FLAGS) {
|
|
4801
4808
|
if (!flags.startsWith("-h")) root.option(flags, text2);
|
|
4802
4809
|
}
|
|
@@ -4858,8 +4865,8 @@ ${notes.join("\n")}`);
|
|
|
4858
4865
|
);
|
|
4859
4866
|
}
|
|
4860
4867
|
const unknownDotted = [...dotted.keys()].filter(
|
|
4861
|
-
(
|
|
4862
|
-
(f) =>
|
|
4868
|
+
(flag2) => !(command.body?.fields ?? []).some(
|
|
4869
|
+
(f) => flag2.startsWith(`${f.flag}.`)
|
|
4863
4870
|
)
|
|
4864
4871
|
);
|
|
4865
4872
|
if (unknownDotted.length > 0) {
|
|
@@ -4880,6 +4887,15 @@ async function parseInvocation(argv, options) {
|
|
|
4880
4887
|
let stdout = "";
|
|
4881
4888
|
let stderr = "";
|
|
4882
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
|
+
}
|
|
4883
4899
|
const { argv: rest, dotted } = splitDotted(argv);
|
|
4884
4900
|
const root = new Command("asteroid").exitOverride().helpCommand(false).configureOutput({
|
|
4885
4901
|
writeOut: (text2) => stdout += text2,
|
|
@@ -4914,34 +4930,34 @@ import { basename } from "node:path";
|
|
|
4914
4930
|
function camel(segment) {
|
|
4915
4931
|
return segment.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
4916
4932
|
}
|
|
4917
|
-
function scalar(
|
|
4918
|
-
const type =
|
|
4919
|
-
if (
|
|
4933
|
+
function scalar(option2, raw) {
|
|
4934
|
+
const type = option2.type.replace("[]", "");
|
|
4935
|
+
if (option2.enum && !option2.enum.includes(raw)) {
|
|
4920
4936
|
throw new UsageError(
|
|
4921
|
-
`--${
|
|
4937
|
+
`--${option2.flag} must be one of ${option2.enum.join(", ")}`
|
|
4922
4938
|
);
|
|
4923
4939
|
}
|
|
4924
4940
|
if (type === "integer" || type === "number") {
|
|
4925
4941
|
const value2 = Number(raw);
|
|
4926
4942
|
if (raw.trim() === "" || Number.isNaN(value2) || type === "integer" && !Number.isInteger(value2)) {
|
|
4927
4943
|
throw new UsageError(
|
|
4928
|
-
`--${
|
|
4944
|
+
`--${option2.flag} must be ${type === "integer" ? "an integer" : "a number"}`
|
|
4929
4945
|
);
|
|
4930
4946
|
}
|
|
4931
4947
|
return value2;
|
|
4932
4948
|
}
|
|
4933
4949
|
if (type === "boolean") {
|
|
4934
4950
|
if (raw !== "true" && raw !== "false") {
|
|
4935
|
-
throw new UsageError(`--${
|
|
4951
|
+
throw new UsageError(`--${option2.flag} must be true or false`);
|
|
4936
4952
|
}
|
|
4937
4953
|
return raw === "true";
|
|
4938
4954
|
}
|
|
4939
|
-
if (type === "json") return parseJson(raw, `--${
|
|
4955
|
+
if (type === "json") return parseJson(raw, `--${option2.flag}`);
|
|
4940
4956
|
return raw;
|
|
4941
4957
|
}
|
|
4942
|
-
function value(
|
|
4943
|
-
if (
|
|
4944
|
-
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));
|
|
4945
4961
|
}
|
|
4946
4962
|
var nodeReader = {
|
|
4947
4963
|
text: async (path) => readFileSync(path, "utf8"),
|
|
@@ -4953,12 +4969,12 @@ async function readInput(raw, io) {
|
|
|
4953
4969
|
return raw;
|
|
4954
4970
|
}
|
|
4955
4971
|
async function resolveInputs(command, parsed, io) {
|
|
4956
|
-
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);
|
|
4957
4973
|
const flags = new Map(parsed.flags);
|
|
4958
|
-
for (const
|
|
4959
|
-
const raws = flags.get(
|
|
4974
|
+
for (const flag2 of jsonFlags) {
|
|
4975
|
+
const raws = flags.get(flag2);
|
|
4960
4976
|
if (raws)
|
|
4961
|
-
flags.set(
|
|
4977
|
+
flags.set(flag2, await Promise.all(raws.map((raw) => readInput(raw, io))));
|
|
4962
4978
|
}
|
|
4963
4979
|
const body = parsed.body === void 0 ? void 0 : await readInput(parsed.body, io);
|
|
4964
4980
|
return { ...parsed, flags, body };
|
|
@@ -5008,18 +5024,18 @@ async function buildRequest(command, input, context = {}) {
|
|
|
5008
5024
|
});
|
|
5009
5025
|
const flags = new Map(parsed.flags);
|
|
5010
5026
|
const query = new URLSearchParams();
|
|
5011
|
-
for (const
|
|
5012
|
-
const raws = flags.get(
|
|
5013
|
-
flags.delete(
|
|
5027
|
+
for (const option2 of command.query) {
|
|
5028
|
+
const raws = flags.get(option2.flag);
|
|
5029
|
+
flags.delete(option2.flag);
|
|
5014
5030
|
if (!raws) {
|
|
5015
|
-
if (
|
|
5031
|
+
if (option2.required) throw new UsageError(`--${option2.flag} is required`);
|
|
5016
5032
|
continue;
|
|
5017
5033
|
}
|
|
5018
|
-
const parsedValue = value(
|
|
5034
|
+
const parsedValue = value(option2, raws);
|
|
5019
5035
|
const values = Array.isArray(parsedValue) ? parsedValue : [parsedValue];
|
|
5020
|
-
if (
|
|
5021
|
-
query.append(
|
|
5022
|
-
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));
|
|
5023
5039
|
}
|
|
5024
5040
|
const built = { method: command.method, path, query };
|
|
5025
5041
|
const body = command.body;
|
|
@@ -5069,11 +5085,11 @@ async function buildRequest(command, input, context = {}) {
|
|
|
5069
5085
|
flags.delete(field.flag);
|
|
5070
5086
|
json[field.name] = value(field, raws);
|
|
5071
5087
|
}
|
|
5072
|
-
for (const [
|
|
5073
|
-
const [head, ...rest] =
|
|
5088
|
+
for (const [flag2, raws] of [...flags]) {
|
|
5089
|
+
const [head, ...rest] = flag2.split(".");
|
|
5074
5090
|
const field = body.fields.find((f) => f.flag === head);
|
|
5075
5091
|
if (!field || rest.length === 0) continue;
|
|
5076
|
-
flags.delete(
|
|
5092
|
+
flags.delete(flag2);
|
|
5077
5093
|
const current = json[field.name];
|
|
5078
5094
|
json[field.name] = typeof current === "object" && current !== null ? current : {};
|
|
5079
5095
|
setPath(json, [field.name, ...rest.map(camel)], loose(raws.at(-1)));
|
|
@@ -5104,12 +5120,806 @@ function rejectUnknown(flags) {
|
|
|
5104
5120
|
);
|
|
5105
5121
|
}
|
|
5106
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
|
+
}
|
|
5107
5904
|
export {
|
|
5905
|
+
ALL_COMMANDS,
|
|
5108
5906
|
COMMANDS,
|
|
5907
|
+
MANIFEST,
|
|
5908
|
+
SYNC_COMMANDS,
|
|
5109
5909
|
UsageError,
|
|
5110
5910
|
buildRequest,
|
|
5911
|
+
formatActivitiesMarkdown,
|
|
5912
|
+
formatExecutionPull,
|
|
5913
|
+
formatWorkflowPull,
|
|
5914
|
+
formatWorkflowPush,
|
|
5111
5915
|
leadingWords,
|
|
5112
5916
|
matchCommand,
|
|
5917
|
+
matchesGlob,
|
|
5113
5918
|
parseInvocation,
|
|
5114
|
-
|
|
5919
|
+
pullExecution,
|
|
5920
|
+
pullWorkflow,
|
|
5921
|
+
pushWorkflow,
|
|
5922
|
+
render,
|
|
5923
|
+
runSync,
|
|
5924
|
+
withSyncCommands
|
|
5115
5925
|
};
|