@astrale-os/cli 0.8.1-alpha.7 → 1.0.0-beta.0
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 +86 -35
- package/package.json +5 -5
- 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/domain/install.ts +78 -15
- 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.0",
|
|
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",
|
|
@@ -87021,16 +87021,20 @@ function schemaUpgradeDetails2(reason) {
|
|
|
87021
87021
|
if (!record10(reason) || reason.code !== "SCHEMA_UPGRADE_INCOMPATIBLE")
|
|
87022
87022
|
return;
|
|
87023
87023
|
const details = record10(reason.details) ? reason.details : {};
|
|
87024
|
-
|
|
87025
|
-
|
|
87026
|
-
|
|
87027
|
-
|
|
87028
|
-
|
|
87029
|
-
|
|
87024
|
+
const origin = typeof details.origin === "string" ? details.origin : undefined;
|
|
87025
|
+
if (details.issue === "issuer-changed" && origin !== undefined && typeof details.installedIssuer === "string" && typeof details.replacementIssuer === "string") {
|
|
87026
|
+
return {
|
|
87027
|
+
origin,
|
|
87028
|
+
issue: details.issue,
|
|
87029
|
+
installedIssuer: details.installedIssuer,
|
|
87030
|
+
replacementIssuer: details.replacementIssuer
|
|
87031
|
+
};
|
|
87032
|
+
}
|
|
87033
|
+
return origin === undefined ? {} : { origin };
|
|
87030
87034
|
}
|
|
87031
87035
|
function schemaUpgradeHint(details) {
|
|
87032
87036
|
const target2 = details.origin ?? "<origin>";
|
|
87033
|
-
const explanation = details.
|
|
87037
|
+
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
87038
|
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
87039
|
}
|
|
87036
87040
|
function reasonWithCode(input, code) {
|
|
@@ -87063,7 +87067,7 @@ var init_reasons = __esm(() => {
|
|
|
87063
87067
|
});
|
|
87064
87068
|
|
|
87065
87069
|
// src/connection/errors.ts
|
|
87066
|
-
async function formatKernelError(error52, isRaw, urlArg = "", debug = false) {
|
|
87070
|
+
async function formatKernelError(error52, isRaw, urlArg = "", debug = false, opts = {}) {
|
|
87067
87071
|
const url3 = urlArg || (error52 instanceof Error ? error52.url ?? "" : "");
|
|
87068
87072
|
const localContext = await contextForError(error52);
|
|
87069
87073
|
if (error52 instanceof AstraleError) {
|
|
@@ -87088,7 +87092,7 @@ async function formatKernelError(error52, isRaw, urlArg = "", debug = false) {
|
|
|
87088
87092
|
const name = error52.name;
|
|
87089
87093
|
switch (name) {
|
|
87090
87094
|
case "TransportError":
|
|
87091
|
-
presentTransportError(error52, isRaw, url3, localContext);
|
|
87095
|
+
presentTransportError(error52, isRaw, url3, localContext, opts.recovery);
|
|
87092
87096
|
break;
|
|
87093
87097
|
case "ResponseError": {
|
|
87094
87098
|
const code = error52.code;
|
|
@@ -87117,11 +87121,9 @@ async function formatKernelError(error52, isRaw, urlArg = "", debug = false) {
|
|
|
87117
87121
|
presentFunctionInputIssues(inputIssues);
|
|
87118
87122
|
if (queryRepair !== undefined)
|
|
87119
87123
|
presentQueryInputRepair(queryRepair);
|
|
87120
|
-
if (upgrade?.
|
|
87121
|
-
log.dim(` installed issuer: ${upgrade.
|
|
87122
|
-
|
|
87123
|
-
if (upgrade?.actual !== undefined) {
|
|
87124
|
-
log.dim(` replacement issuer: ${upgrade.actual}`);
|
|
87124
|
+
if (upgrade?.issue === "issuer-changed") {
|
|
87125
|
+
log.dim(` installed issuer: ${upgrade.installedIssuer}`);
|
|
87126
|
+
log.dim(` replacement issuer: ${upgrade.replacementIssuer}`);
|
|
87125
87127
|
}
|
|
87126
87128
|
if (hint !== undefined)
|
|
87127
87129
|
log.dim(` ${hint}`);
|
|
@@ -87194,7 +87196,7 @@ function mapPublicError(error52) {
|
|
|
87194
87196
|
}
|
|
87195
87197
|
return { code: name && name !== "Error" ? name : "UNKNOWN", message: error52.message };
|
|
87196
87198
|
}
|
|
87197
|
-
function presentTransportError(error52, isRaw, url3, context) {
|
|
87199
|
+
function presentTransportError(error52, isRaw, url3, context, recovery) {
|
|
87198
87200
|
const phase = transportPhase(error52);
|
|
87199
87201
|
const delivery = transportDelivery(error52);
|
|
87200
87202
|
const code = phase === "connect" ? "CONNECTION_ERROR" : phase === "timeout" ? "TIMEOUT" : phase === "closed" ? "DISCONNECTED" : "TRANSPORT_ERROR";
|
|
@@ -87205,7 +87207,8 @@ function presentTransportError(error52, isRaw, url3, context) {
|
|
|
87205
87207
|
...url3 === "" ? {} : { url: url3 },
|
|
87206
87208
|
...phase === undefined ? {} : { phase },
|
|
87207
87209
|
...delivery === undefined ? {} : { delivery },
|
|
87208
|
-
...context === undefined ? {} : { context }
|
|
87210
|
+
...context === undefined ? {} : { context },
|
|
87211
|
+
...delivery === "unknown" && recovery !== undefined ? recovery : {}
|
|
87209
87212
|
});
|
|
87210
87213
|
return;
|
|
87211
87214
|
}
|
|
@@ -87218,10 +87221,18 @@ function presentTransportError(error52, isRaw, url3, context) {
|
|
|
87218
87221
|
log.dim(" Check the target and run `astrale status`.");
|
|
87219
87222
|
else if (phase === "timeout")
|
|
87220
87223
|
log.dim(" Try increasing `--timeout`.");
|
|
87221
|
-
else if (delivery === "unknown")
|
|
87224
|
+
else if (delivery === "unknown")
|
|
87225
|
+
printOperationRecovery(recovery);
|
|
87226
|
+
printLocalContext(context);
|
|
87227
|
+
}
|
|
87228
|
+
function printOperationRecovery(recovery) {
|
|
87229
|
+
if (recovery === undefined) {
|
|
87222
87230
|
log.dim(" Delivery is unknown; do not automatically retry a mutating call.");
|
|
87231
|
+
return;
|
|
87223
87232
|
}
|
|
87224
|
-
|
|
87233
|
+
log.dim(" Delivery is unknown; retry with the same operation id:");
|
|
87234
|
+
log.dim(` operation: ${recovery.operation}`);
|
|
87235
|
+
log.dim(` ${recovery.retry}`);
|
|
87225
87236
|
}
|
|
87226
87237
|
function presentFunctionInputIssues(issues) {
|
|
87227
87238
|
for (const issue2 of issues) {
|
|
@@ -88640,7 +88651,9 @@ async function runKernelCommand(input) {
|
|
|
88640
88651
|
} catch (error52) {
|
|
88641
88652
|
if (!isRaw && spin)
|
|
88642
88653
|
spin.fail(`${label} failed`);
|
|
88643
|
-
await formatKernelError(error52, isRaw, undefined, opts.debug
|
|
88654
|
+
await formatKernelError(error52, isRaw, undefined, opts.debug, {
|
|
88655
|
+
recovery: input.recovery
|
|
88656
|
+
});
|
|
88644
88657
|
process.exit(1);
|
|
88645
88658
|
}
|
|
88646
88659
|
}
|
|
@@ -93593,11 +93606,12 @@ __export(exports_install, {
|
|
|
93593
93606
|
default: () => install_default,
|
|
93594
93607
|
directInstallCallInput: () => directInstallCallInput,
|
|
93595
93608
|
domainRefFromTarget: () => domainRefFromTarget,
|
|
93609
|
+
installDirect: () => installDirect,
|
|
93596
93610
|
installViaAdmin: () => installViaAdmin,
|
|
93597
93611
|
isIdentityOverride: () => isIdentityOverride,
|
|
93598
93612
|
probeDeclaredOrigin: () => probeDeclaredOrigin
|
|
93599
93613
|
});
|
|
93600
|
-
function directInstallCallInput(url3,
|
|
93614
|
+
function directInstallCallInput(url3, operation2, token) {
|
|
93601
93615
|
return Object.freeze({
|
|
93602
93616
|
operation: operation2,
|
|
93603
93617
|
domains: [
|
|
@@ -93611,6 +93625,15 @@ function directInstallCallInput(url3, token, operation2 = crypto.randomUUID()) {
|
|
|
93611
93625
|
]
|
|
93612
93626
|
});
|
|
93613
93627
|
}
|
|
93628
|
+
function acceptOperationId(input) {
|
|
93629
|
+
if (typeof input !== "string" || !OPERATION_ID_PATTERN.test(input)) {
|
|
93630
|
+
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.");
|
|
93631
|
+
}
|
|
93632
|
+
return input;
|
|
93633
|
+
}
|
|
93634
|
+
function createOperationId() {
|
|
93635
|
+
return acceptOperationId(globalThis.crypto.randomUUID());
|
|
93636
|
+
}
|
|
93614
93637
|
async function installViaAdmin(target2, opts, dependencies = {}) {
|
|
93615
93638
|
const admin = { ...defaultAdminInstallDependencies, ...dependencies };
|
|
93616
93639
|
const interactive = !!process.stdin.isTTY && !(opts.ci || opts.noPrompt || process.env.CI);
|
|
@@ -93648,8 +93671,9 @@ async function installViaAdmin(target2, opts, dependencies = {}) {
|
|
|
93648
93671
|
log.dim(` origin: ${result.origin}`);
|
|
93649
93672
|
log.dim(` url: ${result.url}`);
|
|
93650
93673
|
});
|
|
93651
|
-
} catch (
|
|
93652
|
-
|
|
93674
|
+
} catch (error52) {
|
|
93675
|
+
await formatKernelError(error52, isMachine(opts), undefined, opts.debug);
|
|
93676
|
+
process.exit(1);
|
|
93653
93677
|
}
|
|
93654
93678
|
}
|
|
93655
93679
|
function assertInstallTargetReady(instance) {
|
|
@@ -93712,40 +93736,49 @@ async function activeSlug() {
|
|
|
93712
93736
|
return;
|
|
93713
93737
|
}
|
|
93714
93738
|
}
|
|
93715
|
-
async function installDirect(target2, opts) {
|
|
93739
|
+
async function installDirect(target2, opts, dependencies = {}) {
|
|
93740
|
+
const direct = { ...defaultDirectInstallDependencies, ...dependencies };
|
|
93716
93741
|
let host = "";
|
|
93717
93742
|
let consentedOrigin;
|
|
93743
|
+
let operation2;
|
|
93718
93744
|
try {
|
|
93719
93745
|
if (!target2) {
|
|
93720
93746
|
throw new AstraleError("MISSING_ARG", "--direct requires a domain url.", "e.g. astrale domain install https://crm.acme.dev --direct");
|
|
93721
93747
|
}
|
|
93722
93748
|
host = validateInstallUrl(target2);
|
|
93749
|
+
operation2 = opts.operation === undefined ? direct.createOperationId() : direct.acceptOperationId(opts.operation);
|
|
93723
93750
|
consentedOrigin = await ensureIdentityOverrideConsent(target2, host, opts.allowIdentityOverride ?? false);
|
|
93724
93751
|
} catch (e) {
|
|
93725
|
-
fatal(e);
|
|
93752
|
+
fatal(e, opts);
|
|
93726
93753
|
}
|
|
93727
93754
|
const url3 = target2;
|
|
93728
|
-
|
|
93755
|
+
const retry = directInstallRetry(url3, operation2, opts);
|
|
93756
|
+
await direct.runKernelCommand({
|
|
93729
93757
|
opts,
|
|
93730
|
-
label: `Installing domain from ${url3}`,
|
|
93731
|
-
|
|
93758
|
+
label: `Installing domain from ${url3} (operation ${operation2})`,
|
|
93759
|
+
recovery: { operation: operation2, retry },
|
|
93760
|
+
fn: async ({ session }) => await session.call(createPathCall(Path.project(exports_syscalls.install.ref).raw, directInstallCallInput(url3, operation2, opts.token))),
|
|
93732
93761
|
format: (result, fmtOpts, isRaw) => {
|
|
93733
93762
|
if (isRaw) {
|
|
93734
93763
|
output(result, fmtOpts);
|
|
93735
93764
|
return;
|
|
93736
93765
|
}
|
|
93737
93766
|
const installed = result.transitions[0]?.intent;
|
|
93738
|
-
if (installed
|
|
93767
|
+
if (!installed)
|
|
93739
93768
|
throw new Error("Kernel install returned no committed Domain transition.");
|
|
93740
|
-
}
|
|
93741
93769
|
const revision = installed.target?.schemaRevision ?? result.operation;
|
|
93742
93770
|
log.success(`Domain installed: ${installed.origin}@${revision}`);
|
|
93771
|
+
log.dim(` operation: ${result.operation}`);
|
|
93743
93772
|
if (isIdentityOverride(installed.origin, host) && installed.origin !== consentedOrigin) {
|
|
93744
93773
|
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
93774
|
}
|
|
93746
93775
|
}
|
|
93747
93776
|
});
|
|
93748
93777
|
}
|
|
93778
|
+
function directInstallRetry(url3, operation2, opts) {
|
|
93779
|
+
const instance = opts.instance === undefined ? "" : ` -i ${opts.instance}`;
|
|
93780
|
+
return `astrale domain install ${url3} --direct --operation ${operation2}${instance}`;
|
|
93781
|
+
}
|
|
93749
93782
|
function validateInstallUrl(value2) {
|
|
93750
93783
|
let url3;
|
|
93751
93784
|
try {
|
|
@@ -93798,12 +93831,13 @@ async function probeDeclaredOrigin(url3) {
|
|
|
93798
93831
|
return;
|
|
93799
93832
|
}
|
|
93800
93833
|
}
|
|
93801
|
-
var install_default, defaultAdminInstallDependencies;
|
|
93834
|
+
var OPERATION_ID_PATTERN, install_default, defaultAdminInstallDependencies, defaultDirectInstallDependencies;
|
|
93802
93835
|
var init_install3 = __esm(() => {
|
|
93803
93836
|
init_path5();
|
|
93804
93837
|
init_kernel2();
|
|
93805
93838
|
init_source();
|
|
93806
93839
|
init_connection2();
|
|
93840
|
+
init_errors12();
|
|
93807
93841
|
init_errors2();
|
|
93808
93842
|
init_admin_domain();
|
|
93809
93843
|
init_admin_instance();
|
|
@@ -93813,6 +93847,7 @@ var init_install3 = __esm(() => {
|
|
|
93813
93847
|
init_output();
|
|
93814
93848
|
init_prompt();
|
|
93815
93849
|
init_validation2();
|
|
93850
|
+
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
93851
|
install_default = {
|
|
93817
93852
|
name: "install",
|
|
93818
93853
|
description: "Install a domain on an instance (via the admin catalog, or --direct from a url)",
|
|
@@ -93831,6 +93866,10 @@ Behavior:
|
|
|
93831
93866
|
origin differs from its serving host, it requires explicit consent (an
|
|
93832
93867
|
interactive DANGER prompt, or --allow-identity-override in scripts).
|
|
93833
93868
|
|
|
93869
|
+
A fresh, strong operation id is generated automatically. Use --operation
|
|
93870
|
+
only to retry or recover the exact same direct install after an outcome-unknown
|
|
93871
|
+
timeout or disconnect.
|
|
93872
|
+
|
|
93834
93873
|
Examples:
|
|
93835
93874
|
$ astrale domain install crm.acme.dev -i staging # by origin, via admin
|
|
93836
93875
|
$ astrale domain install https://crm.acme.dev # by url, via admin
|
|
@@ -93855,12 +93894,19 @@ Examples:
|
|
|
93855
93894
|
flags: "--token <token>",
|
|
93856
93895
|
description: "Bearer token for private domain install endpoints (--direct only)"
|
|
93857
93896
|
},
|
|
93897
|
+
{
|
|
93898
|
+
flags: "--operation <uuid>",
|
|
93899
|
+
description: "Reuse an exact direct-install operation id for explicit retry/recovery"
|
|
93900
|
+
},
|
|
93858
93901
|
{
|
|
93859
93902
|
flags: "--allow-identity-override",
|
|
93860
93903
|
description: "Consent to a domain whose origin differs from its serving host (--direct only)"
|
|
93861
93904
|
}
|
|
93862
93905
|
],
|
|
93863
93906
|
action: async (target2, opts) => {
|
|
93907
|
+
if (opts.operation !== undefined && !opts.direct) {
|
|
93908
|
+
fatal(new AstraleError("INVALID_FLAG", "--operation is valid only with --direct.", "Ordinary direct installs generate a fresh operation id automatically."), opts);
|
|
93909
|
+
}
|
|
93864
93910
|
if (opts.direct) {
|
|
93865
93911
|
await installDirect(target2, opts);
|
|
93866
93912
|
return;
|
|
@@ -93873,6 +93919,11 @@ Examples:
|
|
|
93873
93919
|
listDomains: listAdminDomainsInContext,
|
|
93874
93920
|
install: installAdminDomainInContext
|
|
93875
93921
|
});
|
|
93922
|
+
defaultDirectInstallDependencies = Object.freeze({
|
|
93923
|
+
acceptOperationId,
|
|
93924
|
+
createOperationId,
|
|
93925
|
+
runKernelCommand
|
|
93926
|
+
});
|
|
93876
93927
|
});
|
|
93877
93928
|
|
|
93878
93929
|
// 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.0",
|
|
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
|
}
|
|
@@ -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
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ResponseError } from '@astrale-os/kernel-client'
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
3
|
|
|
3
4
|
import type { OwnedInstanceInfo } from '../../lib/admin-instance'
|
|
@@ -118,4 +119,69 @@ describe('admin domain install owner boundary', () => {
|
|
|
118
119
|
expect(calls).toEqual([])
|
|
119
120
|
expect(calls).toEqual([])
|
|
120
121
|
})
|
|
122
|
+
|
|
123
|
+
test('preserves an incompatible issuer reason returned through Admin', async () => {
|
|
124
|
+
inventory = [
|
|
125
|
+
{
|
|
126
|
+
id: 'owned-id',
|
|
127
|
+
slug: 'owned',
|
|
128
|
+
url: 'https://owned.eu.astrale.ai',
|
|
129
|
+
state: 'ready',
|
|
130
|
+
},
|
|
131
|
+
]
|
|
132
|
+
const domain = {
|
|
133
|
+
id: 'crm-id',
|
|
134
|
+
origin: 'crm.acme.dev',
|
|
135
|
+
name: 'CRM',
|
|
136
|
+
url: 'https://crm.acme.dev',
|
|
137
|
+
createdAt: '2026-08-20T00:00:00.000Z',
|
|
138
|
+
updatedAt: '2026-08-20T00:00:00.000Z',
|
|
139
|
+
}
|
|
140
|
+
const failure = new ResponseError(5001, 'Schema operation is not supported.', {
|
|
141
|
+
code: 'SCHEMA_UPGRADE_INCOMPATIBLE',
|
|
142
|
+
details: {
|
|
143
|
+
phase: 'upgrade',
|
|
144
|
+
origin: domain.origin,
|
|
145
|
+
issue: 'issuer-changed',
|
|
146
|
+
installedIssuer: 'https://old.example',
|
|
147
|
+
replacementIssuer: 'https://new.example',
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
const { installViaAdmin } = await import('../domain/install')
|
|
151
|
+
|
|
152
|
+
await expect(
|
|
153
|
+
installViaAdmin(
|
|
154
|
+
domain.origin,
|
|
155
|
+
{
|
|
156
|
+
instance: 'owned',
|
|
157
|
+
json: true,
|
|
158
|
+
noPrompt: true,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
listInstances: async () => inventory,
|
|
162
|
+
listDomains: async () => [domain],
|
|
163
|
+
install: async () => {
|
|
164
|
+
throw failure
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
),
|
|
168
|
+
).rejects.toEqual(new ExitError(1))
|
|
169
|
+
|
|
170
|
+
expect(JSON.parse(stderr)).toEqual({
|
|
171
|
+
error: 'RESPONSE_ERROR',
|
|
172
|
+
code: 5001,
|
|
173
|
+
message: 'Schema operation is not supported.',
|
|
174
|
+
reason: {
|
|
175
|
+
code: 'SCHEMA_UPGRADE_INCOMPATIBLE',
|
|
176
|
+
details: {
|
|
177
|
+
phase: 'upgrade',
|
|
178
|
+
origin: 'crm.acme.dev',
|
|
179
|
+
issue: 'issuer-changed',
|
|
180
|
+
installedIssuer: 'https://old.example',
|
|
181
|
+
replacementIssuer: 'https://new.example',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
hint: expect.stringContaining('astrale domain uninstall crm.acme.dev'),
|
|
185
|
+
})
|
|
186
|
+
})
|
|
121
187
|
})
|
|
@@ -4,8 +4,9 @@ import { directInstallCallInput } from '../domain/install'
|
|
|
4
4
|
|
|
5
5
|
describe('directInstallCallInput', () => {
|
|
6
6
|
test('sends the current remote install syscall, not a legacy url list', () => {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const operation = '4a4c9a18-50f6-4d84-a7b7-2d83e3e45dc8'
|
|
8
|
+
expect(directInstallCallInput('https://tasks.example.test', operation, 'secret')).toEqual({
|
|
9
|
+
operation,
|
|
9
10
|
domains: [
|
|
10
11
|
{
|
|
11
12
|
source: {
|