@astrale-os/sdk 0.6.0-beta.21 → 0.6.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.
Files changed (58) hide show
  1. package/README.md +0 -4
  2. package/dist/application/workflow/durable.d.ts +12 -8
  3. package/dist/deployment/adapter/adapter.d.ts +16 -5
  4. package/dist/deployment/adapter/define.d.ts +1 -1
  5. package/dist/deployment/adapter/define.js +40 -1
  6. package/dist/deployment/adapter/release/command.d.ts +33 -0
  7. package/dist/deployment/adapter/release/command.js +1 -0
  8. package/dist/deployment/adapter/release/context.d.ts +3 -4
  9. package/dist/deployment/adapter/release/index.d.ts +1 -0
  10. package/dist/deployment/adapter/release/list.d.ts +11 -10
  11. package/dist/deployment/address/configuration.d.ts +12 -0
  12. package/dist/deployment/address/configuration.js +11 -0
  13. package/dist/deployment/address/index.d.ts +3 -3
  14. package/dist/deployment/address/index.js +2 -3
  15. package/dist/deployment/address/label.d.ts +5 -5
  16. package/dist/deployment/address/label.js +5 -5
  17. package/dist/deployment/address/record.d.ts +21 -0
  18. package/dist/deployment/address/record.js +34 -0
  19. package/dist/deployment/address/summary.d.ts +17 -40
  20. package/dist/deployment/address/summary.js +1 -117
  21. package/dist/deployment/index.d.ts +1 -1
  22. package/dist/deployment/verify/index.d.ts +1 -1
  23. package/dist/deployment/verify/index.js +1 -1
  24. package/dist/deployment/verify/readiness/index.d.ts +7 -0
  25. package/dist/deployment/verify/readiness/index.js +17 -0
  26. package/dist/deployment/worker/address.d.ts +26 -0
  27. package/dist/deployment/worker/address.js +43 -0
  28. package/dist/deployment/worker/build.d.ts +60 -0
  29. package/dist/deployment/worker/build.js +172 -0
  30. package/dist/deployment/worker/configuration.d.ts +73 -0
  31. package/dist/deployment/worker/configuration.js +165 -0
  32. package/dist/deployment/worker/content.d.ts +38 -0
  33. package/dist/deployment/worker/content.js +169 -0
  34. package/dist/deployment/worker/index.d.ts +29 -0
  35. package/dist/deployment/worker/index.js +29 -0
  36. package/dist/deployment/worker/limits.d.ts +30 -0
  37. package/dist/deployment/worker/limits.js +30 -0
  38. package/dist/deployment/worker/record.d.ts +29 -0
  39. package/dist/deployment/worker/record.js +69 -0
  40. package/dist/deployment/worker/request.d.ts +118 -0
  41. package/dist/deployment/worker/request.js +280 -0
  42. package/dist/deployment/worker/response.d.ts +104 -0
  43. package/dist/deployment/worker/response.js +213 -0
  44. package/dist/execution/workflows/run.js +18 -6
  45. package/dist/tooling/cli/adapter-command.d.ts +35 -0
  46. package/dist/tooling/cli/adapter-command.js +66 -0
  47. package/dist/tooling/cli/arguments.d.ts +7 -1
  48. package/dist/tooling/cli/arguments.js +53 -1
  49. package/dist/tooling/cli/help.js +11 -8
  50. package/dist/tooling/cli/immutable-deployment.d.ts +2 -2
  51. package/dist/tooling/cli/index.d.ts +4 -2
  52. package/dist/tooling/cli/index.js +3 -2
  53. package/dist/tooling/cli/list.d.ts +5 -5
  54. package/dist/tooling/cli/list.js +16 -14
  55. package/dist/tooling/cli/orchestrate.js +18 -0
  56. package/dist/tooling/cli/publish/index.d.ts +4 -4
  57. package/dist/tooling/cli/publish/index.js +8 -9
  58. package/package.json +5 -1
@@ -0,0 +1,213 @@
1
+ import { parseDeploymentLabel } from '../address/label.js';
2
+ import { serviceName } from './address.js';
3
+ import { decodeWorkerRecord } from './record.js';
4
+ export const WORKER_DEPLOYMENT_REJECTIONS = Object.freeze([
5
+ 'invalid-deployment',
6
+ 'address-mismatch',
7
+ 'build-mismatch',
8
+ 'service-owned-elsewhere',
9
+ 'deployment-conflict',
10
+ 'deployment-retired',
11
+ 'hosting-unavailable',
12
+ 'service-unavailable',
13
+ ]);
14
+ const STATES = Object.freeze([
15
+ 'deploying',
16
+ 'active',
17
+ 'retiring',
18
+ 'retired',
19
+ 'failed',
20
+ ]);
21
+ /** Which secret states each deployment state reports. */
22
+ const SECRETS_BY_STATE = Object.freeze({
23
+ created: Object.freeze(['bound']),
24
+ reused: Object.freeze(['updated', 'unchanged']),
25
+ });
26
+ /**
27
+ * Admit the answer of a host's `hosting`: its routing domain. The `addressing: 'opaque'` an older
28
+ * Services answers beside it is admitted and dropped: every Service name is opaque.
29
+ */
30
+ export function decodeWorkerHosting(input) {
31
+ const value = exact(input, ['routingDomain'], ['addressing'], 'hosting');
32
+ if (typeof value.routingDomain !== 'string' || !isDnsName(value.routingDomain)) {
33
+ invalid('hosting', 'routingDomain');
34
+ }
35
+ if (value.addressing !== undefined && value.addressing !== 'opaque') {
36
+ invalid('hosting', 'addressing');
37
+ }
38
+ return Object.freeze({ routingDomain: value.routingDomain });
39
+ }
40
+ /**
41
+ * Admit the answer of a deploy exactly: a deployment whose state reports the secret state it
42
+ * pairs with, or a refusal a host can give.
43
+ */
44
+ export function decodeWorkerDeploymentResponse(input) {
45
+ const value = exact(input, ['kind'], ['state', 'secrets', 'deployment', 'reason', 'message', 'retryAfter'], 'deploy');
46
+ if (value.kind === 'rejected') {
47
+ exact(value, ['kind', 'reason', 'message'], ['retryAfter'], 'refusal');
48
+ if (!WORKER_DEPLOYMENT_REJECTIONS.includes(value.reason)) {
49
+ invalid('refusal', 'reason');
50
+ }
51
+ if (typeof value.message !== 'string' ||
52
+ value.message.length < 1 ||
53
+ value.message.length > 500) {
54
+ invalid('refusal', 'message');
55
+ }
56
+ if (value.retryAfter !== undefined && !isInstant(value.retryAfter)) {
57
+ invalid('refusal', 'retryAfter');
58
+ }
59
+ return Object.freeze({
60
+ kind: 'rejected',
61
+ reason: value.reason,
62
+ message: value.message,
63
+ ...(value.retryAfter === undefined ? {} : { retryAfter: value.retryAfter }),
64
+ });
65
+ }
66
+ exact(value, ['kind', 'state', 'secrets', 'deployment'], [], 'deploy');
67
+ if (value.kind !== 'deployed')
68
+ invalid('deploy', 'kind');
69
+ if (value.state !== 'created' && value.state !== 'reused')
70
+ invalid('deploy', 'state');
71
+ const secrets = exact(value.secrets, ['state'], [], 'secrets');
72
+ const allowed = SECRETS_BY_STATE[value.state];
73
+ if (typeof secrets.state !== 'string' || !allowed.includes(secrets.state)) {
74
+ invalid('deploy', 'secrets.state');
75
+ }
76
+ return Object.freeze({
77
+ kind: 'deployed',
78
+ state: value.state,
79
+ secrets: Object.freeze({ state: secrets.state }),
80
+ deployment: decodeWorkerDeploymentSummary(value.deployment),
81
+ });
82
+ }
83
+ /** Admit the answer of a host's `list`: the summaries of the caller's deployments, in its order. */
84
+ export function decodeWorkerDeploymentList(input) {
85
+ const value = exact(input, ['deployments'], [], 'list');
86
+ if (!Array.isArray(value.deployments))
87
+ invalid('list', 'deployments');
88
+ return Object.freeze(value.deployments.map((deployment) => decodeWorkerDeploymentSummary(deployment)));
89
+ }
90
+ /**
91
+ * Admit one summary exactly: node ids and names a person can read, a label whose first part is
92
+ * the Service's name, the URL of that label, a known state, instants, digests, and a record that
93
+ * decodes.
94
+ */
95
+ export function decodeWorkerDeploymentSummary(input) {
96
+ const value = exact(input, [
97
+ 'id',
98
+ 'url',
99
+ 'label',
100
+ 'serviceId',
101
+ 'service',
102
+ 'project',
103
+ 'environment',
104
+ 'owner',
105
+ 'state',
106
+ 'current',
107
+ 'createdAt',
108
+ 'buildDigest',
109
+ 'configuration',
110
+ ], ['retiredAt', 'signingKeyId', 'record'], 'deployment');
111
+ for (const key of [
112
+ 'id',
113
+ 'url',
114
+ 'label',
115
+ 'serviceId',
116
+ 'service',
117
+ 'project',
118
+ 'environment',
119
+ 'owner',
120
+ ]) {
121
+ if (typeof value[key] !== 'string' || !isPrintable(value[key]))
122
+ invalid('deployment', key);
123
+ }
124
+ if (value.signingKeyId !== undefined) {
125
+ if (typeof value.signingKeyId !== 'string' || !isPrintable(value.signingKeyId)) {
126
+ invalid('deployment', 'signingKeyId');
127
+ }
128
+ }
129
+ let line;
130
+ try {
131
+ line = parseDeploymentLabel(value.label).line;
132
+ }
133
+ catch {
134
+ invalid('deployment', 'label');
135
+ }
136
+ if (value.service !== line)
137
+ invalid('deployment', 'service');
138
+ let named;
139
+ try {
140
+ named = serviceName(value.project, value.environment);
141
+ }
142
+ catch {
143
+ invalid('deployment', 'project');
144
+ }
145
+ if (named !== line)
146
+ invalid('deployment', 'service');
147
+ if (!isDeploymentUrl(value.url, value.label))
148
+ invalid('deployment', 'url');
149
+ if (!STATES.includes(value.state))
150
+ invalid('deployment', 'state');
151
+ if (typeof value.current !== 'boolean')
152
+ invalid('deployment', 'current');
153
+ if (!isInstant(value.createdAt))
154
+ invalid('deployment', 'createdAt');
155
+ if (value.retiredAt !== undefined && !isInstant(value.retiredAt))
156
+ invalid('deployment', 'retiredAt');
157
+ for (const key of ['buildDigest', 'configuration']) {
158
+ if (typeof value[key] !== 'string' || !/^sha256:[0-9a-f]{64}$/u.test(value[key])) {
159
+ invalid('deployment', key);
160
+ }
161
+ }
162
+ let record;
163
+ if (value.record !== undefined) {
164
+ try {
165
+ record = decodeWorkerRecord(value.record);
166
+ }
167
+ catch {
168
+ invalid('deployment', 'record');
169
+ }
170
+ }
171
+ const summary = { ...value };
172
+ delete summary.record;
173
+ return Object.freeze({
174
+ ...summary,
175
+ ...(record === undefined ? {} : { record }),
176
+ });
177
+ }
178
+ /** `https://<label>.<routing domain>`, nothing more. */
179
+ function isDeploymentUrl(url, label) {
180
+ const prefix = `https://${label}.`;
181
+ return url.startsWith(prefix) && isDnsName(url.slice(prefix.length));
182
+ }
183
+ /** Text a person can read in a listing: at most 512 characters, none of them a control character. */
184
+ function isPrintable(value) {
185
+ return value.length > 0 && value.length <= 512 && !/[\p{Cc}]/u.test(value);
186
+ }
187
+ function isInstant(value) {
188
+ return (typeof value === 'string' &&
189
+ value.length > 0 &&
190
+ value.length <= 64 &&
191
+ !Number.isNaN(Date.parse(value)));
192
+ }
193
+ function isDnsName(value) {
194
+ return (value.length <= 253 &&
195
+ value.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label)));
196
+ }
197
+ function exact(input, required, optional, label) {
198
+ if (input === null || typeof input !== 'object' || Array.isArray(input))
199
+ invalid(label);
200
+ const value = input;
201
+ for (const key of Reflect.ownKeys(value)) {
202
+ if (typeof key !== 'string' || (!required.includes(key) && !optional.includes(key))) {
203
+ invalid(label, String(key));
204
+ }
205
+ }
206
+ for (const key of required)
207
+ if (!Object.hasOwn(value, key))
208
+ invalid(label, key);
209
+ return value;
210
+ }
211
+ function invalid(label, member) {
212
+ throw new TypeError(`The Worker host returned an invalid ${label} answer${member === undefined ? '' : ` (${member})`}.`);
213
+ }
@@ -86,7 +86,7 @@ function durableContext(input, context, envelope) {
86
86
  const deadline = Date.now() + serving.stepTimeoutMs;
87
87
  const session = input.executor(execution.kernel, execution.id, { deadline, signal });
88
88
  try {
89
- const kernel = keyed(bindSessionOf(session, deadline, signal), await keySequence(execution.id, name));
89
+ const kernel = keyed(bindSessionOf(session, deadline, signal), domain, await keySequence(execution.id, name));
90
90
  const output = await execute(Object.freeze({
91
91
  kernel,
92
92
  query: bindQuery(kernel, domain),
@@ -171,17 +171,29 @@ function bindSessionOf(session, deadline, signal) {
171
171
  return bindSession(session, { deadline, signal });
172
172
  }
173
173
  /**
174
- * Every `invoke` and Mutation of one Step without its own key carries the next key of the Step's
175
- * sequence. The sequence restarts with every attempt, so a retried Step replays the same keys and
176
- * the Kernel answers with the first outcome, or `IDEMPOTENCY_KEY_CONFLICT` if the request changed.
174
+ * Every Mutation and every `invoke` of a durable callable of one Step without its own key carries
175
+ * the next key of the Step's sequence. The sequence restarts with every attempt, so a retried Step
176
+ * replays the same keys: the Kernel answers a Mutation with its receipt and a durable call with the
177
+ * Execution it started, or refuses a request that changed under its key. Any other `invoke` carries
178
+ * no key, because the Kernel accepts one only for a durable Function, and runs again with the Step.
177
179
  */
178
- function keyed(session, next) {
180
+ function keyed(session, domain, next) {
179
181
  const invokeSource = session.invoke;
180
182
  const mutateSource = session.mutate;
181
- const invoke = ((reference, value, options) => invokeSource(reference, value, withKey(options, next)));
183
+ const invoke = ((reference, value, options) => invokeSource(reference, value, declaredDurable(domain, reference) ? withKey(options, next) : options));
182
184
  const mutate = ((author, options) => mutateSource(author, withKey(options, next)));
183
185
  return Object.freeze({ ...session, invoke, mutate });
184
186
  }
187
+ /**
188
+ * Whether the Domain's Schema closure declares the referenced callable durable. An untyped Path
189
+ * names no declaration and a callable outside the closure has none here: both need their own key.
190
+ */
191
+ function declaredDurable(domain, reference) {
192
+ if (reference === null || typeof reference !== 'object')
193
+ return false;
194
+ const key = reference.callable;
195
+ return (typeof key === 'string' && domain.callable(key)?.execution?.durable === true);
196
+ }
185
197
  function withKey(options, next) {
186
198
  if (options?.idempotencyKey !== undefined || options?.body !== undefined)
187
199
  return options;
@@ -0,0 +1,35 @@
1
+ import type { LoadedEnvironment } from './project.js';
2
+ /**
3
+ * `astrale-domain <adapter> <command> <environment> --json` on stdout: what one provider command
4
+ * of the Environment's adapter answered (adapter-cloudflare: `cloudflare plan` and
5
+ * `cloudflare init`, which provision its host on the operator's own account).
6
+ */
7
+ export interface AdapterCommandReportV1 {
8
+ readonly format: 'astrale.adapter-command';
9
+ readonly version: 1;
10
+ /** `<adapter name>@<adapter version>` */
11
+ readonly adapter: string;
12
+ readonly command: string;
13
+ readonly environment: string;
14
+ /** 0 done; 1 refused or blocked. */
15
+ readonly exitCode: 0 | 1;
16
+ /** What the adapter's command answered, as it answered it. */
17
+ readonly result: unknown;
18
+ }
19
+ /**
20
+ * `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: run one
21
+ * provider command an adapter declares, on an Environment that adapter deploys. The CLI only
22
+ * routes: it refuses an Environment whose adapter has another name, a command the adapter does
23
+ * not declare and an option the command does not declare, each before any effect, and prints what
24
+ * the command answers. Exit status: the command's (0 done, 1 refused or blocked), 1 when it could
25
+ * not run.
26
+ */
27
+ export declare function runAdapterCommand(input: {
28
+ readonly adapter: string;
29
+ readonly command: string;
30
+ readonly environment: string;
31
+ readonly definition: LoadedEnvironment;
32
+ readonly options: Readonly<Record<string, string>>;
33
+ readonly json: boolean;
34
+ readonly signal: AbortSignal;
35
+ }): Promise<number>;
@@ -0,0 +1,66 @@
1
+ import { error, info, warn } from './log.js';
2
+ /**
3
+ * `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: run one
4
+ * provider command an adapter declares, on an Environment that adapter deploys. The CLI only
5
+ * routes: it refuses an Environment whose adapter has another name, a command the adapter does
6
+ * not declare and an option the command does not declare, each before any effect, and prints what
7
+ * the command answers. Exit status: the command's (0 done, 1 refused or blocked), 1 when it could
8
+ * not run.
9
+ */
10
+ export async function runAdapterCommand(input) {
11
+ const { adapter, parameters } = input.definition.deployment;
12
+ const name = `${adapter.name}@${adapter.version}`;
13
+ if (adapter.name !== input.adapter) {
14
+ error(`Environment ${input.environment} deploys through adapter ${adapter.name}, not ` +
15
+ `${input.adapter}: run \`astrale-domain ${adapter.name} <command> ${input.environment}\`.`);
16
+ return 1;
17
+ }
18
+ const commands = 'commands' in adapter ? adapter.commands : undefined;
19
+ const command = commands?.[input.command];
20
+ if (command === undefined) {
21
+ const declared = Object.entries(commands ?? {}).map(([candidate, { summary }]) => `${candidate} (${summary})`);
22
+ error(`Adapter ${name} declares no command ${input.command}` +
23
+ (declared.length === 0 ? '.' : `; its commands: ${declared.join('; ')}.`));
24
+ return 1;
25
+ }
26
+ const declaredOptions = command.options ?? {};
27
+ const unknown = Object.keys(input.options).filter((option) => !Object.hasOwn(declaredOptions, option));
28
+ if (unknown.length > 0) {
29
+ const known = Object.entries(declaredOptions).map(([option, description]) => `--${option} (${description})`);
30
+ error(`${input.adapter} ${input.command} takes no option ${unknown.map((option) => `--${option}`).join(', ')}` +
31
+ (known.length === 0 ? '.' : `; its options: ${known.join('; ')}.`));
32
+ return 1;
33
+ }
34
+ let answered;
35
+ try {
36
+ answered = await command.run(parameters, {
37
+ environment: input.environment,
38
+ signal: input.signal,
39
+ options: Object.freeze({ ...input.options }),
40
+ });
41
+ }
42
+ catch (cause) {
43
+ error(`${input.adapter} ${input.command} ${input.environment} (${name}) failed: ` +
44
+ (cause instanceof Error ? cause.message : String(cause)));
45
+ return 1;
46
+ }
47
+ for (const line of answered.warnings ?? [])
48
+ warn(line);
49
+ if (input.json) {
50
+ const report = Object.freeze({
51
+ format: 'astrale.adapter-command',
52
+ version: 1,
53
+ adapter: name,
54
+ command: input.command,
55
+ environment: input.environment,
56
+ exitCode: answered.exitCode,
57
+ result: answered.result,
58
+ });
59
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
60
+ }
61
+ else {
62
+ for (const line of answered.lines)
63
+ info(line);
64
+ }
65
+ return answered.exitCode;
66
+ }
@@ -3,7 +3,8 @@ import { type Version } from '../../platform/versioning/index.js';
3
3
  export declare const COMMANDS: readonly ['build', 'deploy', 'diff', 'list', 'publish', 'test', 'lint', 'package', 'upgrade', 'yank'];
4
4
  export type Command = (typeof COMMANDS)[number];
5
5
  export interface ParsedArgs {
6
- readonly command: Command;
6
+ /** `adapter`: a provider command of an Environment's adapter (`<adapter> <command> <env>`). */
7
+ readonly command: Command | 'adapter';
7
8
  readonly env: string;
8
9
  readonly fix?: boolean;
9
10
  readonly format?: LintReportFormat;
@@ -19,6 +20,11 @@ export interface ParsedArgs {
19
20
  readonly allowDirty?: boolean;
20
21
  /** `list`: instances to ask what they run, in argument order. */
21
22
  readonly instances?: readonly string[];
23
+ /** `<adapter> <command> <env>`: the adapter name and its command. */
24
+ readonly adapter?: string;
25
+ readonly adapterCommand?: string;
26
+ /** `<adapter> <command> <env> --<option> <value>`: the options, by name. */
27
+ readonly options?: Readonly<Record<string, string>>;
22
28
  }
23
29
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
24
30
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
@@ -12,11 +12,18 @@ export const COMMANDS = [
12
12
  'upgrade',
13
13
  'yank',
14
14
  ];
15
+ /** An adapter name or one of its command and option names, as `defineAdapter` admits them. */
16
+ const PROVIDER_NAME = /^[a-z][a-z0-9-]{0,31}$/u;
15
17
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
16
18
  export function parseArgs(argv) {
17
19
  const [command, ...rest] = argv;
18
20
  if (command === 'upgrade')
19
21
  return parseUpgrade(rest);
22
+ if (command !== undefined && !COMMANDS.includes(command)) {
23
+ const provider = parseAdapterCommand(command, rest);
24
+ if (provider !== undefined)
25
+ return provider;
26
+ }
20
27
  const fix = rest.includes('--fix');
21
28
  const deployOnly = rest.includes('--deploy-only');
22
29
  const json = rest.includes('--json');
@@ -84,7 +91,7 @@ export function parseArgs(argv) {
84
91
  command !== 'list' &&
85
92
  command !== 'publish' &&
86
93
  command !== 'yank') {
87
- throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish` or `yank`.');
94
+ throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish`, `yank` or an adapter command.');
88
95
  }
89
96
  if (allowDirty && command !== 'publish') {
90
97
  throw new Error('`--allow-dirty` is only valid for `publish`.');
@@ -222,6 +229,51 @@ function refuseDeployOnly(command, environment) {
222
229
  throw new Error(`${refusal} Run \`astrale-domain ${command} ${environment}\` without it, then install the ` +
223
230
  `URL it prints with \`${installCommand()}\`.`);
224
231
  }
232
+ /**
233
+ * `astrale-domain <adapter> <command> <environment> [--<option> <value>]... [--json]`: a provider
234
+ * command, routed once the Project is loaded to the adapter of that Environment. Anything that is
235
+ * not exactly an adapter name, a command and an Environment is no command at all (undefined).
236
+ */
237
+ function parseAdapterCommand(adapter, args) {
238
+ if (!PROVIDER_NAME.test(adapter))
239
+ return undefined;
240
+ const positionals = [];
241
+ const options = {};
242
+ let json = false;
243
+ for (let index = 0; index < args.length; index += 1) {
244
+ const argument = args[index];
245
+ if (argument === '--json') {
246
+ json = true;
247
+ continue;
248
+ }
249
+ if (!argument.startsWith('-')) {
250
+ positionals.push(argument);
251
+ continue;
252
+ }
253
+ const equals = argument.indexOf('=');
254
+ const name = argument.slice(2, equals === -1 ? undefined : equals);
255
+ if (!argument.startsWith('--') || !PROVIDER_NAME.test(name)) {
256
+ throw new Error(`Unknown flag "${argument}".`);
257
+ }
258
+ if (Object.hasOwn(options, name))
259
+ throw new Error(`--${name} is given twice.`);
260
+ options[name] =
261
+ equals === -1
262
+ ? requiredValue(`--${name}`, args[++index])
263
+ : requiredValue(`--${name}`, argument.slice(equals + 1));
264
+ }
265
+ const [adapterCommand, env] = positionals;
266
+ if (positionals.length !== 2 || !PROVIDER_NAME.test(adapterCommand))
267
+ return undefined;
268
+ return {
269
+ command: 'adapter',
270
+ env: env,
271
+ adapter,
272
+ adapterCommand: adapterCommand,
273
+ options,
274
+ ...(json ? { json: true } : {}),
275
+ };
276
+ }
225
277
  function parseUpgrade(args) {
226
278
  let channel = 'beta';
227
279
  let check = false;
@@ -30,11 +30,15 @@ Commands:
30
30
  package Prepare public declarations for publication.
31
31
  upgrade Update Astrale dependencies through pnpm (default: beta).
32
32
  yank <version> Take a published version out of resolution, or put it back.
33
+ <adapter> <command> <environment>
34
+ Run a command the Environment's adapter declares for its provider,
35
+ such as cloudflare plan production or cloudflare init production.
33
36
 
34
37
  Options:
35
38
  -h, --help Show help.
36
39
 
37
- Run "astrale-domain <command> --help" for command-specific help.
40
+ Run "astrale-domain <command> --help" for command-specific help. An adapter command that
41
+ does not exist names the commands and options the Environment's adapter declares.
38
42
  `;
39
43
  const COMMAND_HELP = Object.freeze({
40
44
  upgrade: `Update existing Astrale dependencies in a pnpm project or workspace.
@@ -140,19 +144,18 @@ Behavior:
140
144
  The registry and the instances are read through the Astrale CLI (${MINIMUM_INSTALLATIONS_CLI_VERSION} or
141
145
  newer) with the caller's identity, only once a deployment is listed.
142
146
  A registry that cannot be read fails the listing; an unknown instance never does.
143
- A preview expires 30 days after its activation or its last call, whichever is
144
- later; a published deployment never does. Last calls are read from the host as it
145
- counts them, so a host whose last calls cannot be read is not listed. A platform
146
- dispatch namespace reads them from its calls dataset, and without Account Analytics
147
- Read lists its deployments without last calls or expiries.
147
+ Admin's Services and a Cloudflare dispatch namespace keep every deployment until its
148
+ owner retires it, and report no last call or expiry; LAST CALL and EXPIRES show one
149
+ only when an older adapter reports it.
148
150
  Listing changes nothing, at any host, registry or instance.
149
151
  An Environment in legacy direct mode, refused by deploy, or whose adapter lists no
150
152
  deployments is reported on stderr and, with --json, under unlisted. A deployment
151
153
  whose record this SDK does not admit is still listed, from what its host knows.
152
154
  With --json, each deployment carries its installedOn and its call target: the path of
153
155
  its node on Admin's Services, for astrale call "<path>.method.setSecret" --admin, or the
154
- namespace and script of a platform dispatch namespace, which its operator tooling
155
- takes; installations says what of INSTALLED ON is unknown.
156
+ namespace and script of a dispatch namespace, which the hosting kit
157
+ (@astrale-os/adapter-cloudflare/hosting) retires; installations says what of
158
+ INSTALLED ON is unknown.
156
159
  Exit status: 0 listed, 1 a host or the registry could not be read (with --json,
157
160
  nothing on stdout), 2 usage error.
158
161
  `,
@@ -1,4 +1,4 @@
1
- import type { DeploymentCommitV1, FrozenConfigurationV1 } from '../../deployment/address/index.js';
1
+ import type { DeploymentCommitV1, DeploymentConfiguration } from '../../deployment/address/index.js';
2
2
  import type { Adapter, Build, Bundle, LegacyAdapter, Placement, Release, ResolvedRuntimeReference } from '../../deployment/index.js';
3
3
  import type { DeployResultV1 } from './deploy-result.js';
4
4
  /**
@@ -15,7 +15,7 @@ export interface ImmutableDeploymentPlan {
15
15
  readonly identity?: string;
16
16
  readonly bundle: Bundle;
17
17
  readonly secrets: Readonly<Record<string, string>>;
18
- readonly configuration: FrozenConfigurationV1;
18
+ readonly configuration: DeploymentConfiguration;
19
19
  readonly placement: Placement;
20
20
  /** `<line>-<content>`: the deployment's DNS label and provider script. */
21
21
  readonly label: string;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@astrale-os/sdk/cli` — the `astrale-domain` CLI
3
- * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank), its JSON results,
4
- * the deploy mode each command selects for an Environment (CT33), and the explicit dotenv boundary.
3
+ * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank, and the
4
+ * provider commands adapters declare), its JSON results, the deploy mode each command selects for an Environment (CT33), and the explicit
5
+ * dotenv boundary.
5
6
  *
6
7
  * Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
7
8
  * Bun (it imports the project's `astrale.config.ts` directly). This subpath is
@@ -14,6 +15,7 @@ export { run } from './run.js';
14
15
  export type { BuildResultV1 } from './build.js';
15
16
  export type { DeployResultV1, DeploymentSecretsState } from './deploy-result.js';
16
17
  export type { DiffChangeV1, DiffReportV1 } from './diff/index.js';
18
+ export type { AdapterCommandReportV1 } from './adapter-command.js';
17
19
  export type { InstallationSourcesV1, InstallationUnknownReason, InstalledOnV1, InstanceSourceV1, ListedDeploymentV1, ListResultV1, UnlistedEnvironmentV1, } from './list.js';
18
20
  export type { PublishReportV1 } from './publish/index.js';
19
21
  export type { YankReportV1 } from './yank.js';
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `@astrale-os/sdk/cli` — the `astrale-domain` CLI
3
- * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank), its JSON results,
4
- * the deploy mode each command selects for an Environment (CT33), and the explicit dotenv boundary.
3
+ * (build | deploy | diff | list | publish | test | lint | package | upgrade | yank, and the
4
+ * provider commands adapters declare), its JSON results, the deploy mode each command selects for an Environment (CT33), and the explicit
5
+ * dotenv boundary.
5
6
  *
6
7
  * Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
7
8
  * Bun (it imports the project's `astrale.config.ts` directly). This subpath is
@@ -26,11 +26,11 @@ export interface ListResultV1 {
26
26
  }
27
27
  /**
28
28
  * One deployment: what its host knows (`DeploymentSummaryV1`), the Environment it was listed for
29
- * and its adapter, its computed name, and its call target. `lastCallAt` and a preview's `expiresAt`
30
- * are as of the listing: a platform namespace (adapter-cloudflare) reads every call from its calls
31
- * dataset, and omits both without Account Analytics Read. Admin's Services (adapter-astrale) keeps
32
- * every deployment until it is retired and counts no call: its rows are `published`, with neither
33
- * clock, and say whether their Service serves them (`current`).
29
+ * and its adapter, its computed name, and its call target. Admin's Services (adapter-astrale) keeps
30
+ * every deployment until it is retired: its rows are `published` and say whether their Service
31
+ * serves them (`current`). A Cloudflare dispatch namespace (adapter-cloudflare) keeps no
32
+ * retention, and knows a deployment's creation instant and key only while its script exists. A
33
+ * last call and an expiry are printed only when an older adapter still reports them.
34
34
  */
35
35
  export type ListedDeploymentV1 = Omit<DeploymentSummaryV1, 'record'> & {
36
36
  /** The Project Environment whose line holds the deployment, as its host listed it. */
@@ -189,13 +189,13 @@ function listedDeployment(hosted, publications, installations) {
189
189
  line: deployment.line,
190
190
  owner: deployment.owner,
191
191
  state: deployment.state,
192
- retention: deployment.retention,
193
- createdAt: deployment.createdAt,
192
+ ...(deployment.retention === undefined ? {} : { retention: deployment.retention }),
193
+ ...(deployment.createdAt === undefined ? {} : { createdAt: deployment.createdAt }),
194
194
  ...(deployment.lastCallAt === undefined ? {} : { lastCallAt: deployment.lastCallAt }),
195
195
  ...(deployment.expiresAt === undefined ? {} : { expiresAt: deployment.expiresAt }),
196
196
  ...(deployment.retiredAt === undefined ? {} : { retiredAt: deployment.retiredAt }),
197
197
  ...(deployment.retirement === undefined ? {} : { retirement: deployment.retirement }),
198
- signingKeyId: deployment.signingKeyId,
198
+ ...(deployment.signingKeyId === undefined ? {} : { signingKeyId: deployment.signingKeyId }),
199
199
  record,
200
200
  ...(recordRefusal === undefined ? {} : { recordRefusal }),
201
201
  ...(deployment.current === undefined ? {} : { current: deployment.current }),
@@ -203,11 +203,11 @@ function listedDeployment(hosted, publications, installations) {
203
203
  });
204
204
  }
205
205
  /**
206
- * Why a deployment its host keeps no record of has none: the adapter lists it without `record`, as a
207
- * platform namespace lists a deployment it has not activated.
206
+ * Why a deployment its host keeps no record of has none: the adapter lists it without `record`, as
207
+ * a dispatch namespace lists a label it retired and never activated.
208
208
  */
209
- const NO_RECORD = 'Its host keeps no record of it: it has not been activated (a deploy still in progress, or one ' +
210
- 'that stopped before activating it).';
209
+ const NO_RECORD = 'Its host keeps no record of it: it was never activated (a deploy still in progress, one that ' +
210
+ 'stopped before activating it, or a label retired before it served).';
211
211
  /** Why an admitted record does not describe the listed deployment, if it does not. */
212
212
  function describesAnother(record, label, listing) {
213
213
  if (record.providerScript !== label) {
@@ -259,18 +259,21 @@ function admitListed(deployment, adapter) {
259
259
  throw new TypeError(`Adapter ${adapter} listed an invalid deployment (${member}).`);
260
260
  };
261
261
  const value = members(deployment) ?? invalid('deployment');
262
- for (const key of ['id', 'url', 'label', 'line', 'owner', 'createdAt', 'signingKeyId']) {
262
+ for (const key of ['id', 'url', 'label', 'line', 'owner']) {
263
263
  if (!text(value[key]))
264
264
  invalid(key);
265
265
  }
266
- for (const key of ['lastCallAt', 'expiresAt', 'retiredAt']) {
266
+ for (const key of ['createdAt', 'signingKeyId', 'lastCallAt', 'expiresAt', 'retiredAt']) {
267
267
  if (value[key] !== undefined && !text(value[key]))
268
268
  invalid(key);
269
269
  }
270
270
  if (!STATES.includes(value.state))
271
271
  invalid('state');
272
- if (value.retention !== 'preview' && value.retention !== 'published')
272
+ if (value.retention !== undefined &&
273
+ value.retention !== 'preview' &&
274
+ value.retention !== 'published') {
273
275
  invalid('retention');
276
+ }
274
277
  if (value.retirement !== undefined &&
275
278
  value.retirement !== 'expired' &&
276
279
  value.retirement !== 'withdrawn') {
@@ -352,10 +355,9 @@ function shortDigest(digest) {
352
355
  return `${digest.slice(0, 'sha256:'.length + 7)}…`;
353
356
  }
354
357
  /**
355
- * When a deployment stops serving: `never` once published, else its preview expiry as its host
356
- * keeps it, 30 days after its activation or its last call, whichever is later (AM-123), with the
357
- * calls counted since the host recorded them; `due` once that instant passed but the host has not
358
- * retired it yet; for a retired deployment, why it was retired.
358
+ * When a deployment stops serving: `never` once published, else the expiry an older adapter
359
+ * reports (`due` once that instant passed), else `—` (a dispatch namespace keeps a deployment
360
+ * until its owner retires it); for a retired deployment, why it was retired.
359
361
  */
360
362
  function expiry(deployment, now) {
361
363
  if (deployment.state === 'retiring')