@astrale-os/adapter-astrale 0.5.0-beta.21 → 0.5.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +2 -10
- package/dist/adapter.js +16 -254
- package/dist/cli/commands/call.d.ts +8 -0
- package/dist/cli/commands/call.js +26 -0
- package/dist/cli/commands/install-domain.d.ts +9 -0
- package/dist/cli/commands/install-domain.js +28 -0
- package/dist/cli/commands/introspect.d.ts +7 -0
- package/dist/cli/commands/introspect.js +26 -0
- package/dist/cli/commands/version.d.ts +5 -0
- package/dist/cli/commands/version.js +49 -0
- package/dist/cli/error.d.ts +7 -0
- package/dist/cli/error.js +13 -0
- package/dist/cli/identity.d.ts +1 -0
- package/dist/cli/identity.js +3 -0
- package/dist/cli/json.d.ts +3 -0
- package/dist/cli/json.js +25 -0
- package/dist/cli/process.d.ts +17 -0
- package/dist/cli/process.js +108 -0
- package/dist/deploy/deploy.d.ts +19 -0
- package/dist/deploy/deploy.js +178 -0
- package/dist/{prerequisite.d.ts → deploy/ensure-services.d.ts} +5 -2
- package/dist/{prerequisite.js → deploy/ensure-services.js} +21 -28
- package/dist/deploy/install.d.ts +13 -0
- package/dist/deploy/install.js +12 -0
- package/dist/deploy/progress.d.ts +7 -0
- package/dist/deploy/progress.js +18 -0
- package/dist/{service → deploy}/readiness.d.ts +1 -1
- package/dist/deploy/readiness.js +42 -0
- package/dist/deploy/service/coordinate.d.ts +1 -0
- package/dist/deploy/service/coordinate.js +14 -0
- package/dist/deploy/service/display-name.d.ts +1 -0
- package/dist/deploy/service/display-name.js +6 -0
- package/dist/deploy/service/method.d.ts +1 -0
- package/dist/deploy/service/method.js +3 -0
- package/dist/deploy/service/request.d.ts +42 -0
- package/dist/deploy/service/request.js +60 -0
- package/dist/deploy/service/response.d.ts +4 -0
- package/dist/deploy/service/response.js +35 -0
- package/dist/params.d.ts +1 -1
- package/dist/prepare/parameters.d.ts +7 -0
- package/dist/prepare/parameters.js +10 -0
- package/dist/prepare/prepare.d.ts +3 -0
- package/dist/prepare/prepare.js +5 -0
- package/dist/prepare/watch.d.ts +3 -0
- package/dist/prepare/watch.js +6 -0
- package/package.json +3 -3
- package/dist/cli.d.ts +0 -15
- package/dist/cli.js +0 -48
- package/dist/service/addressing.d.ts +0 -3
- package/dist/service/addressing.js +0 -16
- package/dist/service/artifact.d.ts +0 -34
- package/dist/service/artifact.js +0 -122
- package/dist/service/readiness.js +0 -16
package/dist/adapter.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import type { ArtifactSource, PrepareContext, WatchContext, WatchHandle } from '@astrale-os/sdk/deployment/adapter';
|
|
2
|
-
import {
|
|
2
|
+
import type { AstraleDeployDependencies } from './deploy/deploy.js';
|
|
3
3
|
import type { AstraleParams } from './params.js';
|
|
4
|
-
|
|
5
|
-
import { ensureServices } from './prerequisite.js';
|
|
6
|
-
import { waitForReadiness } from './service/readiness.js';
|
|
7
|
-
export interface AstraleDependencies {
|
|
4
|
+
export interface AstraleDependencies extends AstraleDeployDependencies {
|
|
8
5
|
prepare(parameters: AstraleParams, context: PrepareContext): Promise<ArtifactSource>;
|
|
9
6
|
watch(parameters: AstraleParams, context: WatchContext): Promise<WatchHandle>;
|
|
10
|
-
ensureServices: typeof ensureServices;
|
|
11
|
-
callAstrale: typeof callAstrale;
|
|
12
|
-
runAstrale: typeof runAstrale;
|
|
13
|
-
waitForReadiness: typeof waitForReadiness;
|
|
14
|
-
signingIdentity: typeof signingIdentity;
|
|
15
7
|
}
|
|
16
8
|
export declare function astrale(environments: Readonly<Record<string, AstraleParams>>): import("@astrale-os/sdk/deployment/adapter").Adapter<AstraleParams>;
|
|
17
9
|
export declare function astraleWithDependencies(environments: Readonly<Record<string, AstraleParams>>, dependencies: AstraleDependencies): import("@astrale-os/sdk/deployment/adapter").Adapter<AstraleParams>;
|
package/dist/adapter.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { signingIdentity, signingIdentitySecrets } from '@astrale-os/adapter-cloudflare/build';
|
|
1
|
+
import { signingIdentity } from '@astrale-os/adapter-cloudflare/build';
|
|
3
2
|
import { defineAdapter } from '@astrale-os/sdk/deployment/adapter';
|
|
4
|
-
import { addressing, assemble } from '@astrale-os/sdk/deployment/release';
|
|
5
3
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { callAstrale
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
import { callAstrale } from './cli/commands/call.js';
|
|
5
|
+
import { installDomain } from './cli/commands/install-domain.js';
|
|
6
|
+
import { introspectDomain } from './cli/commands/introspect.js';
|
|
7
|
+
import { requireCliVersion } from './cli/commands/version.js';
|
|
8
|
+
import { deployAstrale } from './deploy/deploy.js';
|
|
9
|
+
import { ensureServices } from './deploy/ensure-services.js';
|
|
10
|
+
import { waitForReadiness } from './deploy/readiness.js';
|
|
11
|
+
import { prepareAstrale } from './prepare/prepare.js';
|
|
12
|
+
import { watchAstrale } from './prepare/watch.js';
|
|
13
13
|
const packageVersion = createRequire(import.meta.url)('../package.json')
|
|
14
14
|
.version;
|
|
15
|
-
const local = cloudflare({ internal: {} });
|
|
16
15
|
export function astrale(environments) {
|
|
17
16
|
return astraleWithDependencies(environments, {
|
|
18
|
-
prepare:
|
|
19
|
-
watch:
|
|
17
|
+
prepare: prepareAstrale,
|
|
18
|
+
watch: watchAstrale,
|
|
20
19
|
ensureServices,
|
|
21
20
|
callAstrale,
|
|
22
|
-
|
|
21
|
+
introspectDomain,
|
|
22
|
+
installDomain,
|
|
23
|
+
requireCliVersion,
|
|
23
24
|
waitForReadiness,
|
|
24
25
|
signingIdentity,
|
|
25
26
|
});
|
|
@@ -31,246 +32,7 @@ export function astraleWithDependencies(environments, dependencies) {
|
|
|
31
32
|
environments,
|
|
32
33
|
prepare: dependencies.prepare,
|
|
33
34
|
watch: dependencies.watch,
|
|
34
|
-
|
|
35
|
-
if (parameters.instance === undefined) {
|
|
36
|
-
return failed('prerequisite', progress(), new TypeError('Astrale deployment requires an instance.'));
|
|
37
|
-
}
|
|
38
|
-
let secrets;
|
|
39
|
-
let signing;
|
|
40
|
-
try {
|
|
41
|
-
signing = await dependencies.signingIdentity({
|
|
42
|
-
projectDir: context.projectDir,
|
|
43
|
-
source: parameters.signingIdentity,
|
|
44
|
-
origin: context.artifact.build.schema.compiled.root.origin,
|
|
45
|
-
mode: 'deploy',
|
|
46
|
-
});
|
|
47
|
-
secrets = signingIdentitySecrets(context.secrets, signing);
|
|
48
|
-
}
|
|
49
|
-
catch (cause) {
|
|
50
|
-
return failed('prerequisite', progress(), cause);
|
|
51
|
-
}
|
|
52
|
-
const instance = parameters.instance;
|
|
53
|
-
const servicesOrigin = parameters.servicesOrigin ?? SERVICES_ORIGIN;
|
|
54
|
-
const identity = identityArguments(parameters.identity);
|
|
55
|
-
let prerequisite;
|
|
56
|
-
try {
|
|
57
|
-
const evidence = await dependencies.ensureServices({
|
|
58
|
-
projectDir: context.projectDir,
|
|
59
|
-
instance,
|
|
60
|
-
servicesOrigin,
|
|
61
|
-
identity,
|
|
62
|
-
run: dependencies.runAstrale,
|
|
63
|
-
});
|
|
64
|
-
prerequisite =
|
|
65
|
-
evidence.state === 'installed'
|
|
66
|
-
? Object.freeze({ kind: 'applied', evidence })
|
|
67
|
-
: NONE;
|
|
68
|
-
}
|
|
69
|
-
catch (cause) {
|
|
70
|
-
const prerequisite = cause instanceof ServicesPrerequisiteError && cause.mutation === 'installed'
|
|
71
|
-
? Object.freeze({
|
|
72
|
-
kind: 'applied',
|
|
73
|
-
evidence: Object.freeze({ origin: servicesOrigin, state: 'installed' }),
|
|
74
|
-
})
|
|
75
|
-
: cause instanceof ServicesPrerequisiteError && cause.mutation === 'unknown'
|
|
76
|
-
? Object.freeze({ kind: 'unknown' })
|
|
77
|
-
: NONE;
|
|
78
|
-
return failed('prerequisite', progress({ prerequisite }), cause);
|
|
79
|
-
}
|
|
80
|
-
const origin = context.artifact.build.schema.compiled.root.origin;
|
|
81
|
-
const name = parameters.name ?? serviceName(origin);
|
|
82
|
-
let payload;
|
|
83
|
-
try {
|
|
84
|
-
payload = deploymentPayload(context.artifact, { origin, name, vars: parameters.vars });
|
|
85
|
-
}
|
|
86
|
-
catch (cause) {
|
|
87
|
-
return failed('provider', progress({ prerequisite }), cause);
|
|
88
|
-
}
|
|
89
|
-
let deployed;
|
|
90
|
-
try {
|
|
91
|
-
deployed = await dependencies.callAstrale({
|
|
92
|
-
projectDir: context.projectDir,
|
|
93
|
-
instance,
|
|
94
|
-
path: servicesDeployMethod(servicesOrigin),
|
|
95
|
-
identity,
|
|
96
|
-
data: payload,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
catch (cause) {
|
|
100
|
-
return failed('provider', progress({ prerequisite, provider: { kind: 'unknown' } }), cause);
|
|
101
|
-
}
|
|
102
|
-
const expectedPath = servicePath(origin);
|
|
103
|
-
let observation;
|
|
104
|
-
try {
|
|
105
|
-
observation = deployedService(deployed, expectedPath);
|
|
106
|
-
}
|
|
107
|
-
catch (cause) {
|
|
108
|
-
return failed('provider', progress({
|
|
109
|
-
prerequisite,
|
|
110
|
-
provider: { kind: 'partial' },
|
|
111
|
-
}), cause);
|
|
112
|
-
}
|
|
113
|
-
const url = observation.url;
|
|
114
|
-
const applied = Object.freeze({
|
|
115
|
-
kind: 'applied',
|
|
116
|
-
evidence: Object.freeze({
|
|
117
|
-
provider: 'astrale',
|
|
118
|
-
deployment: observation.digest,
|
|
119
|
-
artifact: context.artifact.digest,
|
|
120
|
-
}),
|
|
121
|
-
});
|
|
122
|
-
if (url === undefined) {
|
|
123
|
-
return failed('addressing', progress({ prerequisite, provider: applied }), new Error('Astrale Services deployment returned no URL.'));
|
|
124
|
-
}
|
|
125
|
-
let selected;
|
|
126
|
-
try {
|
|
127
|
-
selected = addressing(url);
|
|
128
|
-
}
|
|
129
|
-
catch (cause) {
|
|
130
|
-
return failed('addressing', progress({ prerequisite, provider: applied }), cause);
|
|
131
|
-
}
|
|
132
|
-
const provider = Object.freeze({
|
|
133
|
-
kind: 'applied',
|
|
134
|
-
evidence: Object.freeze({ ...applied.evidence, issuer: selected.issuer }),
|
|
135
|
-
});
|
|
136
|
-
let configuration = NONE;
|
|
137
|
-
const secretNames = [signing.binding, ...Object.keys(context.secrets).sort()];
|
|
138
|
-
const configured = [];
|
|
139
|
-
if (secretNames.length > 0) {
|
|
140
|
-
try {
|
|
141
|
-
for (const secret of secretNames) {
|
|
142
|
-
await dependencies.callAstrale({
|
|
143
|
-
projectDir: context.projectDir,
|
|
144
|
-
instance,
|
|
145
|
-
path: `${servicePath(origin)}::setSecret`,
|
|
146
|
-
identity,
|
|
147
|
-
data: { name: secret, value: secrets[secret] },
|
|
148
|
-
});
|
|
149
|
-
configured.push(secret);
|
|
150
|
-
}
|
|
151
|
-
configuration = Object.freeze({
|
|
152
|
-
kind: 'applied',
|
|
153
|
-
evidence: Object.freeze({ names: secretNames }),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch (cause) {
|
|
157
|
-
const configuration = configured.length === 0
|
|
158
|
-
? { kind: 'unknown' }
|
|
159
|
-
: {
|
|
160
|
-
kind: 'partial',
|
|
161
|
-
evidence: Object.freeze({ names: Object.freeze([...configured]) }),
|
|
162
|
-
};
|
|
163
|
-
return failed('configuration', progress({ prerequisite, provider, configuration }), cause);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
let release;
|
|
167
|
-
try {
|
|
168
|
-
release = assemble(context.artifact.build, selected);
|
|
169
|
-
}
|
|
170
|
-
catch (cause) {
|
|
171
|
-
return failed('release', progress({ prerequisite, provider, configuration }), cause);
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
await dependencies.waitForReadiness(release);
|
|
175
|
-
}
|
|
176
|
-
catch (cause) {
|
|
177
|
-
return failed('readiness', progress({ prerequisite, provider, configuration }), cause, release);
|
|
178
|
-
}
|
|
179
|
-
let installation;
|
|
180
|
-
try {
|
|
181
|
-
await dependencies.runAstrale({
|
|
182
|
-
projectDir: context.projectDir,
|
|
183
|
-
input: [
|
|
184
|
-
'domain',
|
|
185
|
-
'install',
|
|
186
|
-
url,
|
|
187
|
-
'--direct',
|
|
188
|
-
'-i',
|
|
189
|
-
instance,
|
|
190
|
-
'--allow-identity-override',
|
|
191
|
-
'--json',
|
|
192
|
-
'--timeout',
|
|
193
|
-
'240000',
|
|
194
|
-
...identity,
|
|
195
|
-
],
|
|
196
|
-
});
|
|
197
|
-
installation = Object.freeze({
|
|
198
|
-
kind: 'applied',
|
|
199
|
-
evidence: Object.freeze({ instance, origin }),
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
catch (cause) {
|
|
203
|
-
return failed('installation', progress({ prerequisite, provider, configuration, installation: { kind: 'unknown' } }), cause, release);
|
|
204
|
-
}
|
|
205
|
-
return Object.freeze({
|
|
206
|
-
status: 'ready',
|
|
207
|
-
phase: 'complete',
|
|
208
|
-
progress: Object.freeze({
|
|
209
|
-
...progress({ prerequisite, configuration, installation }),
|
|
210
|
-
provider,
|
|
211
|
-
}),
|
|
212
|
-
release,
|
|
213
|
-
});
|
|
214
|
-
},
|
|
35
|
+
deploy: (parameters, context) => deployAstrale(parameters, context, dependencies),
|
|
215
36
|
secretsFile: (parameters) => parameters.secrets,
|
|
216
37
|
});
|
|
217
38
|
}
|
|
218
|
-
function cloudflareParameters(parameters, watch) {
|
|
219
|
-
return Object.freeze({
|
|
220
|
-
...(parameters.port === undefined ? {} : { port: parameters.port }),
|
|
221
|
-
...(watch && parameters.host !== undefined ? { host: parameters.host } : {}),
|
|
222
|
-
...(parameters.vars === undefined ? {} : { vars: parameters.vars }),
|
|
223
|
-
...(parameters.signingIdentity === undefined
|
|
224
|
-
? {}
|
|
225
|
-
: { signingIdentity: parameters.signingIdentity }),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
function deployedService(input, expectedPath) {
|
|
229
|
-
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
230
|
-
invalidDeployment();
|
|
231
|
-
const value = input;
|
|
232
|
-
if (value.error !== undefined && value.error !== null)
|
|
233
|
-
invalidDeployment();
|
|
234
|
-
if (value.state !== 'ready') {
|
|
235
|
-
throw new TypeError('Astrale Services deployment did not report ready provider state.');
|
|
236
|
-
}
|
|
237
|
-
if (value.path !== expectedPath) {
|
|
238
|
-
throw new TypeError('Astrale Services deployment returned a different Service path.');
|
|
239
|
-
}
|
|
240
|
-
if (typeof value.digest !== 'string' || value.digest.length === 0) {
|
|
241
|
-
throw new TypeError('Astrale Services deployment returned an invalid provider digest.');
|
|
242
|
-
}
|
|
243
|
-
let url;
|
|
244
|
-
if (typeof value.url === 'string') {
|
|
245
|
-
try {
|
|
246
|
-
url = new URL(value.url).href.replace(/\/$/u, '');
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
// Addressing admission below retains the successful provider effect.
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return Object.freeze({
|
|
253
|
-
...(url === undefined ? {} : { url }),
|
|
254
|
-
digest: value.digest,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
function invalidDeployment() {
|
|
258
|
-
throw new TypeError('Astrale Services deployment returned invalid provider evidence.');
|
|
259
|
-
}
|
|
260
|
-
function progress(input = {}) {
|
|
261
|
-
return Object.freeze({
|
|
262
|
-
prerequisite: input.prerequisite ?? NONE,
|
|
263
|
-
provider: input.provider ?? NONE,
|
|
264
|
-
configuration: input.configuration ?? NONE,
|
|
265
|
-
installation: input.installation ?? NONE,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
function failed(phase, deploymentProgress, cause, release) {
|
|
269
|
-
return Object.freeze({
|
|
270
|
-
status: 'failed',
|
|
271
|
-
phase,
|
|
272
|
-
progress: deploymentProgress,
|
|
273
|
-
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
274
|
-
...(release === undefined ? {} : { release }),
|
|
275
|
-
});
|
|
276
|
-
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CliProcessError } from '../error.js';
|
|
2
|
+
import { decodeJson } from '../json.js';
|
|
3
|
+
import { runCliProcess } from '../process.js';
|
|
4
|
+
export async function callAstrale(input) {
|
|
5
|
+
const result = await runCliProcess({
|
|
6
|
+
projectDir: input.projectDir,
|
|
7
|
+
args: [
|
|
8
|
+
'--ci',
|
|
9
|
+
'--no-prompt',
|
|
10
|
+
'call',
|
|
11
|
+
input.path,
|
|
12
|
+
'-i',
|
|
13
|
+
input.instance,
|
|
14
|
+
'--json',
|
|
15
|
+
'--timeout',
|
|
16
|
+
'240000',
|
|
17
|
+
...input.identity,
|
|
18
|
+
],
|
|
19
|
+
stdin: JSON.stringify(input.data),
|
|
20
|
+
signal: input.signal,
|
|
21
|
+
timeoutMs: 250_000,
|
|
22
|
+
});
|
|
23
|
+
if (!result.ok)
|
|
24
|
+
throw new CliProcessError('command', `call ${input.path}`, result.failure);
|
|
25
|
+
return decodeJson(result.stdout, `astrale call ${input.path} success`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function installDomain(input: {
|
|
2
|
+
readonly projectDir: string;
|
|
3
|
+
readonly instance: string;
|
|
4
|
+
readonly target: string;
|
|
5
|
+
readonly direct: boolean;
|
|
6
|
+
readonly allowIdentityOverride?: boolean;
|
|
7
|
+
readonly identity: readonly string[];
|
|
8
|
+
readonly signal: AbortSignal;
|
|
9
|
+
}): Promise<unknown>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CliProcessError } from '../error.js';
|
|
2
|
+
import { decodeJson } from '../json.js';
|
|
3
|
+
import { runCliProcess } from '../process.js';
|
|
4
|
+
export async function installDomain(input) {
|
|
5
|
+
const result = await runCliProcess({
|
|
6
|
+
projectDir: input.projectDir,
|
|
7
|
+
args: [
|
|
8
|
+
'--ci',
|
|
9
|
+
'--no-prompt',
|
|
10
|
+
'domain',
|
|
11
|
+
'install',
|
|
12
|
+
input.target,
|
|
13
|
+
...(input.direct ? ['--direct'] : []),
|
|
14
|
+
'-i',
|
|
15
|
+
input.instance,
|
|
16
|
+
...(input.allowIdentityOverride ? ['--allow-identity-override'] : []),
|
|
17
|
+
'--json',
|
|
18
|
+
'--timeout',
|
|
19
|
+
'240000',
|
|
20
|
+
...input.identity,
|
|
21
|
+
],
|
|
22
|
+
signal: input.signal,
|
|
23
|
+
timeoutMs: 250_000,
|
|
24
|
+
});
|
|
25
|
+
if (!result.ok)
|
|
26
|
+
throw new CliProcessError('command', 'domain install', result.failure);
|
|
27
|
+
return decodeJson(result.stdout, 'astrale domain install success');
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CliProcessError } from '../error.js';
|
|
2
|
+
import { decodeJson } from '../json.js';
|
|
3
|
+
import { runCliProcess } from '../process.js';
|
|
4
|
+
export async function introspectDomain(input) {
|
|
5
|
+
const result = await runCliProcess({
|
|
6
|
+
projectDir: input.projectDir,
|
|
7
|
+
args: [
|
|
8
|
+
'--ci',
|
|
9
|
+
'--no-prompt',
|
|
10
|
+
'introspect',
|
|
11
|
+
input.origin,
|
|
12
|
+
'-i',
|
|
13
|
+
input.instance,
|
|
14
|
+
'--json',
|
|
15
|
+
'--timeout',
|
|
16
|
+
'60000',
|
|
17
|
+
...input.identity,
|
|
18
|
+
],
|
|
19
|
+
signal: input.signal,
|
|
20
|
+
timeoutMs: 70_000,
|
|
21
|
+
});
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
throw new CliProcessError('command', `introspect ${input.origin}`, result.failure);
|
|
24
|
+
}
|
|
25
|
+
return decodeJson(result.stdout, `astrale introspect ${input.origin} success`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { CliProcessError } from '../error.js';
|
|
2
|
+
import { runCliProcess } from '../process.js';
|
|
3
|
+
export const MINIMUM_ASTRALE_CLI_VERSION = '1.0.0-beta.8';
|
|
4
|
+
export async function requireCliVersion(input) {
|
|
5
|
+
const result = await runCliProcess({
|
|
6
|
+
projectDir: input.projectDir,
|
|
7
|
+
args: ['--version'],
|
|
8
|
+
signal: input.signal,
|
|
9
|
+
timeoutMs: 10_000,
|
|
10
|
+
stdoutLimit: 256,
|
|
11
|
+
stderrLimit: 4_096,
|
|
12
|
+
});
|
|
13
|
+
if (!result.ok)
|
|
14
|
+
throw new CliProcessError('command', 'version', result.failure);
|
|
15
|
+
const version = result.stdout.trim().replace(/^v/u, '');
|
|
16
|
+
if (!isCompatible(version)) {
|
|
17
|
+
throw new TypeError(`Astrale CLI ${version || '<invalid>'} is incompatible; ${MINIMUM_ASTRALE_CLI_VERSION} or newer is required.`);
|
|
18
|
+
}
|
|
19
|
+
return version;
|
|
20
|
+
}
|
|
21
|
+
function isCompatible(input) {
|
|
22
|
+
const value = semver(input);
|
|
23
|
+
const minimum = semver(MINIMUM_ASTRALE_CLI_VERSION);
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return false;
|
|
26
|
+
for (let index = 0; index < 3; index += 1) {
|
|
27
|
+
if (value.core[index] > minimum.core[index])
|
|
28
|
+
return true;
|
|
29
|
+
if (value.core[index] < minimum.core[index])
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (value.prerelease === undefined)
|
|
33
|
+
return true;
|
|
34
|
+
if (minimum.prerelease === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
if (value.prerelease.name !== minimum.prerelease.name) {
|
|
37
|
+
return value.prerelease.name.localeCompare(minimum.prerelease.name) > 0;
|
|
38
|
+
}
|
|
39
|
+
return value.prerelease.number >= minimum.prerelease.number;
|
|
40
|
+
}
|
|
41
|
+
function semver(input) {
|
|
42
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([a-z]+)\.(\d+))?$/u.exec(input);
|
|
43
|
+
if (match === null)
|
|
44
|
+
return undefined;
|
|
45
|
+
return {
|
|
46
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
47
|
+
...(match[4] === undefined ? {} : { prerelease: { name: match[4], number: Number(match[5]) } }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type CliProcessFailureKind = 'cancelled' | 'timeout' | 'output-limit' | 'spawn' | 'protocol' | 'command';
|
|
2
|
+
export declare class CliProcessError extends Error {
|
|
3
|
+
readonly kind: CliProcessFailureKind;
|
|
4
|
+
readonly command: string;
|
|
5
|
+
readonly diagnostic?: Readonly<Record<string, unknown>> | undefined;
|
|
6
|
+
constructor(kind: CliProcessFailureKind, command: string, diagnostic?: Readonly<Record<string, unknown>> | undefined, options?: ErrorOptions);
|
|
7
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class CliProcessError extends Error {
|
|
2
|
+
kind;
|
|
3
|
+
command;
|
|
4
|
+
diagnostic;
|
|
5
|
+
constructor(kind, command, diagnostic, options) {
|
|
6
|
+
const code = typeof diagnostic?.error === 'string' ? ` (${diagnostic.error})` : '';
|
|
7
|
+
super(`Astrale CLI ${command} failed: ${kind}${code}.`, options);
|
|
8
|
+
this.kind = kind;
|
|
9
|
+
this.command = command;
|
|
10
|
+
this.diagnostic = diagnostic;
|
|
11
|
+
this.name = 'CliProcessError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function identityArguments(identity: string | undefined): readonly string[];
|
package/dist/cli/json.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function decodeJson(input, label) {
|
|
2
|
+
if (input.trim().length === 0)
|
|
3
|
+
throw new TypeError(`${label} is empty.`);
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(input);
|
|
6
|
+
}
|
|
7
|
+
catch (cause) {
|
|
8
|
+
throw new TypeError(`${label} is not one complete JSON value.`, { cause });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function decodeFailure(input) {
|
|
12
|
+
const value = decodeJson(input, 'Astrale CLI failure');
|
|
13
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
invalid();
|
|
15
|
+
const record = value;
|
|
16
|
+
if (typeof record.error !== 'string' || record.error.length === 0)
|
|
17
|
+
invalid();
|
|
18
|
+
return Object.freeze({ error: record.error });
|
|
19
|
+
}
|
|
20
|
+
export function isDomainNotInstalledFailure(input) {
|
|
21
|
+
return input.error === 'DOMAIN_NOT_INSTALLED';
|
|
22
|
+
}
|
|
23
|
+
function invalid() {
|
|
24
|
+
throw new TypeError('Astrale CLI failure envelope is invalid.');
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CliProcessInput {
|
|
2
|
+
readonly projectDir: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
readonly signal: AbortSignal;
|
|
5
|
+
readonly timeoutMs: number;
|
|
6
|
+
readonly stdin?: string;
|
|
7
|
+
readonly stdoutLimit?: number;
|
|
8
|
+
readonly stderrLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
export type CliProcessResult = {
|
|
11
|
+
readonly ok: true;
|
|
12
|
+
readonly stdout: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly ok: false;
|
|
15
|
+
readonly failure: Readonly<Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
export declare function runCliProcess(input: CliProcessInput): Promise<CliProcessResult>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { CliProcessError } from './error.js';
|
|
3
|
+
import { decodeFailure } from './json.js';
|
|
4
|
+
const DEFAULT_LIMIT = 256 * 1024;
|
|
5
|
+
const TERMINATION_GRACE_MS = 1_000;
|
|
6
|
+
export function runCliProcess(input) {
|
|
7
|
+
const command = commandName(input.args);
|
|
8
|
+
if (input.signal.aborted) {
|
|
9
|
+
return Promise.reject(new CliProcessError('cancelled', command));
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0) {
|
|
12
|
+
return Promise.reject(new TypeError('Astrale CLI timeout must be a positive integer.'));
|
|
13
|
+
}
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let child;
|
|
16
|
+
try {
|
|
17
|
+
child = spawn('astrale', [...input.args], {
|
|
18
|
+
cwd: input.projectDir,
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
shell: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
reject(new CliProcessError('spawn', command, undefined, { cause }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let stdout = '';
|
|
28
|
+
let stderr = '';
|
|
29
|
+
let stdoutBytes = 0;
|
|
30
|
+
let stderrBytes = 0;
|
|
31
|
+
let settled = false;
|
|
32
|
+
let terminationKind;
|
|
33
|
+
let escalation;
|
|
34
|
+
const timeout = setTimeout(() => terminate('timeout'), input.timeoutMs);
|
|
35
|
+
const abort = () => terminate('cancelled');
|
|
36
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
37
|
+
const finish = (handler) => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
settled = true;
|
|
41
|
+
clearTimeout(timeout);
|
|
42
|
+
if (escalation !== undefined)
|
|
43
|
+
clearTimeout(escalation);
|
|
44
|
+
input.signal.removeEventListener('abort', abort);
|
|
45
|
+
handler();
|
|
46
|
+
};
|
|
47
|
+
const fail = (error) => finish(() => reject(error));
|
|
48
|
+
const terminate = (kind) => {
|
|
49
|
+
if (settled || terminationKind !== undefined)
|
|
50
|
+
return;
|
|
51
|
+
terminationKind = kind;
|
|
52
|
+
child.stdin.destroy();
|
|
53
|
+
child.kill('SIGTERM');
|
|
54
|
+
escalation = setTimeout(() => {
|
|
55
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
56
|
+
child.kill('SIGKILL');
|
|
57
|
+
}, TERMINATION_GRACE_MS);
|
|
58
|
+
};
|
|
59
|
+
child.stdout.on('data', (chunk) => {
|
|
60
|
+
stdoutBytes += chunk.byteLength;
|
|
61
|
+
if (stdoutBytes > (input.stdoutLimit ?? DEFAULT_LIMIT))
|
|
62
|
+
terminate('output-limit');
|
|
63
|
+
else
|
|
64
|
+
stdout += chunk.toString('utf8');
|
|
65
|
+
});
|
|
66
|
+
child.stderr.on('data', (chunk) => {
|
|
67
|
+
stderrBytes += chunk.byteLength;
|
|
68
|
+
if (stderrBytes > (input.stderrLimit ?? DEFAULT_LIMIT))
|
|
69
|
+
terminate('output-limit');
|
|
70
|
+
else
|
|
71
|
+
stderr += chunk.toString('utf8');
|
|
72
|
+
});
|
|
73
|
+
child.once('error', (cause) => fail(terminationKind === undefined
|
|
74
|
+
? new CliProcessError('spawn', command, undefined, { cause })
|
|
75
|
+
: new CliProcessError(terminationKind, command, undefined, { cause })));
|
|
76
|
+
child.once('close', (code) => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
if (terminationKind !== undefined) {
|
|
80
|
+
fail(new CliProcessError(terminationKind, command));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const out = stdout;
|
|
84
|
+
const error = stderr;
|
|
85
|
+
if (code === 0) {
|
|
86
|
+
finish(() => resolve(Object.freeze({ ok: true, stdout: out })));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
let failure;
|
|
90
|
+
try {
|
|
91
|
+
failure = decodeFailure(error);
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
fail(new CliProcessError('protocol', command, undefined, { cause }));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
finish(() => resolve(Object.freeze({ ok: false, failure })));
|
|
98
|
+
});
|
|
99
|
+
if (input.stdin === undefined)
|
|
100
|
+
child.stdin.end();
|
|
101
|
+
else
|
|
102
|
+
child.stdin.end(input.stdin);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function commandName(args) {
|
|
106
|
+
const visible = args.filter((arg) => !arg.startsWith('-')).slice(0, 2);
|
|
107
|
+
return visible.join(' ') || 'version';
|
|
108
|
+
}
|