@asteroidai/cli 1.9.96 → 1.9.99

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.
Files changed (3) hide show
  1. package/dist/asteroid.js +238 -83
  2. package/dist/core.js +250 -83
  3. package/package.json +1 -1
package/dist/asteroid.js CHANGED
@@ -378,7 +378,7 @@ var COMMANDS = [
378
378
  flag: "executions-write-back",
379
379
  type: "boolean",
380
380
  required: false,
381
- description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. A run that uses its profile's sticky IP saves anyway."
381
+ description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. The first run that uses its profile's sticky IP saves anyway."
382
382
  }
383
383
  ]
384
384
  },
@@ -795,7 +795,7 @@ var COMMANDS = [
795
795
  flag: "executions-write-back",
796
796
  type: "boolean",
797
797
  required: false,
798
- description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. A run that uses its profile's sticky IP saves anyway."
798
+ description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. The first run that uses its profile's sticky IP saves anyway."
799
799
  }
800
800
  ]
801
801
  },
@@ -1907,7 +1907,7 @@ var COMMANDS = [
1907
1907
  command: "portal-skills add",
1908
1908
  tool: "portal_skills_add",
1909
1909
  title: "Add portal skills",
1910
- description: "Add a portal's skills to an organization, by portal id. It is general knowledge about how a portal works. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
1910
+ description: "Add a portal's skills to an organization, by a `portalId` from `portal_skills_list`. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
1911
1911
  method: "POST",
1912
1912
  path: "/agents/v2/portal-skills/{portalId}",
1913
1913
  args: [
@@ -1923,6 +1923,27 @@ var COMMANDS = [
1923
1923
  readOnly: false,
1924
1924
  destructive: false
1925
1925
  },
1926
+ {
1927
+ command: "portal-skills list",
1928
+ tool: "portal_skills_list",
1929
+ title: "List portal skills",
1930
+ description: "List the skills for third-party portals, or only for the portals matching a portal name or task, such as `caloptima eligibility`. A portal skill is general knowledge about how a portal works: sign-in, page quirks, field mappings. Each portal comes with its skill names, and whether the organization has added it. Add one with `portal_skills_add`.",
1931
+ method: "GET",
1932
+ path: "/astro/v2/portal-skills",
1933
+ args: [],
1934
+ query: [
1935
+ {
1936
+ name: "query",
1937
+ flag: "query",
1938
+ type: "string",
1939
+ required: false,
1940
+ description: "A portal name or task, at least three characters. Omit to list every portal with skills."
1941
+ }
1942
+ ],
1943
+ binary: false,
1944
+ readOnly: true,
1945
+ destructive: false
1946
+ },
1926
1947
  {
1927
1948
  command: "profile clear-cache",
1928
1949
  tool: "profile_clear_browser_cache",
@@ -4441,7 +4462,7 @@ var COMMANDS = [
4441
4462
  command: "workflow head publish",
4442
4463
  tool: "workflow_head_publish",
4443
4464
  title: "Publish workflow head",
4444
- description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of.",
4465
+ description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
4445
4466
  method: "POST",
4446
4467
  path: "/agents/v2/workflows/{workflowId}/head/publish",
4447
4468
  args: [
@@ -5378,7 +5399,7 @@ var COMMANDS = [
5378
5399
  command: "workflow version publish",
5379
5400
  tool: "workflow_version_publish",
5380
5401
  title: "Publish workflow version",
5381
- description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`.",
5402
+ description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`. This also publishes an older version again. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
5382
5403
  method: "POST",
5383
5404
  path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/publish",
5384
5405
  args: [
@@ -6033,16 +6054,26 @@ function leafOptions(leaf, command) {
6033
6054
  for (const file of command.body?.files ?? []) {
6034
6055
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
6035
6056
  }
6036
- if (command.files === "pull")
6057
+ if (command.files === "pull") {
6037
6058
  leaf.option(
6038
6059
  "--out <dir>",
6039
6060
  "Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead."
6040
6061
  );
6041
- if (command.files === "push")
6062
+ leaf.option(
6063
+ "--keep-edits",
6064
+ "Into a folder pulled before: keep every file you edited since that pull and refresh the rest. From a head, a later push sends those edits on top of the new revision."
6065
+ );
6066
+ }
6067
+ if (command.files === "push") {
6042
6068
  leaf.option(
6043
6069
  "--from <dir>",
6044
6070
  "The pulled head folder to push. Defaults to the current folder."
6045
6071
  );
6072
+ leaf.option(
6073
+ "--rebase",
6074
+ "When the head moved since your pull, pull it with --keep-edits and push again, twice at most."
6075
+ );
6076
+ }
6046
6077
  }
6047
6078
  function toParsed(leaf, positionals, dotted2) {
6048
6079
  const globals = leaf.optsWithGlobals();
@@ -6649,7 +6680,7 @@ var ApiError = class extends Error {
6649
6680
  status;
6650
6681
  body;
6651
6682
  };
6652
- var VERSION = "1.9.96";
6683
+ var VERSION = "1.9.99";
6653
6684
  function problem(status, body, fallback) {
6654
6685
  if (body && typeof body === "object") {
6655
6686
  const record2 = body;
@@ -7749,6 +7780,18 @@ function nodeFiles(cwd) {
7749
7780
  };
7750
7781
  }
7751
7782
 
7783
+ // src/sync/manifest.ts
7784
+ async function loadManifest(sync, dir) {
7785
+ const bytes = await sync.files.read(join3(dir, MANIFEST));
7786
+ if (!bytes) return void 0;
7787
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
7788
+ }
7789
+ async function writeManifest(sync, dir, manifest) {
7790
+ const text3 = `${JSON.stringify(manifest, null, 2)}
7791
+ `;
7792
+ await sync.files.write(join3(dir, MANIFEST), Buffer.from(text3, "utf8"));
7793
+ }
7794
+
7752
7795
  // src/sync/workflow-pull.ts
7753
7796
  var CONCURRENCY = 16;
7754
7797
  function pullDir(root, tree, head) {
@@ -7756,6 +7799,9 @@ function pullDir(root, tree, head) {
7756
7799
  if (head) return join3(base, "head");
7757
7800
  return join3(base, ".versions", workflowDirName(tree.versionId, tree.version));
7758
7801
  }
7802
+ function headFilesRequest(workflowId) {
7803
+ return get(`/agents/v2/workflows/${workflowId}/head/files`);
7804
+ }
7759
7805
  async function agentFileBytes(sync, workflowId, tree, entry) {
7760
7806
  const path = `/agents/v2/workflows/${workflowId}/versions/${tree.versionId}/files/${entry.fileId}/signed-url`;
7761
7807
  const { url } = await sync.remote.json(get(path));
@@ -7768,29 +7814,100 @@ async function entryBytes(sync, workflowId, tree, entry) {
7768
7814
  if (!entry.fileId) return void 0;
7769
7815
  return agentFileBytes(sync, workflowId, tree, entry);
7770
7816
  }
7771
- async function writeEntries(sync, options, tree, dir) {
7772
- const hashes = {};
7773
- const skipped = [];
7774
- let shared = 0;
7775
- const wanted = tree.files.filter((entry) => {
7817
+ function inlineBytes(entry) {
7818
+ if (entry.contentBase64 !== void 0)
7819
+ return Buffer.from(entry.contentBase64, "base64");
7820
+ if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
7821
+ return void 0;
7822
+ }
7823
+ function wantedEntries(options, tree, skipped) {
7824
+ return tree.files.filter((entry) => {
7776
7825
  if (!isContained(entry.path)) skipped.push(`${entry.path} (unsafe path)`);
7777
7826
  else if (options.maxFileBytes !== void 0 && entry.size > options.maxFileBytes)
7778
7827
  skipped.push(`${entry.path} (${entry.size} bytes, over the size limit)`);
7779
7828
  else return true;
7780
7829
  return false;
7781
7830
  });
7782
- for (let i = 0; i < wanted.length; i += CONCURRENCY) {
7783
- await Promise.all(
7784
- wanted.slice(i, i + CONCURRENCY).map(async (entry) => {
7785
- const bytes = await entryBytes(sync, options.workflowId, tree, entry);
7786
- if (!bytes) return void skipped.push(`${entry.path} (no content)`);
7787
- await sync.files.write(join3(dir, entry.path), bytes);
7788
- hashes[entry.path] = sha256(bytes);
7789
- if (entry.kind === "agent_file") shared++;
7790
- })
7791
- );
7831
+ }
7832
+ async function inBatches(items, each) {
7833
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
7834
+ await Promise.all(items.slice(i, i + CONCURRENCY).map(each));
7792
7835
  }
7793
- return { hashes, skipped, shared };
7836
+ }
7837
+ async function writeEntries(sync, options, tree, dir) {
7838
+ const hashes = {};
7839
+ const skipped = [];
7840
+ let shared = 0;
7841
+ let written = 0;
7842
+ await inBatches(wantedEntries(options, tree, skipped), async (entry) => {
7843
+ const bytes = await entryBytes(sync, options.workflowId, tree, entry);
7844
+ if (!bytes) return void skipped.push(`${entry.path} (no content)`);
7845
+ await sync.files.write(join3(dir, entry.path), bytes);
7846
+ hashes[entry.path] = sha256(bytes);
7847
+ written++;
7848
+ if (entry.kind === "agent_file") shared++;
7849
+ });
7850
+ return { hashes, skipped, shared, kept: [], written };
7851
+ }
7852
+ async function mergeEntry(sync, options, tree, dir, previous, entry, out) {
7853
+ const local2 = await sync.files.read(join3(dir, entry.path));
7854
+ const localHash = local2 ? sha256(local2) : void 0;
7855
+ const baseline = previous.files[entry.path];
7856
+ const edited = baseline === void 0 ? local2 !== void 0 : localHash !== baseline;
7857
+ let bytes = inlineBytes(entry);
7858
+ let serverHash = bytes ? sha256(bytes) : entry.checksum;
7859
+ if (edited) {
7860
+ if (serverHash === void 0) {
7861
+ bytes = await entryBytes(sync, options.workflowId, tree, entry);
7862
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
7863
+ serverHash = sha256(bytes);
7864
+ }
7865
+ out.hashes[entry.path] = serverHash;
7866
+ out.kept.push(entry.path);
7867
+ return;
7868
+ }
7869
+ if (serverHash !== void 0 && serverHash === localHash) {
7870
+ out.hashes[entry.path] = serverHash;
7871
+ return;
7872
+ }
7873
+ bytes ??= await entryBytes(sync, options.workflowId, tree, entry);
7874
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
7875
+ serverHash = sha256(bytes);
7876
+ if (serverHash !== localHash) {
7877
+ await sync.files.write(join3(dir, entry.path), bytes);
7878
+ out.written++;
7879
+ if (entry.kind === "agent_file") out.shared++;
7880
+ }
7881
+ out.hashes[entry.path] = serverHash;
7882
+ }
7883
+ async function mergeEntries(sync, options, tree, dir, previous) {
7884
+ const out = {
7885
+ hashes: {},
7886
+ skipped: [],
7887
+ shared: 0,
7888
+ kept: [],
7889
+ written: 0
7890
+ };
7891
+ const wanted = wantedEntries(options, tree, out.skipped);
7892
+ await inBatches(
7893
+ wanted,
7894
+ (entry) => mergeEntry(sync, options, tree, dir, previous, entry, out)
7895
+ );
7896
+ const remote = new Set(tree.files.map((entry) => entry.path));
7897
+ for (const path of Object.keys(previous.files)) {
7898
+ if (path in out.hashes) continue;
7899
+ if (remote.has(path)) {
7900
+ out.hashes[path] = previous.files[path];
7901
+ continue;
7902
+ }
7903
+ const local2 = await sync.files.read(join3(dir, path));
7904
+ if (!local2) continue;
7905
+ if (sha256(local2) === previous.files[path])
7906
+ await sync.files.remove(join3(dir, path));
7907
+ else out.kept.push(path);
7908
+ }
7909
+ out.kept.sort();
7910
+ return out;
7794
7911
  }
7795
7912
  async function removeStale(sync, dir, keep) {
7796
7913
  for (const path of await sync.files.list(dir)) {
@@ -7798,11 +7915,6 @@ async function removeStale(sync, dir, keep) {
7798
7915
  await sync.files.remove(join3(dir, path));
7799
7916
  }
7800
7917
  }
7801
- async function writeManifest(sync, dir, manifest) {
7802
- const text3 = `${JSON.stringify(manifest, null, 2)}
7803
- `;
7804
- await sync.files.write(join3(dir, MANIFEST), Buffer.from(text3, "utf8"));
7805
- }
7806
7918
  async function pullWorkflow(sync, options) {
7807
7919
  const request = {
7808
7920
  ...options.request,
@@ -7812,20 +7924,22 @@ async function pullWorkflow(sync, options) {
7812
7924
  const tree = await sync.remote.json(request);
7813
7925
  const head = request.path.endsWith("/head/files");
7814
7926
  const dir = options.dir ?? pullDir(options.root ?? "", tree, head);
7815
- const { hashes, skipped, shared } = await writeEntries(
7816
- sync,
7817
- options,
7818
- tree,
7819
- dir
7820
- );
7821
- await removeStale(sync, dir, new Set(Object.keys(hashes)));
7927
+ const previous = options.keepEdits ? await loadManifest(sync, dir) : void 0;
7928
+ const written = previous ? await mergeEntries(sync, options, tree, dir, previous) : await writeEntries(sync, options, tree, dir);
7929
+ if (!previous)
7930
+ await removeStale(sync, dir, new Set(Object.keys(written.hashes)));
7822
7931
  if (options.manifest !== false) {
7932
+ const variants = request.query.get("variants") ?? void 0;
7823
7933
  await writeManifest(sync, dir, {
7824
7934
  workflowId: options.workflowId,
7825
7935
  versionId: tree.versionId,
7826
7936
  head,
7827
7937
  rev: tree.rev,
7828
- files: hashes
7938
+ files: written.hashes,
7939
+ ...variants !== void 0 && { variants },
7940
+ ...options.maxFileBytes !== void 0 && {
7941
+ maxFileBytes: options.maxFileBytes
7942
+ }
7829
7943
  });
7830
7944
  }
7831
7945
  return {
@@ -7836,9 +7950,11 @@ async function pullWorkflow(sync, options) {
7836
7950
  rev: tree.rev,
7837
7951
  dir,
7838
7952
  head,
7839
- written: Object.keys(hashes).length,
7840
- shared,
7841
- skipped
7953
+ written: written.written,
7954
+ shared: written.shared,
7955
+ skipped: written.skipped,
7956
+ kept: written.kept,
7957
+ ...tree.variantKeys && { variantKeys: tree.variantKeys }
7842
7958
  };
7843
7959
  }
7844
7960
  function formatWorkflowPull(result) {
@@ -7849,6 +7965,8 @@ function formatWorkflowPull(result) {
7849
7965
  `Workflow version: ${result.versionId}${result.version ? ` (v${result.version})` : " (draft)"}`,
7850
7966
  `Written to: ${result.dir}/`,
7851
7967
  files,
7968
+ result.kept.length > 0 ? `Kept your edits in:
7969
+ ${result.kept.map((s) => ` - ${s}`).join("\n")}` : null,
7852
7970
  result.skipped.length > 0 ? `Skipped:
7853
7971
  ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
7854
7972
  result.head ? `Edit the files, then run \`asteroid workflow push\` from ${result.dir}/.` : "This is a pinned version. It is read-only."
@@ -7856,14 +7974,14 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
7856
7974
  }
7857
7975
 
7858
7976
  // src/sync/workflow-push.ts
7977
+ var REBASE_ATTEMPTS = 3;
7859
7978
  async function readManifest(sync, dir) {
7860
- const bytes = await sync.files.read(join3(dir, MANIFEST));
7861
- if (!bytes) {
7979
+ const manifest = await loadManifest(sync, dir);
7980
+ if (!manifest) {
7862
7981
  throw new UsageError(
7863
7982
  `no pulled workflow in ${dir}. Run \`asteroid workflow pull <workflow-id>\`, then push from the head folder it writes.`
7864
7983
  );
7865
7984
  }
7866
- const manifest = JSON.parse(Buffer.from(bytes).toString("utf8"));
7867
7985
  if (!manifest.head) {
7868
7986
  throw new UsageError(
7869
7987
  `${dir} holds a pinned workflow version, which is read-only. Push from a pulled head.`
@@ -7893,25 +8011,42 @@ async function localChanges(sync, dir, manifest) {
7893
8011
  );
7894
8012
  return { writes, deletes };
7895
8013
  }
8014
+ function conflict(manifest) {
8015
+ return new UsageError(
8016
+ `the workflow head changed since your pull (rev ${manifest.rev}). Run \`asteroid workflow pull ${manifest.workflowId} --keep-edits\` to bring its changes under yours, then push again, or push with --rebase to do both.`
8017
+ );
8018
+ }
7896
8019
  async function patch(sync, manifest, writes, deletes) {
7897
8020
  try {
7898
8021
  await sync.remote.json({
7899
8022
  method: "PATCH",
7900
8023
  path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
7901
8024
  query: new URLSearchParams(),
7902
- json: { baseRev: manifest.rev, writes, deletes }
8025
+ json: {
8026
+ baseRev: manifest.rev,
8027
+ writes: writes.map(({ path, contentBase64 }) => ({
8028
+ path,
8029
+ contentBase64
8030
+ })),
8031
+ deletes
8032
+ }
7903
8033
  });
8034
+ return "applied";
7904
8035
  } catch (error) {
7905
8036
  if (statusOf(error) !== 409) throw error;
7906
- throw new UsageError(
7907
- `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.`
7908
- );
8037
+ return "conflict";
7909
8038
  }
7910
8039
  }
8040
+ function headRequest(manifest) {
8041
+ const request = headFilesRequest(manifest.workflowId);
8042
+ if (manifest.variants !== void 0)
8043
+ request.query.set("variants", manifest.variants);
8044
+ if (manifest.maxFileBytes !== void 0)
8045
+ request.query.set("maxFileBytes", String(manifest.maxFileBytes));
8046
+ return request;
8047
+ }
7911
8048
  async function refresh2(sync, dir, manifest, pushed) {
7912
- const tree = await sync.remote.json(
7913
- get(`/agents/v2/workflows/${manifest.workflowId}/head/files`)
7914
- );
8049
+ const tree = await sync.remote.json(headRequest(manifest));
7915
8050
  const files = {};
7916
8051
  for (const entry of tree.files) {
7917
8052
  if (entry.content !== void 0) {
@@ -7921,7 +8056,7 @@ async function refresh2(sync, dir, manifest, pushed) {
7921
8056
  await sync.files.write(join3(dir, entry.path), bytes);
7922
8057
  } else {
7923
8058
  const local2 = await sync.files.read(join3(dir, entry.path));
7924
- const hash = local2 ? sha256(local2) : pushed.get(entry.path) ?? entry.checksum;
8059
+ const hash = local2 ? sha256(local2) : pushed.get(entry.path);
7925
8060
  if (hash) files[entry.path] = hash;
7926
8061
  }
7927
8062
  }
@@ -7933,43 +8068,57 @@ async function refresh2(sync, dir, manifest, pushed) {
7933
8068
  await writeManifest(sync, dir, next);
7934
8069
  return next;
7935
8070
  }
7936
- async function pushWorkflow(sync, dir, workflowId) {
7937
- const manifest = await readManifest(sync, dir);
8071
+ async function rebase(sync, dir, manifest) {
8072
+ await pullWorkflow(sync, {
8073
+ workflowId: manifest.workflowId,
8074
+ request: headRequest(manifest),
8075
+ dir,
8076
+ keepEdits: true,
8077
+ maxFileBytes: manifest.maxFileBytes
8078
+ });
8079
+ return readManifest(sync, dir);
8080
+ }
8081
+ async function pushWorkflow(sync, dir, workflowId, options = {}) {
8082
+ let manifest = await readManifest(sync, dir);
7938
8083
  if (workflowId !== void 0 && manifest.workflowId !== workflowId) {
7939
8084
  throw new UsageError(
7940
8085
  `${dir} holds workflow ${manifest.workflowId}, not ${workflowId}.`
7941
8086
  );
7942
8087
  }
7943
- const { writes, deletes } = await localChanges(sync, dir, manifest);
7944
- const result = {
7945
- workflowId: manifest.workflowId,
7946
- dir,
7947
- written: writes.map((w) => w.path),
7948
- deleted: deletes
7949
- };
7950
- if (writes.length === 0 && deletes.length === 0) {
7951
- return { ...result, rev: manifest.rev, versionId: manifest.versionId };
8088
+ let rebasedFrom;
8089
+ for (let attempt = 1; ; attempt++) {
8090
+ const { writes, deletes } = await localChanges(sync, dir, manifest);
8091
+ const result = {
8092
+ workflowId: manifest.workflowId,
8093
+ dir,
8094
+ written: writes.map((w) => w.path),
8095
+ deleted: deletes,
8096
+ ...rebasedFrom !== void 0 && { rebasedFrom }
8097
+ };
8098
+ if (writes.length === 0 && deletes.length === 0) {
8099
+ return { ...result, rev: manifest.rev, versionId: manifest.versionId };
8100
+ }
8101
+ if (await patch(sync, manifest, writes, deletes) === "applied") {
8102
+ const next = await refresh2(
8103
+ sync,
8104
+ dir,
8105
+ manifest,
8106
+ new Map(writes.map((w) => [w.path, w.hash]))
8107
+ );
8108
+ return { ...result, rev: next.rev, versionId: next.versionId };
8109
+ }
8110
+ if (!options.rebase || attempt >= REBASE_ATTEMPTS) throw conflict(manifest);
8111
+ rebasedFrom ??= manifest.rev;
8112
+ manifest = await rebase(sync, dir, manifest);
7952
8113
  }
7953
- await patch(
7954
- sync,
7955
- manifest,
7956
- writes.map(({ path, contentBase64 }) => ({ path, contentBase64 })),
7957
- deletes
7958
- );
7959
- const next = await refresh2(
7960
- sync,
7961
- dir,
7962
- manifest,
7963
- new Map(writes.map((w) => [w.path, w.hash]))
7964
- );
7965
- return { ...result, rev: next.rev, versionId: next.versionId };
7966
8114
  }
7967
8115
  function formatWorkflowPush(result) {
8116
+ const rebased = result.rebasedFrom !== void 0 ? ` Rebased on the head first: your pull was rev ${result.rebasedFrom}.` : "";
7968
8117
  if (result.written.length === 0 && result.deleted.length === 0) {
7969
- return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).`;
8118
+ return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).${rebased}`;
7970
8119
  }
7971
8120
  return [
7972
- `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.`,
8121
+ `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.${rebased}`,
7973
8122
  ...result.written.map((path) => ` + ${path}`),
7974
8123
  ...result.deleted.map((path) => ` - ${path}`),
7975
8124
  `Head is now rev ${result.rev} (version ${result.versionId}).`
@@ -8000,9 +8149,9 @@ function executionOptions(parsed) {
8000
8149
  dir: dir ? () => dir : void 0
8001
8150
  };
8002
8151
  }
8003
- function withoutFlag(parsed, name) {
8152
+ function withoutFlags(parsed, ...names) {
8004
8153
  const flags = new Map(parsed.flags);
8005
- flags.delete(name);
8154
+ for (const name of names) flags.delete(name);
8006
8155
  return { ...parsed, flags };
8007
8156
  }
8008
8157
  function runsLocally(command, parsed) {
@@ -8023,11 +8172,16 @@ async function pull(command, parsed, sync, deps) {
8023
8172
  rejectPartial(parsed);
8024
8173
  const result = await pullWorkflow(sync, {
8025
8174
  workflowId: parsed.positionals[0],
8026
- request: await buildRequest(command, withoutFlag(parsed, "out"), {
8027
- io: deps.io
8028
- }),
8175
+ request: await buildRequest(
8176
+ command,
8177
+ withoutFlags(parsed, "out", "keep-edits"),
8178
+ {
8179
+ io: deps.io
8180
+ }
8181
+ ),
8029
8182
  dir: flag(parsed, "out"),
8030
8183
  root: deps.root ?? "",
8184
+ keepEdits: flag(parsed, "keep-edits") === "true",
8031
8185
  ...deps.workflowPull
8032
8186
  });
8033
8187
  return { result, text: formatWorkflowPull(result) };
@@ -8038,7 +8192,8 @@ async function execute(command, parsed, sync, deps) {
8038
8192
  const result2 = await pushWorkflow(
8039
8193
  sync,
8040
8194
  flag(parsed, "from") ?? ".",
8041
- parsed.positionals[0] || void 0
8195
+ parsed.positionals[0] || void 0,
8196
+ { rebase: flag(parsed, "rebase") === "true" }
8042
8197
  );
8043
8198
  return { result: result2, text: formatWorkflowPush(result2) };
8044
8199
  }
package/dist/core.js CHANGED
@@ -399,7 +399,7 @@ var COMMANDS = [
399
399
  flag: "executions-write-back",
400
400
  type: "boolean",
401
401
  required: false,
402
- description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. A run that uses its profile's sticky IP saves anyway."
402
+ description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. The first run that uses its profile's sticky IP saves anyway."
403
403
  }
404
404
  ]
405
405
  },
@@ -816,7 +816,7 @@ var COMMANDS = [
816
816
  flag: "executions-write-back",
817
817
  type: "boolean",
818
818
  required: false,
819
- description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. A run that uses its profile's sticky IP saves anyway."
819
+ description: "Whether executions on the environment save the browser profile when they stop. Off by default: only the login agent saves it, so a run that ends logged out cannot overwrite the session for later runs. The first run that uses its profile's sticky IP saves anyway."
820
820
  }
821
821
  ]
822
822
  },
@@ -1928,7 +1928,7 @@ var COMMANDS = [
1928
1928
  command: "portal-skills add",
1929
1929
  tool: "portal_skills_add",
1930
1930
  title: "Add portal skills",
1931
- description: "Add a portal's skills to an organization, by portal id. It is general knowledge about how a portal works. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
1931
+ description: "Add a portal's skills to an organization, by a `portalId` from `portal_skills_list`. Needs a Startup plan or above. Astro loads the portal's skills once it is added. Adding one the organization already has is a no-op.",
1932
1932
  method: "POST",
1933
1933
  path: "/agents/v2/portal-skills/{portalId}",
1934
1934
  args: [
@@ -1944,6 +1944,27 @@ var COMMANDS = [
1944
1944
  readOnly: false,
1945
1945
  destructive: false
1946
1946
  },
1947
+ {
1948
+ command: "portal-skills list",
1949
+ tool: "portal_skills_list",
1950
+ title: "List portal skills",
1951
+ description: "List the skills for third-party portals, or only for the portals matching a portal name or task, such as `caloptima eligibility`. A portal skill is general knowledge about how a portal works: sign-in, page quirks, field mappings. Each portal comes with its skill names, and whether the organization has added it. Add one with `portal_skills_add`.",
1952
+ method: "GET",
1953
+ path: "/astro/v2/portal-skills",
1954
+ args: [],
1955
+ query: [
1956
+ {
1957
+ name: "query",
1958
+ flag: "query",
1959
+ type: "string",
1960
+ required: false,
1961
+ description: "A portal name or task, at least three characters. Omit to list every portal with skills."
1962
+ }
1963
+ ],
1964
+ binary: false,
1965
+ readOnly: true,
1966
+ destructive: false
1967
+ },
1947
1968
  {
1948
1969
  command: "profile clear-cache",
1949
1970
  tool: "profile_clear_browser_cache",
@@ -4462,7 +4483,7 @@ var COMMANDS = [
4462
4483
  command: "workflow head publish",
4463
4484
  tool: "workflow_head_publish",
4464
4485
  title: "Publish workflow head",
4465
- description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of.",
4486
+ description: "Publish the workflow's editable head, assigning it the next version number and making it the default for `workflow_execute_post`. Use after `workflow_files_patch`; `workflow_version_publish` does the same for a version you already have the ID of. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
4466
4487
  method: "POST",
4467
4488
  path: "/agents/v2/workflows/{workflowId}/head/publish",
4468
4489
  args: [
@@ -5399,7 +5420,7 @@ var COMMANDS = [
5399
5420
  command: "workflow version publish",
5400
5421
  tool: "workflow_version_publish",
5401
5422
  title: "Publish workflow version",
5402
- description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`.",
5423
+ description: "Publish a workflow version, assigning it a permanent version number and making it the default for `workflow_execute_post`. This also publishes an older version again. `schemaChanges` lists the input and output schema changes against the version published before, which can break callers that send inputs or read outputs by name.",
5403
5424
  method: "POST",
5404
5425
  path: "/agents/v2/workflows/{workflowId}/versions/{versionId}/publish",
5405
5426
  args: [
@@ -5903,16 +5924,26 @@ function leafOptions(leaf, command) {
5903
5924
  for (const file of command.body?.files ?? []) {
5904
5925
  leaf.option(`--${file} <path>`, "File to upload. Repeatable.", collect);
5905
5926
  }
5906
- if (command.files === "pull")
5927
+ if (command.files === "pull") {
5907
5928
  leaf.option(
5908
5929
  "--out <dir>",
5909
5930
  "Write here instead of ./<workflow-name>/head (a version: ./<workflow-name>/.versions/...). --json prints the file listing instead."
5910
5931
  );
5911
- if (command.files === "push")
5932
+ leaf.option(
5933
+ "--keep-edits",
5934
+ "Into a folder pulled before: keep every file you edited since that pull and refresh the rest. From a head, a later push sends those edits on top of the new revision."
5935
+ );
5936
+ }
5937
+ if (command.files === "push") {
5912
5938
  leaf.option(
5913
5939
  "--from <dir>",
5914
5940
  "The pulled head folder to push. Defaults to the current folder."
5915
5941
  );
5942
+ leaf.option(
5943
+ "--rebase",
5944
+ "When the head moved since your pull, pull it with --keep-edits and push again, twice at most."
5945
+ );
5946
+ }
5916
5947
  }
5917
5948
  function toParsed(leaf, positionals, dotted2) {
5918
5949
  const globals = leaf.optsWithGlobals();
@@ -6703,6 +6734,18 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6703
6734
  ].filter(Boolean).join("\n");
6704
6735
  }
6705
6736
 
6737
+ // src/sync/manifest.ts
6738
+ async function loadManifest(sync, dir) {
6739
+ const bytes = await sync.files.read(join(dir, MANIFEST));
6740
+ if (!bytes) return void 0;
6741
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
6742
+ }
6743
+ async function writeManifest(sync, dir, manifest) {
6744
+ const text2 = `${JSON.stringify(manifest, null, 2)}
6745
+ `;
6746
+ await sync.files.write(join(dir, MANIFEST), Buffer.from(text2, "utf8"));
6747
+ }
6748
+
6706
6749
  // src/sync/node-files.ts
6707
6750
  import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
6708
6751
  import { dirname, relative, resolve, sep } from "node:path";
@@ -6754,6 +6797,9 @@ function pullDir(root, tree, head) {
6754
6797
  if (head) return join(base, "head");
6755
6798
  return join(base, ".versions", workflowDirName(tree.versionId, tree.version));
6756
6799
  }
6800
+ function headFilesRequest(workflowId) {
6801
+ return get(`/agents/v2/workflows/${workflowId}/head/files`);
6802
+ }
6757
6803
  async function agentFileBytes(sync, workflowId, tree, entry) {
6758
6804
  const path = `/agents/v2/workflows/${workflowId}/versions/${tree.versionId}/files/${entry.fileId}/signed-url`;
6759
6805
  const { url } = await sync.remote.json(get(path));
@@ -6766,29 +6812,100 @@ async function entryBytes(sync, workflowId, tree, entry) {
6766
6812
  if (!entry.fileId) return void 0;
6767
6813
  return agentFileBytes(sync, workflowId, tree, entry);
6768
6814
  }
6769
- async function writeEntries(sync, options, tree, dir) {
6770
- const hashes = {};
6771
- const skipped = [];
6772
- let shared = 0;
6773
- const wanted = tree.files.filter((entry) => {
6815
+ function inlineBytes(entry) {
6816
+ if (entry.contentBase64 !== void 0)
6817
+ return Buffer.from(entry.contentBase64, "base64");
6818
+ if (entry.content !== void 0) return Buffer.from(entry.content, "utf8");
6819
+ return void 0;
6820
+ }
6821
+ function wantedEntries(options, tree, skipped) {
6822
+ return tree.files.filter((entry) => {
6774
6823
  if (!isContained(entry.path)) skipped.push(`${entry.path} (unsafe path)`);
6775
6824
  else if (options.maxFileBytes !== void 0 && entry.size > options.maxFileBytes)
6776
6825
  skipped.push(`${entry.path} (${entry.size} bytes, over the size limit)`);
6777
6826
  else return true;
6778
6827
  return false;
6779
6828
  });
6780
- for (let i = 0; i < wanted.length; i += CONCURRENCY) {
6781
- await Promise.all(
6782
- wanted.slice(i, i + CONCURRENCY).map(async (entry) => {
6783
- const bytes = await entryBytes(sync, options.workflowId, tree, entry);
6784
- if (!bytes) return void skipped.push(`${entry.path} (no content)`);
6785
- await sync.files.write(join(dir, entry.path), bytes);
6786
- hashes[entry.path] = sha256(bytes);
6787
- if (entry.kind === "agent_file") shared++;
6788
- })
6789
- );
6829
+ }
6830
+ async function inBatches(items, each) {
6831
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
6832
+ await Promise.all(items.slice(i, i + CONCURRENCY).map(each));
6790
6833
  }
6791
- return { hashes, skipped, shared };
6834
+ }
6835
+ async function writeEntries(sync, options, tree, dir) {
6836
+ const hashes = {};
6837
+ const skipped = [];
6838
+ let shared = 0;
6839
+ let written = 0;
6840
+ await inBatches(wantedEntries(options, tree, skipped), async (entry) => {
6841
+ const bytes = await entryBytes(sync, options.workflowId, tree, entry);
6842
+ if (!bytes) return void skipped.push(`${entry.path} (no content)`);
6843
+ await sync.files.write(join(dir, entry.path), bytes);
6844
+ hashes[entry.path] = sha256(bytes);
6845
+ written++;
6846
+ if (entry.kind === "agent_file") shared++;
6847
+ });
6848
+ return { hashes, skipped, shared, kept: [], written };
6849
+ }
6850
+ async function mergeEntry(sync, options, tree, dir, previous, entry, out) {
6851
+ const local2 = await sync.files.read(join(dir, entry.path));
6852
+ const localHash = local2 ? sha256(local2) : void 0;
6853
+ const baseline = previous.files[entry.path];
6854
+ const edited = baseline === void 0 ? local2 !== void 0 : localHash !== baseline;
6855
+ let bytes = inlineBytes(entry);
6856
+ let serverHash = bytes ? sha256(bytes) : entry.checksum;
6857
+ if (edited) {
6858
+ if (serverHash === void 0) {
6859
+ bytes = await entryBytes(sync, options.workflowId, tree, entry);
6860
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
6861
+ serverHash = sha256(bytes);
6862
+ }
6863
+ out.hashes[entry.path] = serverHash;
6864
+ out.kept.push(entry.path);
6865
+ return;
6866
+ }
6867
+ if (serverHash !== void 0 && serverHash === localHash) {
6868
+ out.hashes[entry.path] = serverHash;
6869
+ return;
6870
+ }
6871
+ bytes ??= await entryBytes(sync, options.workflowId, tree, entry);
6872
+ if (!bytes) return void out.skipped.push(`${entry.path} (no content)`);
6873
+ serverHash = sha256(bytes);
6874
+ if (serverHash !== localHash) {
6875
+ await sync.files.write(join(dir, entry.path), bytes);
6876
+ out.written++;
6877
+ if (entry.kind === "agent_file") out.shared++;
6878
+ }
6879
+ out.hashes[entry.path] = serverHash;
6880
+ }
6881
+ async function mergeEntries(sync, options, tree, dir, previous) {
6882
+ const out = {
6883
+ hashes: {},
6884
+ skipped: [],
6885
+ shared: 0,
6886
+ kept: [],
6887
+ written: 0
6888
+ };
6889
+ const wanted = wantedEntries(options, tree, out.skipped);
6890
+ await inBatches(
6891
+ wanted,
6892
+ (entry) => mergeEntry(sync, options, tree, dir, previous, entry, out)
6893
+ );
6894
+ const remote = new Set(tree.files.map((entry) => entry.path));
6895
+ for (const path of Object.keys(previous.files)) {
6896
+ if (path in out.hashes) continue;
6897
+ if (remote.has(path)) {
6898
+ out.hashes[path] = previous.files[path];
6899
+ continue;
6900
+ }
6901
+ const local2 = await sync.files.read(join(dir, path));
6902
+ if (!local2) continue;
6903
+ if (sha256(local2) === previous.files[path])
6904
+ await sync.files.remove(join(dir, path));
6905
+ else out.kept.push(path);
6906
+ }
6907
+ out.kept.sort();
6908
+ return out;
6792
6909
  }
6793
6910
  async function removeStale(sync, dir, keep) {
6794
6911
  for (const path of await sync.files.list(dir)) {
@@ -6796,11 +6913,6 @@ async function removeStale(sync, dir, keep) {
6796
6913
  await sync.files.remove(join(dir, path));
6797
6914
  }
6798
6915
  }
6799
- async function writeManifest(sync, dir, manifest) {
6800
- const text2 = `${JSON.stringify(manifest, null, 2)}
6801
- `;
6802
- await sync.files.write(join(dir, MANIFEST), Buffer.from(text2, "utf8"));
6803
- }
6804
6916
  async function pullWorkflow(sync, options) {
6805
6917
  const request = {
6806
6918
  ...options.request,
@@ -6810,20 +6922,22 @@ async function pullWorkflow(sync, options) {
6810
6922
  const tree = await sync.remote.json(request);
6811
6923
  const head = request.path.endsWith("/head/files");
6812
6924
  const dir = options.dir ?? pullDir(options.root ?? "", tree, head);
6813
- const { hashes, skipped, shared } = await writeEntries(
6814
- sync,
6815
- options,
6816
- tree,
6817
- dir
6818
- );
6819
- await removeStale(sync, dir, new Set(Object.keys(hashes)));
6925
+ const previous = options.keepEdits ? await loadManifest(sync, dir) : void 0;
6926
+ const written = previous ? await mergeEntries(sync, options, tree, dir, previous) : await writeEntries(sync, options, tree, dir);
6927
+ if (!previous)
6928
+ await removeStale(sync, dir, new Set(Object.keys(written.hashes)));
6820
6929
  if (options.manifest !== false) {
6930
+ const variants = request.query.get("variants") ?? void 0;
6821
6931
  await writeManifest(sync, dir, {
6822
6932
  workflowId: options.workflowId,
6823
6933
  versionId: tree.versionId,
6824
6934
  head,
6825
6935
  rev: tree.rev,
6826
- files: hashes
6936
+ files: written.hashes,
6937
+ ...variants !== void 0 && { variants },
6938
+ ...options.maxFileBytes !== void 0 && {
6939
+ maxFileBytes: options.maxFileBytes
6940
+ }
6827
6941
  });
6828
6942
  }
6829
6943
  return {
@@ -6834,9 +6948,11 @@ async function pullWorkflow(sync, options) {
6834
6948
  rev: tree.rev,
6835
6949
  dir,
6836
6950
  head,
6837
- written: Object.keys(hashes).length,
6838
- shared,
6839
- skipped
6951
+ written: written.written,
6952
+ shared: written.shared,
6953
+ skipped: written.skipped,
6954
+ kept: written.kept,
6955
+ ...tree.variantKeys && { variantKeys: tree.variantKeys }
6840
6956
  };
6841
6957
  }
6842
6958
  function formatWorkflowPull(result) {
@@ -6847,6 +6963,8 @@ function formatWorkflowPull(result) {
6847
6963
  `Workflow version: ${result.versionId}${result.version ? ` (v${result.version})` : " (draft)"}`,
6848
6964
  `Written to: ${result.dir}/`,
6849
6965
  files,
6966
+ result.kept.length > 0 ? `Kept your edits in:
6967
+ ${result.kept.map((s) => ` - ${s}`).join("\n")}` : null,
6850
6968
  result.skipped.length > 0 ? `Skipped:
6851
6969
  ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6852
6970
  result.head ? `Edit the files, then run \`asteroid workflow push\` from ${result.dir}/.` : "This is a pinned version. It is read-only."
@@ -6854,14 +6972,14 @@ ${result.skipped.map((s) => ` - ${s}`).join("\n")}` : null,
6854
6972
  }
6855
6973
 
6856
6974
  // src/sync/workflow-push.ts
6975
+ var REBASE_ATTEMPTS = 3;
6857
6976
  async function readManifest(sync, dir) {
6858
- const bytes = await sync.files.read(join(dir, MANIFEST));
6859
- if (!bytes) {
6977
+ const manifest = await loadManifest(sync, dir);
6978
+ if (!manifest) {
6860
6979
  throw new UsageError(
6861
6980
  `no pulled workflow in ${dir}. Run \`asteroid workflow pull <workflow-id>\`, then push from the head folder it writes.`
6862
6981
  );
6863
6982
  }
6864
- const manifest = JSON.parse(Buffer.from(bytes).toString("utf8"));
6865
6983
  if (!manifest.head) {
6866
6984
  throw new UsageError(
6867
6985
  `${dir} holds a pinned workflow version, which is read-only. Push from a pulled head.`
@@ -6891,25 +7009,42 @@ async function localChanges(sync, dir, manifest) {
6891
7009
  );
6892
7010
  return { writes, deletes };
6893
7011
  }
7012
+ function conflict(manifest) {
7013
+ return new UsageError(
7014
+ `the workflow head changed since your pull (rev ${manifest.rev}). Run \`asteroid workflow pull ${manifest.workflowId} --keep-edits\` to bring its changes under yours, then push again, or push with --rebase to do both.`
7015
+ );
7016
+ }
6894
7017
  async function patch(sync, manifest, writes, deletes) {
6895
7018
  try {
6896
7019
  await sync.remote.json({
6897
7020
  method: "PATCH",
6898
7021
  path: `/agents/v2/workflows/${manifest.workflowId}/head/files`,
6899
7022
  query: new URLSearchParams(),
6900
- json: { baseRev: manifest.rev, writes, deletes }
7023
+ json: {
7024
+ baseRev: manifest.rev,
7025
+ writes: writes.map(({ path, contentBase64 }) => ({
7026
+ path,
7027
+ contentBase64
7028
+ })),
7029
+ deletes
7030
+ }
6901
7031
  });
7032
+ return "applied";
6902
7033
  } catch (error) {
6903
7034
  if (statusOf(error) !== 409) throw error;
6904
- throw new UsageError(
6905
- `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.`
6906
- );
7035
+ return "conflict";
6907
7036
  }
6908
7037
  }
7038
+ function headRequest(manifest) {
7039
+ const request = headFilesRequest(manifest.workflowId);
7040
+ if (manifest.variants !== void 0)
7041
+ request.query.set("variants", manifest.variants);
7042
+ if (manifest.maxFileBytes !== void 0)
7043
+ request.query.set("maxFileBytes", String(manifest.maxFileBytes));
7044
+ return request;
7045
+ }
6909
7046
  async function refresh(sync, dir, manifest, pushed) {
6910
- const tree = await sync.remote.json(
6911
- get(`/agents/v2/workflows/${manifest.workflowId}/head/files`)
6912
- );
7047
+ const tree = await sync.remote.json(headRequest(manifest));
6913
7048
  const files = {};
6914
7049
  for (const entry of tree.files) {
6915
7050
  if (entry.content !== void 0) {
@@ -6919,7 +7054,7 @@ async function refresh(sync, dir, manifest, pushed) {
6919
7054
  await sync.files.write(join(dir, entry.path), bytes);
6920
7055
  } else {
6921
7056
  const local2 = await sync.files.read(join(dir, entry.path));
6922
- const hash = local2 ? sha256(local2) : pushed.get(entry.path) ?? entry.checksum;
7057
+ const hash = local2 ? sha256(local2) : pushed.get(entry.path);
6923
7058
  if (hash) files[entry.path] = hash;
6924
7059
  }
6925
7060
  }
@@ -6931,43 +7066,57 @@ async function refresh(sync, dir, manifest, pushed) {
6931
7066
  await writeManifest(sync, dir, next);
6932
7067
  return next;
6933
7068
  }
6934
- async function pushWorkflow(sync, dir, workflowId) {
6935
- const manifest = await readManifest(sync, dir);
7069
+ async function rebase(sync, dir, manifest) {
7070
+ await pullWorkflow(sync, {
7071
+ workflowId: manifest.workflowId,
7072
+ request: headRequest(manifest),
7073
+ dir,
7074
+ keepEdits: true,
7075
+ maxFileBytes: manifest.maxFileBytes
7076
+ });
7077
+ return readManifest(sync, dir);
7078
+ }
7079
+ async function pushWorkflow(sync, dir, workflowId, options = {}) {
7080
+ let manifest = await readManifest(sync, dir);
6936
7081
  if (workflowId !== void 0 && manifest.workflowId !== workflowId) {
6937
7082
  throw new UsageError(
6938
7083
  `${dir} holds workflow ${manifest.workflowId}, not ${workflowId}.`
6939
7084
  );
6940
7085
  }
6941
- const { writes, deletes } = await localChanges(sync, dir, manifest);
6942
- const result = {
6943
- workflowId: manifest.workflowId,
6944
- dir,
6945
- written: writes.map((w) => w.path),
6946
- deleted: deletes
6947
- };
6948
- if (writes.length === 0 && deletes.length === 0) {
6949
- return { ...result, rev: manifest.rev, versionId: manifest.versionId };
7086
+ let rebasedFrom;
7087
+ for (let attempt = 1; ; attempt++) {
7088
+ const { writes, deletes } = await localChanges(sync, dir, manifest);
7089
+ const result = {
7090
+ workflowId: manifest.workflowId,
7091
+ dir,
7092
+ written: writes.map((w) => w.path),
7093
+ deleted: deletes,
7094
+ ...rebasedFrom !== void 0 && { rebasedFrom }
7095
+ };
7096
+ if (writes.length === 0 && deletes.length === 0) {
7097
+ return { ...result, rev: manifest.rev, versionId: manifest.versionId };
7098
+ }
7099
+ if (await patch(sync, manifest, writes, deletes) === "applied") {
7100
+ const next = await refresh(
7101
+ sync,
7102
+ dir,
7103
+ manifest,
7104
+ new Map(writes.map((w) => [w.path, w.hash]))
7105
+ );
7106
+ return { ...result, rev: next.rev, versionId: next.versionId };
7107
+ }
7108
+ if (!options.rebase || attempt >= REBASE_ATTEMPTS) throw conflict(manifest);
7109
+ rebasedFrom ??= manifest.rev;
7110
+ manifest = await rebase(sync, dir, manifest);
6950
7111
  }
6951
- await patch(
6952
- sync,
6953
- manifest,
6954
- writes.map(({ path, contentBase64 }) => ({ path, contentBase64 })),
6955
- deletes
6956
- );
6957
- const next = await refresh(
6958
- sync,
6959
- dir,
6960
- manifest,
6961
- new Map(writes.map((w) => [w.path, w.hash]))
6962
- );
6963
- return { ...result, rev: next.rev, versionId: next.versionId };
6964
7112
  }
6965
7113
  function formatWorkflowPush(result) {
7114
+ const rebased = result.rebasedFrom !== void 0 ? ` Rebased on the head first: your pull was rev ${result.rebasedFrom}.` : "";
6966
7115
  if (result.written.length === 0 && result.deleted.length === 0) {
6967
- return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).`;
7116
+ return `Nothing to push: ${result.dir}/ matches the head you pulled (rev ${result.rev}).${rebased}`;
6968
7117
  }
6969
7118
  return [
6970
- `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.`,
7119
+ `Pushed to workflow ${result.workflowId}: ${result.written.length} written, ${result.deleted.length} deleted.${rebased}`,
6971
7120
  ...result.written.map((path) => ` + ${path}`),
6972
7121
  ...result.deleted.map((path) => ` - ${path}`),
6973
7122
  `Head is now rev ${result.rev} (version ${result.versionId}).`
@@ -6998,11 +7147,17 @@ function executionOptions(parsed) {
6998
7147
  dir: dir ? () => dir : void 0
6999
7148
  };
7000
7149
  }
7001
- function withoutFlag(parsed, name) {
7150
+ function withoutFlags(parsed, ...names) {
7002
7151
  const flags = new Map(parsed.flags);
7003
- flags.delete(name);
7152
+ for (const name of names) flags.delete(name);
7004
7153
  return { ...parsed, flags };
7005
7154
  }
7155
+ function runsLocally(command, parsed) {
7156
+ if (command.local) return true;
7157
+ if (command.files === "pull") return !parsed.json;
7158
+ if (command.files === "push") return parsed.body === void 0;
7159
+ return false;
7160
+ }
7006
7161
  var PARTIAL = ["paths", "variants"];
7007
7162
  function rejectPartial(parsed) {
7008
7163
  const used = PARTIAL.filter((name) => parsed.flags.has(name));
@@ -7015,11 +7170,16 @@ async function pull(command, parsed, sync, deps) {
7015
7170
  rejectPartial(parsed);
7016
7171
  const result = await pullWorkflow(sync, {
7017
7172
  workflowId: parsed.positionals[0],
7018
- request: await buildRequest(command, withoutFlag(parsed, "out"), {
7019
- io: deps.io
7020
- }),
7173
+ request: await buildRequest(
7174
+ command,
7175
+ withoutFlags(parsed, "out", "keep-edits"),
7176
+ {
7177
+ io: deps.io
7178
+ }
7179
+ ),
7021
7180
  dir: flag(parsed, "out"),
7022
7181
  root: deps.root ?? "",
7182
+ keepEdits: flag(parsed, "keep-edits") === "true",
7023
7183
  ...deps.workflowPull
7024
7184
  });
7025
7185
  return { result, text: formatWorkflowPull(result) };
@@ -7030,7 +7190,8 @@ async function execute(command, parsed, sync, deps) {
7030
7190
  const result2 = await pushWorkflow(
7031
7191
  sync,
7032
7192
  flag(parsed, "from") ?? ".",
7033
- parsed.positionals[0] || void 0
7193
+ parsed.positionals[0] || void 0,
7194
+ { rebase: flag(parsed, "rebase") === "true" }
7034
7195
  );
7035
7196
  return { result: result2, text: formatWorkflowPush(result2) };
7036
7197
  }
@@ -7064,14 +7225,20 @@ export {
7064
7225
  formatExecutionPull,
7065
7226
  formatWorkflowPull,
7066
7227
  formatWorkflowPush,
7228
+ headFilesRequest,
7067
7229
  leadingWords,
7230
+ loadManifest,
7068
7231
  matchCommand,
7069
7232
  matchesGlob,
7070
7233
  parseInvocation,
7234
+ pullDir,
7071
7235
  pullExecution,
7072
7236
  pullWorkflow,
7073
7237
  pushWorkflow,
7238
+ readManifest,
7074
7239
  render,
7075
7240
  runSync,
7076
- withSyncCommands
7241
+ runsLocally,
7242
+ withSyncCommands,
7243
+ writeManifest
7077
7244
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteroidai/cli",
3
- "version": "1.9.96",
3
+ "version": "1.9.99",
4
4
  "description": "The asteroid command-line interface for the Asteroid platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",