@astrale-os/sdk 0.6.0-beta.16 → 0.6.0-beta.18
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/deployment/address/summary.d.ts +3 -2
- package/dist/execution/identity/jwks.js +57 -12
- package/dist/platform/schema/property.d.ts +5 -0
- package/dist/tooling/cli/arguments.d.ts +2 -0
- package/dist/tooling/cli/arguments.js +9 -0
- package/dist/tooling/cli/help.js +26 -14
- package/dist/tooling/cli/index.d.ts +1 -1
- package/dist/tooling/cli/installed-on.d.ts +83 -0
- package/dist/tooling/cli/installed-on.js +156 -0
- package/dist/tooling/cli/list.d.ts +56 -22
- package/dist/tooling/cli/list.js +111 -32
- package/dist/tooling/cli/orchestrate.js +23 -5
- package/dist/tooling/cli/registry/client.d.ts +11 -11
- package/dist/tooling/cli/registry/client.js +41 -18
- package/dist/tooling/cli/registry/error.d.ts +11 -0
- package/dist/tooling/cli/registry/error.js +17 -0
- package/dist/tooling/cli/registry/executable.d.ts +9 -0
- package/dist/tooling/cli/registry/executable.js +14 -3
- package/dist/tooling/cli/registry/index.d.ts +4 -1
- package/dist/tooling/cli/registry/index.js +3 -1
- package/dist/tooling/cli/registry/installations.d.ts +67 -0
- package/dist/tooling/cli/registry/installations.js +108 -0
- package/dist/tooling/cli/registry/process.d.ts +16 -1
- package/dist/tooling/cli/registry/process.js +39 -4
- package/package.json +6 -6
package/dist/tooling/cli/list.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { acceptDeploymentRecord } from '../../deployment/address/index.js';
|
|
2
2
|
import { deploymentName } from '../../platform/versioning/name.js';
|
|
3
3
|
import { selectDeployMode } from './deploy-mode.js';
|
|
4
|
+
import { installationNotices, installedOn, installedOnCell, readInstallations, skippedInstallations, } from './installed-on.js';
|
|
4
5
|
/**
|
|
5
|
-
* List the deployments of the selected Environments, one adapter `list` call each
|
|
6
|
+
* List the deployments of the selected Environments, one adapter `list` call each, then name them
|
|
7
|
+
* and say where they are installed (tech [.59528]: the records, the registry's versions and the
|
|
8
|
+
* instances' pins).
|
|
6
9
|
*
|
|
7
10
|
* Only an Environment that deploys immutable deployments (CT33 canonical) is listed; the others are
|
|
8
11
|
* `unlisted`, with the reason. A record the host returns but `acceptDeploymentRecord` refuses never
|
|
@@ -11,9 +14,17 @@ import { selectDeployMode } from './deploy-mode.js';
|
|
|
11
14
|
* record itself may fail its own list (Admin's Services does, AM-152). An Environment whose host
|
|
12
15
|
* cannot be listed, or whose adapter answers anything but deployments, is a failure, and the others
|
|
13
16
|
* are still listed.
|
|
17
|
+
*
|
|
18
|
+
* Once a deployment is listed, the registry is opened once: its index of the origin names each
|
|
19
|
+
* deployment by the version of its release or build (CT28; none when the origin is unregistered or
|
|
20
|
+
* not readable), and the instances that pin each deployment come from each `--instance` (CT24);
|
|
21
|
+
* Admin does not say which instances pin a release (AM-241). A registry that cannot be opened or
|
|
22
|
+
* whose index cannot be read is `registryFailure`, and the instance reads still running are
|
|
23
|
+
* stopped at once; an instance that cannot tell never fails the listing: it is partial (AM-198,
|
|
24
|
+
* AM-224).
|
|
14
25
|
*/
|
|
15
26
|
export async function listDeployments(input) {
|
|
16
|
-
const
|
|
27
|
+
const hosted = [];
|
|
17
28
|
const unlisted = [];
|
|
18
29
|
const notices = [];
|
|
19
30
|
const failures = [];
|
|
@@ -62,58 +73,116 @@ export async function listDeployments(input) {
|
|
|
62
73
|
continue;
|
|
63
74
|
}
|
|
64
75
|
for (const deployment of listed) {
|
|
65
|
-
const row =
|
|
76
|
+
const row = hostedDeployment(deployment, { origin: input.origin, environment, adapter: name });
|
|
66
77
|
if (row.recordRefusal !== undefined) {
|
|
67
78
|
notices.push(deployment.record === undefined
|
|
68
|
-
? `Deployment ${
|
|
79
|
+
? `Deployment ${deployment.label} (${environment}) is listed from what its host knows: ` +
|
|
69
80
|
`${row.recordRefusal}`
|
|
70
|
-
: `Deployment ${
|
|
81
|
+
: `Deployment ${deployment.label} (${environment}) is listed from what its host knows: its ` +
|
|
71
82
|
`stored record is not admitted. ${row.recordRefusal}`);
|
|
72
83
|
}
|
|
73
|
-
|
|
84
|
+
hosted.push(row);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let publications = [];
|
|
88
|
+
let installations = skippedInstallations(input.instances ?? []);
|
|
89
|
+
let registryFailure;
|
|
90
|
+
if (hosted.length > 0) {
|
|
91
|
+
// One signal for every registry read, so that an unreadable index stops the others.
|
|
92
|
+
const reads = new AbortController();
|
|
93
|
+
const interrupt = () => reads.abort(input.signal.reason);
|
|
94
|
+
if (input.signal.aborted)
|
|
95
|
+
interrupt();
|
|
96
|
+
else
|
|
97
|
+
input.signal.addEventListener('abort', interrupt, { once: true });
|
|
98
|
+
try {
|
|
99
|
+
const registry = await input.registry(reads.signal);
|
|
100
|
+
// The index and every `--instance` are read together; the latter fail only when the reads
|
|
101
|
+
// are stopped, which is handled where they are awaited below.
|
|
102
|
+
const read = readInstallations(registry, input.origin, input.instances ?? [], reads.signal);
|
|
103
|
+
read.catch(() => undefined);
|
|
104
|
+
try {
|
|
105
|
+
publications = (await registry.index(input.origin))?.publications ?? [];
|
|
106
|
+
}
|
|
107
|
+
catch (cause) {
|
|
108
|
+
// Nothing is printed without the versions: stop the CLI processes still asking instances
|
|
109
|
+
// what they run, and wait only for them to end.
|
|
110
|
+
reads.abort(cause);
|
|
111
|
+
await read.catch(() => undefined);
|
|
112
|
+
throw cause;
|
|
113
|
+
}
|
|
114
|
+
installations = await read;
|
|
115
|
+
}
|
|
116
|
+
catch (cause) {
|
|
117
|
+
registryFailure = cause instanceof Error ? cause : new Error(String(cause));
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
input.signal.removeEventListener('abort', interrupt);
|
|
74
121
|
}
|
|
75
122
|
}
|
|
123
|
+
// INSTALLED ON is reported only for rows the listing prints.
|
|
124
|
+
if (hosted.length > 0 && registryFailure === undefined) {
|
|
125
|
+
notices.push(...installationNotices(installations.sources));
|
|
126
|
+
}
|
|
76
127
|
return Object.freeze({
|
|
77
128
|
result: Object.freeze({
|
|
78
129
|
format: 'astrale.deployment-list',
|
|
79
130
|
version: 1,
|
|
80
131
|
origin: input.origin,
|
|
81
|
-
deployments: Object.freeze(
|
|
132
|
+
deployments: Object.freeze(hosted.map((row) => listedDeployment(row, publications, installations))),
|
|
82
133
|
unlisted: Object.freeze(unlisted),
|
|
134
|
+
installations: installations.sources,
|
|
83
135
|
}),
|
|
84
136
|
notices: Object.freeze(notices),
|
|
85
137
|
failures: Object.freeze(failures),
|
|
138
|
+
...(registryFailure === undefined ? {} : { registryFailure }),
|
|
86
139
|
});
|
|
87
140
|
}
|
|
88
141
|
/**
|
|
89
|
-
* One listed deployment
|
|
90
|
-
* origin and Environment
|
|
91
|
-
* listed Environment, with the commit of the stored record only when that record names this
|
|
92
|
-
* deployment, origin and Environment, as `deploymentName` reads a commit.
|
|
142
|
+
* One listed deployment's record, admitted when it describes exactly this deployment of this
|
|
143
|
+
* origin and Environment, else refused with why, or none when its host keeps none (`NO_RECORD`).
|
|
93
144
|
*/
|
|
94
|
-
|
|
95
|
-
let record = null;
|
|
96
|
-
let recordRefusal;
|
|
145
|
+
function hostedDeployment(deployment, listing) {
|
|
97
146
|
if (deployment.record === undefined) {
|
|
98
|
-
recordRefusal
|
|
147
|
+
return { deployment, listing, record: null, recordRefusal: NO_RECORD };
|
|
99
148
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
catch (cause) {
|
|
108
|
-
recordRefusal = cause instanceof Error ? cause.message : String(cause);
|
|
109
|
-
}
|
|
149
|
+
try {
|
|
150
|
+
const admitted = acceptDeploymentRecord(deployment.record);
|
|
151
|
+
const recordRefusal = describesAnother(admitted, deployment.label, listing);
|
|
152
|
+
return recordRefusal === undefined
|
|
153
|
+
? { deployment, listing, record: admitted }
|
|
154
|
+
: { deployment, listing, record: null, recordRefusal };
|
|
110
155
|
}
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
catch (cause) {
|
|
157
|
+
return {
|
|
158
|
+
deployment,
|
|
159
|
+
listing,
|
|
160
|
+
record: null,
|
|
161
|
+
recordRefusal: cause instanceof Error ? cause.message : String(cause),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* One listed deployment, named and placed. With an admitted record, its name comes from the
|
|
167
|
+
* registry's Publications of the origin that name its release, else its build, else from its
|
|
168
|
+
* record (CT28). Otherwise it is named in the listed Environment, with the commit of the stored
|
|
169
|
+
* record only when that record names this deployment, origin and Environment, and never by a
|
|
170
|
+
* version: a record the listing does not admit gives no digest to name.
|
|
171
|
+
*/
|
|
172
|
+
function listedDeployment(hosted, publications, installations) {
|
|
173
|
+
const { deployment, listing, record, recordRefusal } = hosted;
|
|
174
|
+
// A build digest names the row from any Publication of the origin, whatever its URL: the row is
|
|
175
|
+
// a deployment its own host lists for the caller, and the build of a version deployed elsewhere
|
|
176
|
+
// is another deployment at another URL (`1.5.0 · staging`, tech [.83533]). An instance's pin,
|
|
177
|
+
// which a frozen Worker's tenant may serve, is named only within its issuer (AM-73, AM-224).
|
|
178
|
+
const name = record === null
|
|
179
|
+
? deploymentName(canonicalParts(deployment.record, deployment.label, listing), [])
|
|
180
|
+
: deploymentName(record, publications);
|
|
113
181
|
return Object.freeze({
|
|
114
182
|
environment: listing.environment,
|
|
115
183
|
adapter: listing.adapter,
|
|
116
184
|
name,
|
|
185
|
+
installedOn: installedOn(deployment.url, installations),
|
|
117
186
|
id: deployment.id,
|
|
118
187
|
url: deployment.url,
|
|
119
188
|
label: deployment.label,
|
|
@@ -224,16 +293,25 @@ function members(input) {
|
|
|
224
293
|
? input
|
|
225
294
|
: undefined;
|
|
226
295
|
}
|
|
227
|
-
const COLUMNS = [
|
|
296
|
+
const COLUMNS = [
|
|
297
|
+
'RELEASE',
|
|
298
|
+
'VERSION',
|
|
299
|
+
'ENV',
|
|
300
|
+
'STATE',
|
|
301
|
+
'INSTALLED ON',
|
|
302
|
+
'LAST CALL',
|
|
303
|
+
'EXPIRES',
|
|
304
|
+
'URL',
|
|
305
|
+
];
|
|
228
306
|
const MINUTE = 60_000;
|
|
229
307
|
const HOUR = 60 * MINUTE;
|
|
230
308
|
const DAY = 24 * HOUR;
|
|
231
309
|
/**
|
|
232
310
|
* The listing as a table for a person, at `now`: each deployment's release, its name as computed
|
|
233
|
-
* (never shortened, AM-57), its Environment and state,
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* was not.
|
|
311
|
+
* (never shortened, AM-57), its Environment and state, the instances known to pin it (`?` when a
|
|
312
|
+
* source could not tell, AM-198), its last call and its expiry, and the URL `astrale domain
|
|
313
|
+
* install` takes. Without deployments, it says so only for what was listed: nothing when a host
|
|
314
|
+
* `failed` (stderr says which), and that some Environments were not listed when any was not.
|
|
237
315
|
*/
|
|
238
316
|
export function renderDeploymentList(result, now, failed = false) {
|
|
239
317
|
if (result.deployments.length === 0) {
|
|
@@ -251,6 +329,7 @@ export function renderDeploymentList(result, now, failed = false) {
|
|
|
251
329
|
deployment.name,
|
|
252
330
|
deployment.environment,
|
|
253
331
|
deployment.state,
|
|
332
|
+
installedOnCell(deployment.installedOn, result.installations),
|
|
254
333
|
deployment.lastCallAt === undefined
|
|
255
334
|
? '—'
|
|
256
335
|
: `${elapsed(now - Date.parse(deployment.lastCallAt))} ago`,
|
|
@@ -16,7 +16,7 @@ import { listDeployments, renderDeploymentList } from './list.js';
|
|
|
16
16
|
import { error, info, progress, warn } from './log.js';
|
|
17
17
|
import { importModule, loadProject, runtimeEntrypoint, selectEnvironment, } from './project.js';
|
|
18
18
|
import { runPublish } from './publish/index.js';
|
|
19
|
-
import { RegistryCliError } from './registry/index.js';
|
|
19
|
+
import { MINIMUM_INSTALLATIONS_CLI_VERSION, openDomainRegistry, RegistryCliError, } from './registry/index.js';
|
|
20
20
|
import { executeTests } from './test.js';
|
|
21
21
|
import { yankPublication, yankSummary } from './yank.js';
|
|
22
22
|
const CONFIG_NAMES = ['astrale.config.ts', 'astrale.config.js', 'astrale.config.mjs'];
|
|
@@ -68,7 +68,7 @@ export async function run(argv) {
|
|
|
68
68
|
}
|
|
69
69
|
const project = await loadProject(configPath);
|
|
70
70
|
if (parsed.command === 'list')
|
|
71
|
-
return list(parsed, project);
|
|
71
|
+
return list(parsed, project, projectDir);
|
|
72
72
|
if (parsed.command === 'yank')
|
|
73
73
|
return yank(parsed, project);
|
|
74
74
|
if (parsed.command === 'dev') {
|
|
@@ -235,10 +235,14 @@ export async function run(argv) {
|
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
237
237
|
* `astrale-domain list`: the deployments of every Environment, or of the one `--environment`
|
|
238
|
-
* names, as their hosts keep them for the caller
|
|
239
|
-
*
|
|
238
|
+
* names, as their hosts keep them for the caller, named by the registry's versions and placed on
|
|
239
|
+
* the instances each `--instance` says pin them. With `--json`, stdout
|
|
240
|
+
* carries only the ListResultV1, and only when every host and the registry answered; notices go to
|
|
241
|
+
* stderr. A registry that cannot be read (no Astrale CLI, one older than the listing's floor, Admin
|
|
242
|
+
* out of reach, an unreadable index) prints nothing on stdout and exits 1: versions are never
|
|
243
|
+
* dropped silently (AM-224).
|
|
240
244
|
*/
|
|
241
|
-
async function list(parsed, project) {
|
|
245
|
+
async function list(parsed, project, projectDir) {
|
|
242
246
|
const origin = compile(project.domain).schema.compiled.root.origin;
|
|
243
247
|
const environments = parsed.env === ''
|
|
244
248
|
? Object.entries(project.environments)
|
|
@@ -250,6 +254,13 @@ async function list(parsed, project) {
|
|
|
250
254
|
environments,
|
|
251
255
|
...(parsed.identity === undefined ? {} : { identity: parsed.identity }),
|
|
252
256
|
signal: lifecycle.signal,
|
|
257
|
+
registry: (signal) => openDomainRegistry({
|
|
258
|
+
projectDir,
|
|
259
|
+
...(parsed.identity === undefined ? {} : { identity: parsed.identity }),
|
|
260
|
+
signal,
|
|
261
|
+
minimum: MINIMUM_INSTALLATIONS_CLI_VERSION,
|
|
262
|
+
}),
|
|
263
|
+
...(parsed.instances === undefined ? {} : { instances: parsed.instances }),
|
|
253
264
|
});
|
|
254
265
|
for (const notice of listing.notices)
|
|
255
266
|
warn(notice);
|
|
@@ -257,6 +268,13 @@ async function list(parsed, project) {
|
|
|
257
268
|
error(`Environment ${failure.environment} could not be listed (${failure.adapter}): ` +
|
|
258
269
|
failure.error.message);
|
|
259
270
|
}
|
|
271
|
+
if (listing.registryFailure !== undefined) {
|
|
272
|
+
const failure = listing.registryFailure;
|
|
273
|
+
error(failure instanceof RegistryCliError
|
|
274
|
+
? `Domain registry: ${failure.code}: ${failure.message}`
|
|
275
|
+
: `Domain registry: ${failure.message}`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
260
278
|
if (parsed.json === true) {
|
|
261
279
|
if (listing.failures.length > 0)
|
|
262
280
|
return 1;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Version } from '../../../platform/versioning/index.js';
|
|
2
2
|
import type { RegistryExecutableDependencies } from './executable.js';
|
|
3
|
+
import type { InstanceListing } from './installations.js';
|
|
3
4
|
import type { RegistryProcessInput, RegistryProcessResult } from './process.js';
|
|
5
|
+
export { RegistryCliError } from './error.js';
|
|
4
6
|
/** A `sha256:` digest as the registry stores it. */
|
|
5
7
|
export type RegistryDigest = `sha256:${string}`;
|
|
6
8
|
/**
|
|
@@ -77,17 +79,6 @@ export interface RegistryYank {
|
|
|
77
79
|
readonly yanked: boolean;
|
|
78
80
|
};
|
|
79
81
|
}
|
|
80
|
-
/**
|
|
81
|
-
* One refusal the Astrale CLI printed as its CT29 `{ error: { code, message, details? } }`, or a
|
|
82
|
-
* registry command this SDK cannot read: `REGISTRY_CLI_FAILED` when the command printed no
|
|
83
|
-
* registry document or did not finish, `REGISTRY_CLI_PROTOCOL` when its document is not the
|
|
84
|
-
* requested answer.
|
|
85
|
-
*/
|
|
86
|
-
export declare class RegistryCliError extends Error {
|
|
87
|
-
readonly code: string;
|
|
88
|
-
readonly details?: Readonly<Record<string, unknown>>;
|
|
89
|
-
constructor(code: string, message: string, details?: Readonly<Record<string, unknown>>);
|
|
90
|
-
}
|
|
91
82
|
/** The Admin registry, read and changed through the Astrale CLI with the caller's own credential. */
|
|
92
83
|
export interface DomainRegistry {
|
|
93
84
|
/** The Domain's index, or `undefined` when it is absent or not readable by the caller. */
|
|
@@ -107,11 +98,18 @@ export interface DomainRegistry {
|
|
|
107
98
|
yank(origin: string, version: Version, options: {
|
|
108
99
|
readonly undo: boolean;
|
|
109
100
|
}): Promise<RegistryYank>;
|
|
101
|
+
/**
|
|
102
|
+
* What one instance the CLI knows by that name runs, as its Kernel lists it for the caller
|
|
103
|
+
* (`astrale domain list -i <instance> --json`, CT24), or why that is unknown. Never throws for a
|
|
104
|
+
* refusal of that instance: an instance that cannot tell is a partial answer (AM-198).
|
|
105
|
+
*/
|
|
106
|
+
instance(name: string): Promise<InstanceListing>;
|
|
110
107
|
}
|
|
111
108
|
export interface DomainRegistryDependencies {
|
|
112
109
|
readonly resolve: (input: {
|
|
113
110
|
readonly projectDir: string;
|
|
114
111
|
readonly signal: AbortSignal;
|
|
112
|
+
readonly minimum?: string;
|
|
115
113
|
}, dependencies?: RegistryExecutableDependencies) => Promise<string>;
|
|
116
114
|
readonly run: (input: RegistryProcessInput) => Promise<RegistryProcessResult>;
|
|
117
115
|
}
|
|
@@ -124,4 +122,6 @@ export declare function openDomainRegistry(input: {
|
|
|
124
122
|
readonly projectDir: string;
|
|
125
123
|
readonly identity?: string;
|
|
126
124
|
readonly signal: AbortSignal;
|
|
125
|
+
/** The first CLI release serving every read the command makes (default: the registry). */
|
|
126
|
+
readonly minimum?: string;
|
|
127
127
|
}, dependencies?: DomainRegistryDependencies): Promise<DomainRegistry>;
|
|
@@ -3,27 +3,15 @@ import { BUNDLE_MEDIA_TYPE } from '@astrale-os/kernel-protocol/release/bundle';
|
|
|
3
3
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { RegistryCliError } from './error.js';
|
|
6
7
|
import { resolveRegistryExecutable } from './executable.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* registry command this SDK cannot read: `REGISTRY_CLI_FAILED` when the command printed no
|
|
11
|
-
* registry document or did not finish, `REGISTRY_CLI_PROTOCOL` when its document is not the
|
|
12
|
-
* requested answer.
|
|
13
|
-
*/
|
|
14
|
-
export class RegistryCliError extends Error {
|
|
15
|
-
code;
|
|
16
|
-
details;
|
|
17
|
-
constructor(code, message, details) {
|
|
18
|
-
super(message);
|
|
19
|
-
this.name = 'RegistryCliError';
|
|
20
|
-
this.code = code;
|
|
21
|
-
if (details !== undefined)
|
|
22
|
-
this.details = details;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
8
|
+
import { decodeInstanceListing, instanceRefusal, instanceTimeout, instanceUnreadable, } from './installations.js';
|
|
9
|
+
import { RegistryProcessTimeoutError, runRegistryProcess } from './process.js';
|
|
10
|
+
export { RegistryCliError } from './error.js';
|
|
25
11
|
const INDEX_LIMIT = 8 * 1024 * 1024;
|
|
26
12
|
const DOCUMENT_LIMIT = 256 * 1024;
|
|
13
|
+
/** The tail of an instance listing's stderr that is kept: its machine error is printed last. */
|
|
14
|
+
const MACHINE_ERROR_LIMIT = 16 * 1024;
|
|
27
15
|
/** Admin bounds a stored bundle to 16 MiB; the download writes it to a file, not to stdout. */
|
|
28
16
|
const BUNDLE_TIMEOUT_MS = 120_000;
|
|
29
17
|
const INDEX_TIMEOUT_MS = 60_000;
|
|
@@ -33,6 +21,8 @@ const CHANGE_TIMEOUT_MS = 120_000;
|
|
|
33
21
|
const PUBLISH_TIMEOUT_MS = 180_000;
|
|
34
22
|
/** CT29 bounds the publish request the CLI reads on stdin to 64 KiB. */
|
|
35
23
|
const REQUEST_LIMIT = 64 * 1024;
|
|
24
|
+
/** One instance Kernel, then the registry and the deployment records it names (CT24). */
|
|
25
|
+
const INSTANCE_TIMEOUT_MS = 60_000;
|
|
36
26
|
/**
|
|
37
27
|
* Open the Domain registry through one admitted Astrale CLI (CT29 plumbing); the CLI's configured
|
|
38
28
|
* Admin target answers. Reads and publish run under `--as` when the caller names an identity, a
|
|
@@ -45,6 +35,7 @@ export async function openDomainRegistry(input, dependencies = {
|
|
|
45
35
|
const executable = await dependencies.resolve({
|
|
46
36
|
projectDir: input.projectDir,
|
|
47
37
|
signal: input.signal,
|
|
38
|
+
...(input.minimum === undefined ? {} : { minimum: input.minimum }),
|
|
48
39
|
});
|
|
49
40
|
const authority = input.identity === undefined ? [] : ['--as', input.identity];
|
|
50
41
|
// A read or a publish runs in machine mode without prompts: a document or a refusal on stdout,
|
|
@@ -152,6 +143,38 @@ export async function openDomainRegistry(input, dependencies = {
|
|
|
152
143
|
],
|
|
153
144
|
}, { stdout: DOCUMENT_LIMIT, timeoutMs: CHANGE_TIMEOUT_MS, change: true }), { reference, version, undo: options.undo });
|
|
154
145
|
},
|
|
146
|
+
async instance(name) {
|
|
147
|
+
let result;
|
|
148
|
+
try {
|
|
149
|
+
result = await dependencies.run({
|
|
150
|
+
executable,
|
|
151
|
+
// `domain list -i` prints its refusal as a machine error on stderr, not as a document on
|
|
152
|
+
// stdout: only that error's code is read, never relayed.
|
|
153
|
+
args: ['--ci', '--no-prompt', 'domain', 'list', '-i', name, '--json', ...authority],
|
|
154
|
+
projectDir: input.projectDir,
|
|
155
|
+
signal: input.signal,
|
|
156
|
+
timeoutMs: INSTANCE_TIMEOUT_MS,
|
|
157
|
+
stdoutLimit: INDEX_LIMIT,
|
|
158
|
+
stderrLimit: MACHINE_ERROR_LIMIT,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (cause) {
|
|
162
|
+
if (cause instanceof RegistryProcessTimeoutError)
|
|
163
|
+
return instanceTimeout();
|
|
164
|
+
throw cause;
|
|
165
|
+
}
|
|
166
|
+
if (result.code !== 0)
|
|
167
|
+
return instanceRefusal(result.stderr ?? '');
|
|
168
|
+
const document = parseDocument(result.stdout);
|
|
169
|
+
if (document === undefined)
|
|
170
|
+
return instanceUnreadable();
|
|
171
|
+
try {
|
|
172
|
+
return decodeInstanceListing(document);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return instanceUnreadable();
|
|
176
|
+
}
|
|
177
|
+
},
|
|
155
178
|
});
|
|
156
179
|
}
|
|
157
180
|
function parseDocument(stdout) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One refusal the Astrale CLI printed as its CT29 `{ error: { code, message, details? } }`, or a
|
|
3
|
+
* registry command this SDK cannot read: `REGISTRY_CLI_FAILED` when the command printed no
|
|
4
|
+
* registry document or did not finish, `REGISTRY_CLI_PROTOCOL` when its document is not the
|
|
5
|
+
* requested answer.
|
|
6
|
+
*/
|
|
7
|
+
export declare class RegistryCliError extends Error {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
10
|
+
constructor(code: string, message: string, details?: Readonly<Record<string, unknown>>);
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One refusal the Astrale CLI printed as its CT29 `{ error: { code, message, details? } }`, or a
|
|
3
|
+
* registry command this SDK cannot read: `REGISTRY_CLI_FAILED` when the command printed no
|
|
4
|
+
* registry document or did not finish, `REGISTRY_CLI_PROTOCOL` when its document is not the
|
|
5
|
+
* requested answer.
|
|
6
|
+
*/
|
|
7
|
+
export class RegistryCliError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
details;
|
|
10
|
+
constructor(code, message, details) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'RegistryCliError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
if (details !== undefined)
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -5,6 +5,13 @@ import type { RegistryProcessInput, RegistryProcessResult } from './process.js';
|
|
|
5
5
|
* 1.0.0-beta.127 and older were released without them.
|
|
6
6
|
*/
|
|
7
7
|
export declare const MINIMUM_REGISTRY_CLI_VERSION = "1.0.0-beta.128";
|
|
8
|
+
/**
|
|
9
|
+
* The first Astrale CLI release that serves what `astrale-domain list` reads beside the registry
|
|
10
|
+
* index: the instance listing (`astrale domain list -i <instance> --json`, CT24). It shipped after
|
|
11
|
+
* the registry plumbing, so this floor is named on its own and is never below the registry's;
|
|
12
|
+
* 1.0.0-beta.128 and older were released without it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const MINIMUM_INSTALLATIONS_CLI_VERSION = "1.0.0-beta.129";
|
|
8
15
|
export interface RegistryExecutableDependencies {
|
|
9
16
|
readonly candidates: () => Promise<readonly string[]>;
|
|
10
17
|
readonly run: (input: RegistryProcessInput) => Promise<RegistryProcessResult>;
|
|
@@ -17,4 +24,6 @@ export interface RegistryExecutableDependencies {
|
|
|
17
24
|
export declare function resolveRegistryExecutable(input: {
|
|
18
25
|
readonly projectDir: string;
|
|
19
26
|
readonly signal: AbortSignal;
|
|
27
|
+
/** The first CLI release serving what the command reads, never below the registry floor. */
|
|
28
|
+
readonly minimum?: string;
|
|
20
29
|
}, dependencies?: RegistryExecutableDependencies): Promise<string>;
|
|
@@ -10,6 +10,13 @@ import { runRegistryProcess } from './process.js';
|
|
|
10
10
|
* 1.0.0-beta.127 and older were released without them.
|
|
11
11
|
*/
|
|
12
12
|
export const MINIMUM_REGISTRY_CLI_VERSION = '1.0.0-beta.128';
|
|
13
|
+
/**
|
|
14
|
+
* The first Astrale CLI release that serves what `astrale-domain list` reads beside the registry
|
|
15
|
+
* index: the instance listing (`astrale domain list -i <instance> --json`, CT24). It shipped after
|
|
16
|
+
* the registry plumbing, so this floor is named on its own and is never below the registry's;
|
|
17
|
+
* 1.0.0-beta.128 and older were released without it.
|
|
18
|
+
*/
|
|
19
|
+
export const MINIMUM_INSTALLATIONS_CLI_VERSION = '1.0.0-beta.129';
|
|
13
20
|
/**
|
|
14
21
|
* Resolve one Astrale CLI for a whole command, the official installation before PATH, and admit
|
|
15
22
|
* it only when it serves the registry plumbing, so an older CLI is refused by name instead of
|
|
@@ -29,11 +36,15 @@ export async function resolveRegistryExecutable(input, dependencies = {
|
|
|
29
36
|
stdoutLimit: 4 * 1024,
|
|
30
37
|
});
|
|
31
38
|
const version = result.stdout.trim().replace(/^v/u, '');
|
|
39
|
+
// Never below the registry plumbing every caller of this executable reads.
|
|
40
|
+
const minimum = input.minimum === undefined || semver.lt(input.minimum, MINIMUM_REGISTRY_CLI_VERSION)
|
|
41
|
+
? MINIMUM_REGISTRY_CLI_VERSION
|
|
42
|
+
: input.minimum;
|
|
32
43
|
if (result.code !== 0 || semver.valid(version) === null) {
|
|
33
|
-
throw new TypeError(`Astrale CLI did not report its version; ${
|
|
44
|
+
throw new TypeError(`Astrale CLI did not report its version; ${minimum} or newer is required to read the Domain registry.`);
|
|
34
45
|
}
|
|
35
|
-
if (semver.lt(version,
|
|
36
|
-
throw new TypeError(`Astrale CLI ${version} cannot read the Domain registry; ${
|
|
46
|
+
if (semver.lt(version, minimum)) {
|
|
47
|
+
throw new TypeError(`Astrale CLI ${version} cannot read the Domain registry; ${minimum} or newer is required (astrale update).`);
|
|
37
48
|
}
|
|
38
49
|
return executable;
|
|
39
50
|
}
|
|
@@ -3,4 +3,7 @@
|
|
|
3
3
|
* registry plumbing (CT29), with the caller's own credential and the CLI's configured Admin target.
|
|
4
4
|
*/
|
|
5
5
|
export { RegistryCliError, openDomainRegistry, type DomainRegistry, type DomainRegistryDependencies, type RegistryBundle, type RegistryDigest, type RegistryIndex, type RegistryPublication, type RegistryPublishRequest, type RegistryPublishResult, type RegistryPublishRetention, type RegistryYank, } from './client.js';
|
|
6
|
-
export { MINIMUM_REGISTRY_CLI_VERSION, resolveRegistryExecutable } from './executable.js';
|
|
6
|
+
export { MINIMUM_INSTALLATIONS_CLI_VERSION, MINIMUM_REGISTRY_CLI_VERSION, resolveRegistryExecutable, } from './executable.js';
|
|
7
|
+
export { isAdminFailure } from './installations.js';
|
|
8
|
+
export type { InstallationUnknownReason, InstanceInstallation, InstanceListing, } from './installations.js';
|
|
9
|
+
export { RegistryProcessTimeoutError } from './process.js';
|
|
@@ -3,4 +3,6 @@
|
|
|
3
3
|
* registry plumbing (CT29), with the caller's own credential and the CLI's configured Admin target.
|
|
4
4
|
*/
|
|
5
5
|
export { RegistryCliError, openDomainRegistry, } from './client.js';
|
|
6
|
-
export { MINIMUM_REGISTRY_CLI_VERSION, resolveRegistryExecutable } from './executable.js';
|
|
6
|
+
export { MINIMUM_INSTALLATIONS_CLI_VERSION, MINIMUM_REGISTRY_CLI_VERSION, resolveRegistryExecutable, } from './executable.js';
|
|
7
|
+
export { isAdminFailure } from './installations.js';
|
|
8
|
+
export { RegistryProcessTimeoutError } from './process.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the installations of one instance are unknown, as its listing's refusal says (CT24, AM-225):
|
|
3
|
+
* an unknown instance is a partial answer, never "not installed" (AM-198).
|
|
4
|
+
* - `timeout`: it did not answer in time;
|
|
5
|
+
* - `refused`: its Kernel refused the read;
|
|
6
|
+
* - `unavailable`: it could not be reached, or answered nothing readable;
|
|
7
|
+
* - `unsupported`: its Kernel does not list installed releases (before K14, such as the 1Pact Host
|
|
8
|
+
* on beta.117).
|
|
9
|
+
*/
|
|
10
|
+
export type InstallationUnknownReason = 'timeout' | 'refused' | 'unavailable' | 'unsupported';
|
|
11
|
+
/** One installation the instance Kernel lists, as `astrale domain list -i --json` prints it. */
|
|
12
|
+
export interface InstanceInstallation {
|
|
13
|
+
readonly origin: string;
|
|
14
|
+
/** The deployment URL the Kernel lists the pin under. */
|
|
15
|
+
readonly url: string;
|
|
16
|
+
}
|
|
17
|
+
/** What one instance answered to `astrale domain list -i <instance> --json` (CT24). */
|
|
18
|
+
export type InstanceListing = {
|
|
19
|
+
readonly kind: 'listed';
|
|
20
|
+
/** The instance Kernel URL that answered. */
|
|
21
|
+
readonly kernel: string;
|
|
22
|
+
/**
|
|
23
|
+
* The listing's own `partial`: true when an origin it does not list may still run there. A
|
|
24
|
+
* Kernel lists only the installations the caller reads in its Domain directory and says
|
|
25
|
+
* nothing of the rest (AM-83, AM-84), so C7 always answers true.
|
|
26
|
+
*/
|
|
27
|
+
readonly partial: boolean;
|
|
28
|
+
readonly installations: readonly InstanceInstallation[];
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'unknown';
|
|
31
|
+
readonly reason: InstallationUnknownReason;
|
|
32
|
+
/** The CLI's error code, or the SDK's own when the CLI answered nothing readable. */
|
|
33
|
+
readonly code: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Decode one `astrale.installed-list` v1 document: the instance Kernel that answered, whether the
|
|
37
|
+
* listing may omit installations (`partial`, required) and the deployment URL of each
|
|
38
|
+
* installation. Anything else is refused.
|
|
39
|
+
*/
|
|
40
|
+
export declare function decodeInstanceListing(document: Readonly<Record<string, unknown>>): Extract<InstanceListing, {
|
|
41
|
+
readonly kind: 'listed';
|
|
42
|
+
}>;
|
|
43
|
+
/**
|
|
44
|
+
* Why an instance listing failed, from the machine error the CLI printed on stderr (the last line
|
|
45
|
+
* that is a JSON object with an `error` code): only that code is read, never its message, since
|
|
46
|
+
* stderr may name the caller's credential. A Kernel without the listing is `unsupported`; a timeout
|
|
47
|
+
* at any phase is `timeout`; a Kernel refusal is `refused`; anything else, a command that printed
|
|
48
|
+
* no machine error and a failed read of Admin's registry (`isAdminFailure`) included, is
|
|
49
|
+
* `unavailable`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function instanceRefusal(stderr: string): Extract<InstanceListing, {
|
|
52
|
+
readonly kind: 'unknown';
|
|
53
|
+
}>;
|
|
54
|
+
/**
|
|
55
|
+
* Whether an instance listing failed on Admin's side, not the instance's: the listing also reads
|
|
56
|
+
* Admin's registry to name what runs there, and fails as a whole when that read fails (CT24, C7).
|
|
57
|
+
* Its codes are Admin's (`REGISTRY_*`, `ADMIN_*`), never the SDK's own `REGISTRY_CLI_*`.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isAdminFailure(code: string): boolean;
|
|
60
|
+
/** The instance did not answer within the probe's time. */
|
|
61
|
+
export declare function instanceTimeout(): Extract<InstanceListing, {
|
|
62
|
+
readonly kind: 'unknown';
|
|
63
|
+
}>;
|
|
64
|
+
/** The CLI answered the instance listing with something this SDK cannot read. */
|
|
65
|
+
export declare function instanceUnreadable(): Extract<InstanceListing, {
|
|
66
|
+
readonly kind: 'unknown';
|
|
67
|
+
}>;
|