@astrale-os/cli 0.8.1-alpha.7 → 1.0.0-beta.1

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 (68) hide show
  1. package/README.md +15 -3
  2. package/dist/astrale.js +113 -56
  3. package/package.json +5 -5
  4. package/src/admin/instance/__tests__/client.test.ts +3 -1
  5. package/src/admin/instance/client.ts +3 -16
  6. package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
  7. package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
  8. package/src/commands/__tests__/install-direct.test.ts +3 -2
  9. package/src/commands/__tests__/update.test.ts +66 -0
  10. package/src/commands/domain/install.ts +78 -15
  11. package/src/commands/update.ts +32 -10
  12. package/src/connection/__tests__/errors.test.ts +82 -6
  13. package/src/connection/command.ts +9 -1
  14. package/src/connection/errors.ts +19 -8
  15. package/src/connection/index.ts +1 -1
  16. package/src/connection/reasons.ts +26 -12
  17. package/src/program/__tests__/program.test.ts +1 -1
  18. package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
  19. package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
  20. package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
  21. package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
  22. package/studio/client/dist/index.html +2 -2
  23. package/studio/package.json +1 -1
  24. package/studio/server/agent/prompts/anchors.test.ts +74 -0
  25. package/studio/server/agent/prompts/anchors.ts +85 -15
  26. package/studio/server/agent/prompts/system.test.ts +12 -0
  27. package/studio/server/agent/prompts/system.ts +6 -6
  28. package/studio/server/api.ts +1 -5
  29. package/studio/server/cache.ts +5 -2
  30. package/studio/server/domain.test.ts +67 -0
  31. package/studio/server/domain.ts +29 -6
  32. package/studio/server/index.ts +1 -3
  33. package/studio/server/introspect/anatomy-extras.test.ts +104 -1
  34. package/studio/server/introspect/anatomy-extras.ts +340 -8
  35. package/studio/server/introspect/anatomy.test.ts +33 -0
  36. package/studio/server/introspect/anatomy.ts +22 -7
  37. package/studio/server/introspect/bundle.ts +7 -1
  38. package/studio/server/introspect/canonical-schema.test.ts +395 -0
  39. package/studio/server/introspect/canonical-schema.ts +751 -0
  40. package/studio/server/introspect/core-extractor.ts +30 -10
  41. package/studio/server/introspect/core.ts +4 -2
  42. package/studio/server/introspect/diff.test.ts +124 -0
  43. package/studio/server/introspect/diff.ts +252 -23
  44. package/studio/server/introspect/extractor.ts +46 -14
  45. package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
  46. package/studio/server/introspect/overlay-tsmorph.ts +381 -106
  47. package/studio/server/introspect/overlay.test.ts +72 -0
  48. package/studio/server/introspect/overlay.ts +16 -6
  49. package/studio/server/introspect/runtime.test.ts +217 -0
  50. package/studio/server/introspect/runtime.ts +18 -6
  51. package/studio/server/introspect/schema-refs.test.ts +100 -0
  52. package/studio/server/introspect/schema-refs.ts +23 -2
  53. package/studio/server/state/baseline.test.ts +51 -0
  54. package/studio/server/state/baseline.ts +31 -2
  55. package/studio/server/state/create.test.ts +37 -0
  56. package/studio/server/state/create.ts +21 -15
  57. package/studio/server/state/instance.test.ts +63 -0
  58. package/studio/server/state/instance.ts +65 -29
  59. package/studio/server/state/views.test.ts +209 -9
  60. package/studio/server/state/views.ts +176 -69
  61. package/studio/server/watch.test.ts +36 -0
  62. package/studio/server/watch.ts +24 -16
  63. package/studio/server/workspace-watch.ts +8 -3
  64. package/studio/shared/types.ts +164 -33
  65. package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
  66. package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
  67. package/studio/server/view-dev-server.test.ts +0 -111
  68. package/studio/server/view-dev-server.ts +0 -372
package/README.md CHANGED
@@ -13,6 +13,12 @@ curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | sh
13
13
  The installer places a verified standalone binary at
14
14
  `~/.astrale/bin/astrale` by default. No local runtime is installed.
15
15
 
16
+ During the prerelease period, install the current standalone beta explicitly:
17
+
18
+ ```bash
19
+ curl -fsSL https://raw.githubusercontent.com/astrale-os/cli/main/install.sh | ASTRALE_CHANNEL=beta sh
20
+ ```
21
+
16
22
  Optional installer environment:
17
23
 
18
24
  ```bash
@@ -29,6 +35,12 @@ ships the [Domain Studio](studio/README.md) (`astrale studio`):
29
35
  npm install -g @astrale-os/cli
30
36
  ```
31
37
 
38
+ During the prerelease period, install the current beta explicitly:
39
+
40
+ ```bash
41
+ npm install -g @astrale-os/cli@beta
42
+ ```
43
+
32
44
  The npm build bundles all dependencies, so no private-registry access is needed.
33
45
  `astrale studio` launches a local web GUI to author and inspect a domain, with
34
46
  local Claude Code and Codex agent harnesses (`--harness claude|codex`); it runs on
@@ -36,9 +48,9 @@ local Claude Code and Codex agent harnesses (`--harness claude|codex`); it runs
36
48
  (the rest of the CLI needs only Node ≥ 22). The curl-installed standalone binary
37
49
  above does not include the studio — use the npm install for `astrale studio`.
38
50
 
39
- > Pre-1.0, `npm install -g @astrale-os/cli` installs the latest published
40
- > build (currently an alpha). The curl installer's channels (alpha/beta/rc) are
41
- > a binary-only concept.
51
+ > Prereleases use their matching npm dist-tag (`alpha`, `beta`, or `rc`); bare
52
+ > `npm install -g @astrale-os/cli` follows the stable `latest` tag. The curl
53
+ > installer has separate binary channels with the same names.
42
54
 
43
55
  ## Quickstart
44
56
 
package/dist/astrale.js CHANGED
@@ -9738,7 +9738,7 @@ var package_default;
9738
9738
  var init_package = __esm(() => {
9739
9739
  package_default = {
9740
9740
  name: "@astrale-os/cli",
9741
- version: "0.8.1-alpha.7",
9741
+ version: "1.0.0-beta.1",
9742
9742
  description: "Astrale CLI — connect to existing Astrale kernels",
9743
9743
  keywords: [
9744
9744
  "astrale",
@@ -9799,7 +9799,7 @@ var init_package = __esm(() => {
9799
9799
  "lint:fix": "oxlint --fix .",
9800
9800
  format: "pnpm exec oxfmt --write .",
9801
9801
  "format:check": "pnpm exec oxfmt --check .",
9802
- test: "bun test src",
9802
+ test: "bun test src && node --test scripts/*.test.mjs",
9803
9803
  "test:watch": "bun test --watch src"
9804
9804
  },
9805
9805
  dependencies: {
@@ -9814,10 +9814,10 @@ var init_package = __esm(() => {
9814
9814
  zod: "^3.25.0"
9815
9815
  },
9816
9816
  devDependencies: {
9817
- "@astrale-os/kernel-client": "0.6.0-beta.3",
9817
+ "@astrale-os/kernel-client": ">=0.6.0-beta.3 <1.0.0",
9818
9818
  "@astrale-os/ox": ">=0.1.0 <1.0.0",
9819
- "@astrale-os/sdk": "0.5.0-beta.3",
9820
- "@astrale-os/shell": "0.3.8-beta.2",
9819
+ "@astrale-os/sdk": ">=0.5.0-beta.3 <1.0.0",
9820
+ "@astrale-os/shell": ">=0.3.8-beta.2 <1.0.0",
9821
9821
  "@astrale/commitlint-config": "jsr:~2.0.1",
9822
9822
  "@commitlint/cli": "~20.3.1",
9823
9823
  "@commitlint/config-conventional": "~20.3.1",
@@ -86817,10 +86817,6 @@ async function connectAdminInstances(context, dependencies = {}) {
86817
86817
  throw new AdminHostNotFoundError(hostId);
86818
86818
  return instanceFromSummary(await binding6.$.invoke(Host.$.method("createInstance"), Path.parse(host.path), input));
86819
86819
  }
86820
- const inventory = await hostInventory(context.graph, binding6, Host, fleet);
86821
- const eligible = inventory.hosts.filter((host) => host.state === "ready" && host.path !== inventory.reserved);
86822
- if (eligible.length > 1)
86823
- throw hostSelectionRequired(eligible.map((host) => host.id));
86824
86820
  return instanceFromSummary(await binding6.$.invoke(Fleet.$.method("createInstance"), fleet, input));
86825
86821
  },
86826
86822
  status: (identifier) => invokeInstance("status", identifier),
@@ -86864,9 +86860,6 @@ async function hostInventory(graph, binding6, Host, fleet) {
86864
86860
  ...reserved === undefined ? {} : { reserved }
86865
86861
  });
86866
86862
  }
86867
- function hostSelectionRequired(ids2) {
86868
- return new Error(`alphaCreate could not choose a host: ${ids2.length} ready hosts are assigned (${[...ids2].sort().join(", ")}). ` + "Specify host_id once multi-host placement is enabled.");
86869
- }
86870
86863
  function instanceFromSummary(input) {
86871
86864
  const value2 = record9(input, "Admin Instance summary");
86872
86865
  const failure = value2.failure === undefined ? undefined : record9(value2.failure, "Admin failure");
@@ -87021,16 +87014,20 @@ function schemaUpgradeDetails2(reason) {
87021
87014
  if (!record10(reason) || reason.code !== "SCHEMA_UPGRADE_INCOMPATIBLE")
87022
87015
  return;
87023
87016
  const details = record10(reason.details) ? reason.details : {};
87024
- return {
87025
- ...typeof details.origin === "string" ? { origin: details.origin } : {},
87026
- ...typeof details.issue === "string" ? { issue: details.issue } : {},
87027
- ...typeof details.expected === "string" ? { expected: details.expected } : {},
87028
- ...typeof details.actual === "string" ? { actual: details.actual } : {}
87029
- };
87017
+ const origin = typeof details.origin === "string" ? details.origin : undefined;
87018
+ if (details.issue === "issuer-changed" && origin !== undefined && typeof details.installedIssuer === "string" && typeof details.replacementIssuer === "string") {
87019
+ return {
87020
+ origin,
87021
+ issue: details.issue,
87022
+ installedIssuer: details.installedIssuer,
87023
+ replacementIssuer: details.replacementIssuer
87024
+ };
87025
+ }
87026
+ return origin === undefined ? {} : { origin };
87030
87027
  }
87031
87028
  function schemaUpgradeHint(details) {
87032
87029
  const target2 = details.origin ?? "<origin>";
87033
- const explanation = details.expected !== undefined && details.actual !== undefined ? "A replacement cannot change an installed Domain issuer." : "The replacement changes an immutable part of the installed Domain.";
87030
+ const explanation = details.issue === "issuer-changed" ? "A replacement cannot change an installed Domain issuer." : "The replacement changes an immutable part of the installed Domain.";
87034
87031
  return `${explanation} If this change is intentional, first run ` + `\`astrale domain uninstall ${target2}\`, then install it again. ` + "The Kernel refuses uninstall while dependents or business data remain; uninstall never deletes business data.";
87035
87032
  }
87036
87033
  function reasonWithCode(input, code) {
@@ -87063,7 +87060,7 @@ var init_reasons = __esm(() => {
87063
87060
  });
87064
87061
 
87065
87062
  // src/connection/errors.ts
87066
- async function formatKernelError(error52, isRaw, urlArg = "", debug = false) {
87063
+ async function formatKernelError(error52, isRaw, urlArg = "", debug = false, opts = {}) {
87067
87064
  const url3 = urlArg || (error52 instanceof Error ? error52.url ?? "" : "");
87068
87065
  const localContext = await contextForError(error52);
87069
87066
  if (error52 instanceof AstraleError) {
@@ -87088,7 +87085,7 @@ async function formatKernelError(error52, isRaw, urlArg = "", debug = false) {
87088
87085
  const name = error52.name;
87089
87086
  switch (name) {
87090
87087
  case "TransportError":
87091
- presentTransportError(error52, isRaw, url3, localContext);
87088
+ presentTransportError(error52, isRaw, url3, localContext, opts.recovery);
87092
87089
  break;
87093
87090
  case "ResponseError": {
87094
87091
  const code = error52.code;
@@ -87117,11 +87114,9 @@ async function formatKernelError(error52, isRaw, urlArg = "", debug = false) {
87117
87114
  presentFunctionInputIssues(inputIssues);
87118
87115
  if (queryRepair !== undefined)
87119
87116
  presentQueryInputRepair(queryRepair);
87120
- if (upgrade?.expected !== undefined) {
87121
- log.dim(` installed issuer: ${upgrade.expected}`);
87122
- }
87123
- if (upgrade?.actual !== undefined) {
87124
- log.dim(` replacement issuer: ${upgrade.actual}`);
87117
+ if (upgrade?.issue === "issuer-changed") {
87118
+ log.dim(` installed issuer: ${upgrade.installedIssuer}`);
87119
+ log.dim(` replacement issuer: ${upgrade.replacementIssuer}`);
87125
87120
  }
87126
87121
  if (hint !== undefined)
87127
87122
  log.dim(` ${hint}`);
@@ -87194,7 +87189,7 @@ function mapPublicError(error52) {
87194
87189
  }
87195
87190
  return { code: name && name !== "Error" ? name : "UNKNOWN", message: error52.message };
87196
87191
  }
87197
- function presentTransportError(error52, isRaw, url3, context) {
87192
+ function presentTransportError(error52, isRaw, url3, context, recovery) {
87198
87193
  const phase = transportPhase(error52);
87199
87194
  const delivery = transportDelivery(error52);
87200
87195
  const code = phase === "connect" ? "CONNECTION_ERROR" : phase === "timeout" ? "TIMEOUT" : phase === "closed" ? "DISCONNECTED" : "TRANSPORT_ERROR";
@@ -87205,7 +87200,8 @@ function presentTransportError(error52, isRaw, url3, context) {
87205
87200
  ...url3 === "" ? {} : { url: url3 },
87206
87201
  ...phase === undefined ? {} : { phase },
87207
87202
  ...delivery === undefined ? {} : { delivery },
87208
- ...context === undefined ? {} : { context }
87203
+ ...context === undefined ? {} : { context },
87204
+ ...delivery === "unknown" && recovery !== undefined ? recovery : {}
87209
87205
  });
87210
87206
  return;
87211
87207
  }
@@ -87218,10 +87214,18 @@ function presentTransportError(error52, isRaw, url3, context) {
87218
87214
  log.dim(" Check the target and run `astrale status`.");
87219
87215
  else if (phase === "timeout")
87220
87216
  log.dim(" Try increasing `--timeout`.");
87221
- else if (delivery === "unknown") {
87217
+ else if (delivery === "unknown")
87218
+ printOperationRecovery(recovery);
87219
+ printLocalContext(context);
87220
+ }
87221
+ function printOperationRecovery(recovery) {
87222
+ if (recovery === undefined) {
87222
87223
  log.dim(" Delivery is unknown; do not automatically retry a mutating call.");
87224
+ return;
87223
87225
  }
87224
- printLocalContext(context);
87226
+ log.dim(" Delivery is unknown; retry with the same operation id:");
87227
+ log.dim(` operation: ${recovery.operation}`);
87228
+ log.dim(` ${recovery.retry}`);
87225
87229
  }
87226
87230
  function presentFunctionInputIssues(issues) {
87227
87231
  for (const issue2 of issues) {
@@ -88640,7 +88644,9 @@ async function runKernelCommand(input) {
88640
88644
  } catch (error52) {
88641
88645
  if (!isRaw && spin)
88642
88646
  spin.fail(`${label} failed`);
88643
- await formatKernelError(error52, isRaw, undefined, opts.debug);
88647
+ await formatKernelError(error52, isRaw, undefined, opts.debug, {
88648
+ recovery: input.recovery
88649
+ });
88644
88650
  process.exit(1);
88645
88651
  }
88646
88652
  }
@@ -89766,7 +89772,9 @@ var init_update = __esm(() => {
89766
89772
  // src/commands/update.ts
89767
89773
  var exports_update = {};
89768
89774
  __export(exports_update, {
89769
- default: () => update_default
89775
+ cliStale: () => cliStale,
89776
+ default: () => update_default,
89777
+ fetchNpmTargetVersion: () => fetchNpmTargetVersion
89770
89778
  });
89771
89779
  async function refreshSkills() {
89772
89780
  if (!detectSkill(ASTRALE_CLI_SKILL).installed) {
@@ -89815,29 +89823,35 @@ async function refreshSdkDeps(check3, assumeYes = false) {
89815
89823
  }
89816
89824
  return true;
89817
89825
  }
89818
- async function cliStale(opts) {
89826
+ async function cliStale(opts, dependencies = CLI_STALE_DEPENDENCIES) {
89819
89827
  const running = package_default.version;
89820
- const latest = await fetchNpmLatestVersion().catch(() => {
89821
- return;
89822
- });
89823
89828
  try {
89824
- const r = await updateAstrale({
89829
+ const r = await dependencies.update({
89825
89830
  check: true,
89826
89831
  channel: opts.channel,
89827
89832
  version: opts.version,
89828
89833
  currentVersion: running
89829
89834
  });
89830
89835
  if (r.status === "updated") {
89831
- return { stale: false, managed: false, current: running, latest: latest ?? running };
89836
+ return {
89837
+ stale: false,
89838
+ managed: false,
89839
+ current: r.currentVersion,
89840
+ latest: r.currentVersion,
89841
+ channel: r.channel
89842
+ };
89832
89843
  }
89833
89844
  return {
89834
- stale: latest !== undefined ? latest !== running : r.status === "available",
89845
+ stale: r.status === "available",
89835
89846
  managed: false,
89836
- current: running,
89837
- latest: latest ?? r.latestVersion,
89838
- channel: latest !== undefined ? "npm" : r.channel
89847
+ current: r.currentVersion,
89848
+ latest: r.latestVersion,
89849
+ channel: r.channel
89839
89850
  };
89840
89851
  } catch {
89852
+ const latest = await dependencies.fetchPackageVersion(opts).catch(() => {
89853
+ return;
89854
+ });
89841
89855
  return {
89842
89856
  stale: latest !== undefined && latest !== running,
89843
89857
  managed: true,
@@ -89846,8 +89860,9 @@ async function cliStale(opts) {
89846
89860
  };
89847
89861
  }
89848
89862
  }
89849
- async function fetchNpmLatestVersion() {
89850
- const response2 = await fetch("https://registry.npmjs.org/@astrale-os/cli/latest");
89863
+ async function fetchNpmTargetVersion(opts) {
89864
+ const target2 = opts.version ?? (opts.channel === "stable" ? "latest" : opts.channel) ?? "latest";
89865
+ const response2 = await fetch(`https://registry.npmjs.org/@astrale-os/cli/${target2}`);
89851
89866
  if (!response2.ok)
89852
89867
  throw new Error(`npm registry HTTP ${response2.status}`);
89853
89868
  const body = await response2.json();
@@ -89863,7 +89878,7 @@ async function sdkStale() {
89863
89878
  const outdated = await findSdkOutdated();
89864
89879
  return { stale: outdated.length > 0, inProject: true, outdated };
89865
89880
  }
89866
- var update_default;
89881
+ var CLI_STALE_DEPENDENCIES, update_default;
89867
89882
  var init_update2 = __esm(() => {
89868
89883
  init_package();
89869
89884
  init_log();
@@ -89872,6 +89887,10 @@ var init_update2 = __esm(() => {
89872
89887
  init_sdk_deps();
89873
89888
  init_skills();
89874
89889
  init_update();
89890
+ CLI_STALE_DEPENDENCIES = {
89891
+ update: updateAstrale,
89892
+ fetchPackageVersion: fetchNpmTargetVersion
89893
+ };
89875
89894
  update_default = {
89876
89895
  name: "update",
89877
89896
  description: "Check for and install Astrale CLI updates",
@@ -93593,11 +93612,12 @@ __export(exports_install, {
93593
93612
  default: () => install_default,
93594
93613
  directInstallCallInput: () => directInstallCallInput,
93595
93614
  domainRefFromTarget: () => domainRefFromTarget,
93615
+ installDirect: () => installDirect,
93596
93616
  installViaAdmin: () => installViaAdmin,
93597
93617
  isIdentityOverride: () => isIdentityOverride,
93598
93618
  probeDeclaredOrigin: () => probeDeclaredOrigin
93599
93619
  });
93600
- function directInstallCallInput(url3, token, operation2 = crypto.randomUUID()) {
93620
+ function directInstallCallInput(url3, operation2, token) {
93601
93621
  return Object.freeze({
93602
93622
  operation: operation2,
93603
93623
  domains: [
@@ -93611,6 +93631,15 @@ function directInstallCallInput(url3, token, operation2 = crypto.randomUUID()) {
93611
93631
  ]
93612
93632
  });
93613
93633
  }
93634
+ function acceptOperationId(input) {
93635
+ if (typeof input !== "string" || !OPERATION_ID_PATTERN.test(input)) {
93636
+ throw new AstraleError("INVALID_FLAG", "--operation must be a canonical lowercase UUIDv4.", "Omit --operation for a fresh install; use it only with the exact UUID printed for recovery.");
93637
+ }
93638
+ return input;
93639
+ }
93640
+ function createOperationId() {
93641
+ return acceptOperationId(globalThis.crypto.randomUUID());
93642
+ }
93614
93643
  async function installViaAdmin(target2, opts, dependencies = {}) {
93615
93644
  const admin = { ...defaultAdminInstallDependencies, ...dependencies };
93616
93645
  const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI);
@@ -93648,8 +93677,9 @@ async function installViaAdmin(target2, opts, dependencies = {}) {
93648
93677
  log.dim(` origin: ${result.origin}`);
93649
93678
  log.dim(` url: ${result.url}`);
93650
93679
  });
93651
- } catch (e) {
93652
- fatal(e, opts);
93680
+ } catch (error52) {
93681
+ await formatKernelError(error52, isMachine(opts), undefined, opts.debug);
93682
+ process.exit(1);
93653
93683
  }
93654
93684
  }
93655
93685
  function assertInstallTargetReady(instance) {
@@ -93712,40 +93742,49 @@ async function activeSlug() {
93712
93742
  return;
93713
93743
  }
93714
93744
  }
93715
- async function installDirect(target2, opts) {
93745
+ async function installDirect(target2, opts, dependencies = {}) {
93746
+ const direct = { ...defaultDirectInstallDependencies, ...dependencies };
93716
93747
  let host = "";
93717
93748
  let consentedOrigin;
93749
+ let operation2;
93718
93750
  try {
93719
93751
  if (!target2) {
93720
93752
  throw new AstraleError("MISSING_ARG", "--direct requires a domain url.", "e.g. astrale domain install https://crm.acme.dev --direct");
93721
93753
  }
93722
93754
  host = validateInstallUrl(target2);
93755
+ operation2 = opts.operation === undefined ? direct.createOperationId() : direct.acceptOperationId(opts.operation);
93723
93756
  consentedOrigin = await ensureIdentityOverrideConsent(target2, host, opts.allowIdentityOverride ?? false);
93724
93757
  } catch (e) {
93725
- fatal(e);
93758
+ fatal(e, opts);
93726
93759
  }
93727
93760
  const url3 = target2;
93728
- await runKernelCommand({
93761
+ const retry = directInstallRetry(url3, operation2, opts);
93762
+ await direct.runKernelCommand({
93729
93763
  opts,
93730
- label: `Installing domain from ${url3}`,
93731
- fn: async ({ session }) => await session.call(createPathCall(Path.project(exports_syscalls.install.ref).raw, directInstallCallInput(url3, opts.token))),
93764
+ label: `Installing domain from ${url3} (operation ${operation2})`,
93765
+ recovery: { operation: operation2, retry },
93766
+ fn: async ({ session }) => await session.call(createPathCall(Path.project(exports_syscalls.install.ref).raw, directInstallCallInput(url3, operation2, opts.token))),
93732
93767
  format: (result, fmtOpts, isRaw) => {
93733
93768
  if (isRaw) {
93734
93769
  output(result, fmtOpts);
93735
93770
  return;
93736
93771
  }
93737
93772
  const installed = result.transitions[0]?.intent;
93738
- if (installed === undefined) {
93773
+ if (!installed)
93739
93774
  throw new Error("Kernel install returned no committed Domain transition.");
93740
- }
93741
93775
  const revision = installed.target?.schemaRevision ?? result.operation;
93742
93776
  log.success(`Domain installed: ${installed.origin}@${revision}`);
93777
+ log.dim(` operation: ${result.operation}`);
93743
93778
  if (isIdentityOverride(installed.origin, host) && installed.origin !== consentedOrigin) {
93744
93779
  log.warn(`Installed origin "${installed.origin}" differs from the serving host "${host}" ` + `and was not confirmed before install (the worker's /meta did not declare it). ` + `Every ${installed.origin}/* call on this instance now routes to ${host}.`);
93745
93780
  }
93746
93781
  }
93747
93782
  });
93748
93783
  }
93784
+ function directInstallRetry(url3, operation2, opts) {
93785
+ const instance = opts.instance === undefined ? "" : ` -i ${opts.instance}`;
93786
+ return `astrale domain install ${url3} --direct --operation ${operation2}${instance}`;
93787
+ }
93749
93788
  function validateInstallUrl(value2) {
93750
93789
  let url3;
93751
93790
  try {
@@ -93798,12 +93837,13 @@ async function probeDeclaredOrigin(url3) {
93798
93837
  return;
93799
93838
  }
93800
93839
  }
93801
- var install_default, defaultAdminInstallDependencies;
93840
+ var OPERATION_ID_PATTERN, install_default, defaultAdminInstallDependencies, defaultDirectInstallDependencies;
93802
93841
  var init_install3 = __esm(() => {
93803
93842
  init_path5();
93804
93843
  init_kernel2();
93805
93844
  init_source();
93806
93845
  init_connection2();
93846
+ init_errors12();
93807
93847
  init_errors2();
93808
93848
  init_admin_domain();
93809
93849
  init_admin_instance();
@@ -93813,6 +93853,7 @@ var init_install3 = __esm(() => {
93813
93853
  init_output();
93814
93854
  init_prompt();
93815
93855
  init_validation2();
93856
+ OPERATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
93816
93857
  install_default = {
93817
93858
  name: "install",
93818
93859
  description: "Install a domain on an instance (via the admin catalog, or --direct from a url)",
@@ -93831,6 +93872,10 @@ Behavior:
93831
93872
  origin differs from its serving host, it requires explicit consent (an
93832
93873
  interactive DANGER prompt, or --allow-identity-override in scripts).
93833
93874
 
93875
+ A fresh, strong operation id is generated automatically. Use --operation
93876
+ only to retry or recover the exact same direct install after an outcome-unknown
93877
+ timeout or disconnect.
93878
+
93834
93879
  Examples:
93835
93880
  $ astrale domain install crm.acme.dev -i staging # by origin, via admin
93836
93881
  $ astrale domain install https://crm.acme.dev # by url, via admin
@@ -93855,12 +93900,19 @@ Examples:
93855
93900
  flags: "--token <token>",
93856
93901
  description: "Bearer token for private domain install endpoints (--direct only)"
93857
93902
  },
93903
+ {
93904
+ flags: "--operation <uuid>",
93905
+ description: "Reuse an exact direct-install operation id for explicit retry/recovery"
93906
+ },
93858
93907
  {
93859
93908
  flags: "--allow-identity-override",
93860
93909
  description: "Consent to a domain whose origin differs from its serving host (--direct only)"
93861
93910
  }
93862
93911
  ],
93863
93912
  action: async (target2, opts) => {
93913
+ if (opts.operation !== undefined && !opts.direct) {
93914
+ fatal(new AstraleError("INVALID_FLAG", "--operation is valid only with --direct.", "Ordinary direct installs generate a fresh operation id automatically."), opts);
93915
+ }
93864
93916
  if (opts.direct) {
93865
93917
  await installDirect(target2, opts);
93866
93918
  return;
@@ -93873,6 +93925,11 @@ Examples:
93873
93925
  listDomains: listAdminDomainsInContext,
93874
93926
  install: installAdminDomainInContext
93875
93927
  });
93928
+ defaultDirectInstallDependencies = Object.freeze({
93929
+ acceptOperationId,
93930
+ createOperationId,
93931
+ runKernelCommand
93932
+ });
93876
93933
  });
93877
93934
 
93878
93935
  // src/commands/domain/uninstall.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "0.8.1-alpha.7",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -62,10 +62,10 @@
62
62
  "zod": "^3.25.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@astrale-os/kernel-client": "0.6.0-beta.3",
65
+ "@astrale-os/kernel-client": ">=0.6.0-beta.3 <1.0.0",
66
66
  "@astrale-os/ox": ">=0.1.0 <1.0.0",
67
- "@astrale-os/sdk": "0.5.0-beta.3",
68
- "@astrale-os/shell": "0.3.8-beta.2",
67
+ "@astrale-os/sdk": ">=0.5.0-beta.3 <1.0.0",
68
+ "@astrale-os/shell": ">=0.3.8-beta.2 <1.0.0",
69
69
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
70
70
  "@commitlint/cli": "~20.3.1",
71
71
  "@commitlint/config-conventional": "~20.3.1",
@@ -107,7 +107,7 @@
107
107
  "lint:fix": "oxlint --fix .",
108
108
  "format": "pnpm exec oxfmt --write .",
109
109
  "format:check": "pnpm exec oxfmt --check .",
110
- "test": "bun test src",
110
+ "test": "bun test src && node --test scripts/*.test.mjs",
111
111
  "test:watch": "bun test --watch src"
112
112
  }
113
113
  }
@@ -153,7 +153,7 @@ describe('V2 Admin Instance adapter', () => {
153
153
  )
154
154
  })
155
155
 
156
- test('invokes Fleet.createInstance with an internal operation identity', async () => {
156
+ test('delegates default placement to Fleet without reading Host inventory', async () => {
157
157
  const created = {
158
158
  id: '@instance-node',
159
159
  slug: 'demo',
@@ -179,6 +179,8 @@ describe('V2 Admin Instance adapter', () => {
179
179
  Path.id(NodeId('fleet')),
180
180
  { operationId: 'cli.instance.create:test', slug: 'demo' },
181
181
  )
182
+ expect(contract.graph.query).not.toHaveBeenCalled()
183
+ expect(contract.graph.neighbors).not.toHaveBeenCalled()
182
184
  })
183
185
 
184
186
  test('resolves --host-id to an exact Host receiver and rejects the reserved Admin Host', async () => {
@@ -119,15 +119,9 @@ export async function connectAdminInstances(
119
119
  )
120
120
  }
121
121
 
122
- // Preserve the existing interactive multi-Host picker without accepting a
123
- // doomed operation first. Graph read policy exposes only caller-usable
124
- // Hosts; the reserved Admin Host is excluded explicitly.
125
- const inventory = await hostInventory(context.graph, binding, Host, fleet)
126
- const eligible = inventory.hosts.filter(
127
- (host) => host.state === 'ready' && host.path !== inventory.reserved,
128
- )
129
- if (eligible.length > 1) throw hostSelectionRequired(eligible.map((host) => host.id))
130
-
122
+ // Default placement belongs to Admin. Avoid a redundant Host graph read:
123
+ // it requires broader graph authority than this Fleet operation and can
124
+ // deny an otherwise authorized caller before the server selects a Host.
131
125
  return instanceFromSummary(
132
126
  await binding.$.invoke(Fleet.$.method('createInstance') as never, fleet, input as never),
133
127
  )
@@ -200,13 +194,6 @@ async function hostInventory(
200
194
  })
201
195
  }
202
196
 
203
- function hostSelectionRequired(ids: readonly string[]): Error {
204
- return new Error(
205
- `alphaCreate could not choose a host: ${ids.length} ready hosts are assigned (${[...ids].sort().join(', ')}). ` +
206
- 'Specify host_id once multi-host placement is enabled.',
207
- )
208
- }
209
-
210
197
  type DynamicDefinition = ReturnType<DomainBinding['$']['class']>
211
198
 
212
199
  function instanceFromSummary(input: unknown): InstanceInfo {
@@ -0,0 +1,121 @@
1
+ import { afterEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ import { installDirect } from '../domain/install'
4
+
5
+ const GENERATED = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
6
+ const RETRY = '139137b5-af47-47ce-92b2-b64a2b0c63d7'
7
+
8
+ const originalFetch = globalThis.fetch
9
+
10
+ class ExitError extends Error {}
11
+
12
+ afterEach(() => {
13
+ globalThis.fetch = originalFetch
14
+ })
15
+
16
+ describe('direct domain install operation identity', () => {
17
+ test('generates one operation before transport and exposes it for recovery', async () => {
18
+ globalThis.fetch = mock(async () =>
19
+ Response.json({ domainName: 'crm.test' }),
20
+ ) as unknown as typeof fetch
21
+ const create = mock(() => GENERATED)
22
+ const requests: unknown[] = []
23
+ let recovery: unknown
24
+
25
+ await installDirect(
26
+ 'https://crm.test',
27
+ { direct: true, json: true },
28
+ {
29
+ createOperationId: create,
30
+ runKernelCommand: async (input) => {
31
+ recovery = input.recovery
32
+ requests.push(
33
+ await input.fn({
34
+ session: {
35
+ call: async (call: { input: unknown }) => call.input,
36
+ },
37
+ } as never),
38
+ )
39
+ },
40
+ },
41
+ )
42
+
43
+ expect(create).toHaveBeenCalledTimes(1)
44
+ expect(requests).toEqual([
45
+ {
46
+ operation: GENERATED,
47
+ domains: [{ source: { kind: 'remote', url: 'https://crm.test' } }],
48
+ },
49
+ ])
50
+ expect(recovery).toEqual({
51
+ operation: GENERATED,
52
+ retry: `astrale domain install https://crm.test --direct --operation ${GENERATED}`,
53
+ })
54
+ })
55
+
56
+ test('accepts an explicit operation only as the exact retry identity', async () => {
57
+ globalThis.fetch = mock(async () =>
58
+ Response.json({ domainName: 'crm.test' }),
59
+ ) as unknown as typeof fetch
60
+ const create = mock(() => GENERATED)
61
+ const accepted: string[] = []
62
+ const requests: unknown[] = []
63
+
64
+ await installDirect(
65
+ 'https://crm.test',
66
+ { direct: true, operation: RETRY, json: true },
67
+ {
68
+ createOperationId: create,
69
+ acceptOperationId: (input) => {
70
+ accepted.push(String(input))
71
+ return RETRY
72
+ },
73
+ runKernelCommand: async (input) => {
74
+ requests.push(
75
+ await input.fn({
76
+ session: {
77
+ call: async (call: { input: unknown }) => call.input,
78
+ },
79
+ } as never),
80
+ )
81
+ },
82
+ },
83
+ )
84
+
85
+ expect(create).not.toHaveBeenCalled()
86
+ expect(accepted).toEqual([RETRY])
87
+ expect(requests).toEqual([
88
+ {
89
+ operation: RETRY,
90
+ domains: [{ source: { kind: 'remote', url: 'https://crm.test' } }],
91
+ },
92
+ ])
93
+ })
94
+
95
+ test('rejects a non-UUID operation before metadata or Kernel transport', async () => {
96
+ const originalExit = process.exit
97
+ const originalWrite = process.stderr.write
98
+ const fetch = mock(async () => Response.json({ domainName: 'crm.test' }))
99
+ const run = mock(async () => undefined)
100
+ globalThis.fetch = fetch as unknown as typeof globalThis.fetch
101
+ process.exit = (() => {
102
+ throw new ExitError('exit')
103
+ }) as typeof process.exit
104
+ process.stderr.write = (() => true) as typeof process.stderr.write
105
+ try {
106
+ await expect(
107
+ installDirect(
108
+ 'https://crm.test',
109
+ { direct: true, operation: 'guessable-operation', json: true },
110
+ { runKernelCommand: run },
111
+ ),
112
+ ).rejects.toBeInstanceOf(ExitError)
113
+ } finally {
114
+ process.exit = originalExit
115
+ process.stderr.write = originalWrite
116
+ }
117
+
118
+ expect(fetch).not.toHaveBeenCalled()
119
+ expect(run).not.toHaveBeenCalled()
120
+ })
121
+ })