@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.
@@ -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 deployments = [];
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 = listedDeployment(deployment, { origin: input.origin, environment, adapter: name });
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 ${row.label} (${environment}) is listed from what its host knows: ` +
79
+ ? `Deployment ${deployment.label} (${environment}) is listed from what its host knows: ` +
69
80
  `${row.recordRefusal}`
70
- : `Deployment ${row.label} (${environment}) is listed from what its host knows: its ` +
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
- deployments.push(row);
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(deployments),
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, its record admitted when it describes exactly this deployment of this
90
- * origin and Environment. Its name comes from the admitted record; otherwise it is named in the
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
- export function listedDeployment(deployment, listing) {
95
- let record = null;
96
- let recordRefusal;
145
+ function hostedDeployment(deployment, listing) {
97
146
  if (deployment.record === undefined) {
98
- recordRefusal = NO_RECORD;
147
+ return { deployment, listing, record: null, recordRefusal: NO_RECORD };
99
148
  }
100
- else {
101
- try {
102
- const admitted = acceptDeploymentRecord(deployment.record);
103
- recordRefusal = describesAnother(admitted, deployment.label, listing);
104
- if (recordRefusal === undefined)
105
- record = admitted;
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
- // No Publication names a deployment yet: the registry is read by `list` once versions exist.
112
- const name = deploymentName(record ?? canonicalParts(deployment.record, deployment.label, listing), []);
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 = ['RELEASE', 'VERSION', 'ENV', 'STATE', 'LAST CALL', 'EXPIRES', 'URL'];
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, its last call and its expiry, and the URL
234
- * `astrale domain install` takes. Without deployments, it says so only for what was listed: nothing
235
- * when a host `failed` (stderr says which), and that some Environments were not listed when any
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. With `--json`, stdout carries only the
239
- * ListResultV1, and only when every host answered; notices go to stderr.
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 { runRegistryProcess } from './process.js';
8
- /**
9
- * One refusal the Astrale CLI printed as its CT29 `{ error: { code, message, details? } }`, or a
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; ${MINIMUM_REGISTRY_CLI_VERSION} or newer is required to read the Domain registry.`);
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, MINIMUM_REGISTRY_CLI_VERSION)) {
36
- throw new TypeError(`Astrale CLI ${version} cannot read the Domain registry; ${MINIMUM_REGISTRY_CLI_VERSION} or newer is required (astrale update).`);
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
+ }>;