@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.
- package/README.md +15 -3
- package/dist/astrale.js +113 -56
- package/package.json +5 -5
- package/src/admin/instance/__tests__/client.test.ts +3 -1
- package/src/admin/instance/client.ts +3 -16
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- package/src/commands/__tests__/update.test.ts +66 -0
- package/src/commands/domain/install.ts +78 -15
- package/src/commands/update.ts +32 -10
- package/src/connection/__tests__/errors.test.ts +82 -6
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +19 -8
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +26 -12
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +1 -1
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- 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
|
-
>
|
|
40
|
-
>
|
|
41
|
-
>
|
|
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.
|
|
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
|
-
|
|
87025
|
-
|
|
87026
|
-
|
|
87027
|
-
|
|
87028
|
-
|
|
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.
|
|
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?.
|
|
87121
|
-
log.dim(` installed issuer: ${upgrade.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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:
|
|
89845
|
+
stale: r.status === "available",
|
|
89835
89846
|
managed: false,
|
|
89836
|
-
current:
|
|
89837
|
-
latest:
|
|
89838
|
-
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
|
|
89850
|
-
const
|
|
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,
|
|
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 (
|
|
93652
|
-
|
|
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
|
-
|
|
93761
|
+
const retry = directInstallRetry(url3, operation2, opts);
|
|
93762
|
+
await direct.runKernelCommand({
|
|
93729
93763
|
opts,
|
|
93730
|
-
label: `Installing domain from ${url3}`,
|
|
93731
|
-
|
|
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
|
|
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.
|
|
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('
|
|
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
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
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
|
+
})
|