@astrale-os/sdk 0.6.0-beta.15 → 0.6.0-beta.17

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 (49) hide show
  1. package/dist/deployment/adapter/adapter.d.ts +3 -2
  2. package/dist/deployment/adapter/index.d.ts +1 -0
  3. package/dist/deployment/adapter/index.js +1 -0
  4. package/dist/deployment/adapter/release/refusal.d.ts +10 -0
  5. package/dist/deployment/adapter/release/refusal.js +14 -0
  6. package/dist/deployment/address/summary.d.ts +3 -2
  7. package/dist/tooling/cli/arguments.d.ts +9 -1
  8. package/dist/tooling/cli/arguments.js +76 -5
  9. package/dist/tooling/cli/commit.d.ts +12 -0
  10. package/dist/tooling/cli/commit.js +19 -0
  11. package/dist/tooling/cli/deploy-mode.d.ts +18 -6
  12. package/dist/tooling/cli/deploy-mode.js +28 -1
  13. package/dist/tooling/cli/diff/declared.d.ts +7 -0
  14. package/dist/tooling/cli/diff/declared.js +27 -0
  15. package/dist/tooling/cli/diff/index.d.ts +30 -0
  16. package/dist/tooling/cli/diff/index.js +58 -0
  17. package/dist/tooling/cli/diff/render.d.ts +15 -0
  18. package/dist/tooling/cli/diff/render.js +131 -0
  19. package/dist/tooling/cli/diff/report.d.ts +88 -0
  20. package/dist/tooling/cli/diff/report.js +154 -0
  21. package/dist/tooling/cli/help.js +124 -14
  22. package/dist/tooling/cli/immutable-deployment.d.ts +65 -9
  23. package/dist/tooling/cli/immutable-deployment.js +59 -19
  24. package/dist/tooling/cli/index.d.ts +9 -3
  25. package/dist/tooling/cli/index.js +6 -2
  26. package/dist/tooling/cli/installed-on.d.ts +83 -0
  27. package/dist/tooling/cli/installed-on.js +156 -0
  28. package/dist/tooling/cli/list.d.ts +56 -22
  29. package/dist/tooling/cli/list.js +111 -32
  30. package/dist/tooling/cli/orchestrate.js +126 -6
  31. package/dist/tooling/cli/publish/index.d.ts +74 -0
  32. package/dist/tooling/cli/publish/index.js +364 -0
  33. package/dist/tooling/cli/publish/report.d.ts +49 -0
  34. package/dist/tooling/cli/publish/report.js +15 -0
  35. package/dist/tooling/cli/registry/client.d.ts +127 -0
  36. package/dist/tooling/cli/registry/client.js +351 -0
  37. package/dist/tooling/cli/registry/error.d.ts +11 -0
  38. package/dist/tooling/cli/registry/error.js +17 -0
  39. package/dist/tooling/cli/registry/executable.d.ts +29 -0
  40. package/dist/tooling/cli/registry/executable.js +90 -0
  41. package/dist/tooling/cli/registry/index.d.ts +9 -0
  42. package/dist/tooling/cli/registry/index.js +8 -0
  43. package/dist/tooling/cli/registry/installations.d.ts +67 -0
  44. package/dist/tooling/cli/registry/installations.js +108 -0
  45. package/dist/tooling/cli/registry/process.d.ts +36 -0
  46. package/dist/tooling/cli/registry/process.js +119 -0
  47. package/dist/tooling/cli/yank.d.ts +34 -0
  48. package/dist/tooling/cli/yank.js +32 -0
  49. package/package.json +2 -2
@@ -39,8 +39,9 @@ export interface Adapter<Parameters extends object = object> {
39
39
  list?(parameters: Parameters, context: ListContext): Promise<readonly ListedDeployment[]>;
40
40
  /**
41
41
  * Keep one published deployment where the host has no registry principal to mark it (a platform
42
- * namespace). Called only after its Publication commits. Absent: the registry marks it (Admin's
43
- * Services).
42
+ * namespace). Called only after its Publication commits. Rejects with a `RetainRefusal` when the
43
+ * host can no longer keep it (absent or retired), which no rerun changes; any other rejection is
44
+ * transient. Absent: the registry marks it (Admin's Services).
44
45
  */
45
46
  retain?(parameters: Parameters, context: RetainContext): Promise<void>;
46
47
  /**
@@ -1,5 +1,6 @@
1
1
  export type { Adapter, AdapterEnvironmentContext, AdapterInput } from './adapter.js';
2
2
  export { defineAdapter, defineDeployment, isAdapter } from './define.js';
3
+ export { RetainRefusal } from './release/refusal.js';
3
4
  export type * from './bundle/index.js';
4
5
  export type * from './release/index.js';
5
6
  /** @deprecated The contract before adapter interface v2; see `./legacy/index.ts`. */
@@ -1 +1,2 @@
1
1
  export { defineAdapter, defineDeployment, isAdapter } from './define.js';
2
+ export { RetainRefusal } from './release/refusal.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Why the host can no longer keep one published deployment (`Adapter.retain`): it is `absent`
3
+ * (the host holds no deployment of that label, or it was deleted) or `retired` (withdrawn, never
4
+ * served again). No rerun keeps it; a new version publishes a new deployment. Any other rejection
5
+ * of `retain` is transient: once it has passed, a rerun keeps the deployment.
6
+ */
7
+ export declare class RetainRefusal extends Error {
8
+ readonly reason: 'absent' | 'retired';
9
+ constructor(reason: RetainRefusal['reason'], message: string);
10
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Why the host can no longer keep one published deployment (`Adapter.retain`): it is `absent`
3
+ * (the host holds no deployment of that label, or it was deleted) or `retired` (withdrawn, never
4
+ * served again). No rerun keeps it; a new version publishes a new deployment. Any other rejection
5
+ * of `retain` is transient: once it has passed, a rerun keeps the deployment.
6
+ */
7
+ export class RetainRefusal extends Error {
8
+ reason;
9
+ constructor(reason, message) {
10
+ super(message);
11
+ this.name = 'RetainRefusal';
12
+ this.reason = reason;
13
+ }
14
+ }
@@ -30,8 +30,9 @@ export declare const PLATFORM_OWNER = "platform";
30
30
  * `summary:<label>` in the routing KV of a platform dispatch namespace (CT20, AM-131): the
31
31
  * `DeploymentSummaryV1` of one deployment without its record, which stays at `record:<label>`.
32
32
  *
33
- * Its writers are the deployer (`cloudflare({ namespace })`), which writes it once at activation,
34
- * before `record:<label>`, then marks it published (`retain`), and the platform retirement tooling
33
+ * Its writers are the deployer (`cloudflare({ namespace })`), which writes it at activation,
34
+ * before `record:<label>`, names in it the new key of a script it recreates under the label after
35
+ * an operator deleted it, and marks it published (`retain`), and the platform retirement tooling
35
36
  * (SV6), which records a withdrawal. So it names the deployment by its label (`id` is the label),
36
37
  * is owned by `platform`, is `active` until withdrawn, and carries no `lastCallAt` or `expiresAt`:
37
38
  * nothing on the platform namespace keeps those clocks, and listings read them from the calls
@@ -1,5 +1,6 @@
1
1
  import type { LintReportFormat } from '../linter/index.js';
2
- export declare const COMMANDS: readonly ['dev', 'build', 'deploy', 'list', 'test', 'lint', 'package', 'upgrade'];
2
+ import { type Version } from '../../platform/versioning/index.js';
3
+ export declare const COMMANDS: readonly ['dev', 'build', 'deploy', 'diff', 'list', 'publish', 'test', 'lint', 'package', 'upgrade', 'yank'];
3
4
  export type Command = (typeof COMMANDS)[number];
4
5
  export interface ParsedArgs {
5
6
  readonly command: Command;
@@ -13,6 +14,13 @@ export interface ParsedArgs {
13
14
  readonly channel?: string;
14
15
  readonly check?: boolean;
15
16
  readonly json?: boolean;
17
+ /** `yank`: the published version it names. */
18
+ readonly version?: Version;
19
+ /** `yank --undo`: restore the version instead of yanking it. */
20
+ readonly undo?: boolean;
21
+ readonly allowDirty?: boolean;
22
+ /** `list`: instances to ask what they run, in argument order. */
23
+ readonly instances?: readonly string[];
16
24
  }
17
25
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
18
26
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
@@ -1,14 +1,18 @@
1
1
  import { isAbsolute, normalize } from 'node:path';
2
+ import { acceptVersion } from '../../platform/versioning/index.js';
2
3
  import { installCommand } from '../../project/install-command.js';
3
4
  export const COMMANDS = [
4
5
  'dev',
5
6
  'build',
6
7
  'deploy',
8
+ 'diff',
7
9
  'list',
10
+ 'publish',
8
11
  'test',
9
12
  'lint',
10
13
  'package',
11
14
  'upgrade',
15
+ 'yank',
12
16
  ];
13
17
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
14
18
  export function parseArgs(argv) {
@@ -18,12 +22,15 @@ export function parseArgs(argv) {
18
22
  const fix = rest.includes('--fix');
19
23
  const deployOnly = rest.includes('--deploy-only');
20
24
  const json = rest.includes('--json');
25
+ const undo = rest.includes('--undo');
26
+ const allowDirty = rest.includes('--allow-dirty');
21
27
  let secretsFile;
22
28
  let credentialFile;
23
29
  let identity;
24
30
  let suite;
25
31
  let format;
26
32
  let environment;
33
+ const instances = [];
27
34
  const cleaned = [];
28
35
  for (let index = 0; index < rest.length; index += 1) {
29
36
  const argument = rest[index];
@@ -57,13 +64,24 @@ export function parseArgs(argv) {
57
64
  else if (argument.startsWith('--environment=')) {
58
65
  environment = requiredValue('--environment', argument.slice('--environment='.length));
59
66
  }
67
+ else if (command === 'list' && argument === '--instance') {
68
+ // Only `list` asks instances; elsewhere `--instance` stays unknown, as deploy removed it.
69
+ instances.push(requiredValue('--instance', rest[++index]));
70
+ }
71
+ else if (command === 'list' && argument.startsWith('--instance=')) {
72
+ instances.push(requiredValue('--instance', argument.slice('--instance='.length)));
73
+ }
60
74
  else if (argument === '--suite') {
61
75
  suite = requiredValue('--suite', rest[++index]);
62
76
  }
63
77
  else if (argument.startsWith('--suite=')) {
64
78
  suite = requiredValue('--suite', argument.slice('--suite='.length));
65
79
  }
66
- else if (argument === '--fix' || argument === '--deploy-only' || argument === '--json') {
80
+ else if (argument === '--fix' ||
81
+ argument === '--deploy-only' ||
82
+ argument === '--json' ||
83
+ argument === '--undo' ||
84
+ argument === '--allow-dirty') {
67
85
  continue;
68
86
  }
69
87
  else if (argument.startsWith('-')) {
@@ -75,9 +93,20 @@ export function parseArgs(argv) {
75
93
  }
76
94
  if (fix && command !== 'lint')
77
95
  throw new Error('`--fix` is only valid for `lint`.');
78
- if (json && command !== 'build' && command !== 'deploy' && command !== 'list') {
79
- throw new Error('`--json` is only valid for `build`, `deploy` or `list`.');
96
+ if (json &&
97
+ command !== 'build' &&
98
+ command !== 'deploy' &&
99
+ command !== 'diff' &&
100
+ command !== 'list' &&
101
+ command !== 'publish' &&
102
+ command !== 'yank') {
103
+ throw new Error('`--json` is only valid for `build`, `deploy`, `diff`, `list`, `publish` or `yank`.');
80
104
  }
105
+ if (allowDirty && command !== 'publish') {
106
+ throw new Error('`--allow-dirty` is only valid for `publish`.');
107
+ }
108
+ if (undo && command !== 'yank')
109
+ throw new Error('`--undo` is only valid for `yank`.');
81
110
  if (format !== undefined && command !== 'lint') {
82
111
  throw new Error('`--format` is only valid for `lint`.');
83
112
  }
@@ -89,8 +118,13 @@ export function parseArgs(argv) {
89
118
  }
90
119
  if (deployOnly && command !== 'dev' && command !== 'deploy')
91
120
  refuseDeployOnly(undefined);
92
- if (identity !== undefined && command !== 'dev' && command !== 'deploy' && command !== 'list') {
93
- throw new Error('`--as` is only valid for `dev`, `deploy` or `list`.');
121
+ if (identity !== undefined &&
122
+ command !== 'dev' &&
123
+ command !== 'deploy' &&
124
+ command !== 'diff' &&
125
+ command !== 'list' &&
126
+ command !== 'publish') {
127
+ throw new Error('`--as` is only valid for `dev`, `deploy`, `diff`, `list` or `publish`.');
94
128
  }
95
129
  if (environment !== undefined &&
96
130
  command !== 'dev' &&
@@ -137,6 +171,7 @@ export function parseArgs(argv) {
137
171
  env: environment ?? '',
138
172
  ...(json ? { json: true } : {}),
139
173
  ...(identity === undefined ? {} : { identity }),
174
+ ...(instances.length === 0 ? {} : { instances: [...new Set(instances)] }),
140
175
  };
141
176
  case 'build':
142
177
  case 'package':
@@ -144,6 +179,30 @@ export function parseArgs(argv) {
144
179
  throw new Error(`\`${command}\` does not accept positional arguments.`);
145
180
  }
146
181
  return { command, env: '', ...(json ? { json: true } : {}) };
182
+ case 'diff':
183
+ // The version is package.json's: no command takes a version number (tech Versioning).
184
+ if (positionals.length > 0) {
185
+ throw new Error('`diff` does not accept positional arguments; it reads package.json.');
186
+ }
187
+ return {
188
+ command: 'diff',
189
+ env: '',
190
+ ...(json ? { json: true } : {}),
191
+ ...(identity === undefined ? {} : { identity }),
192
+ };
193
+ case 'publish': {
194
+ // The version is package.json's: the one positional names the Environment (tech Versioning).
195
+ if (positionals.length !== 1) {
196
+ throw new Error('`publish` takes exactly one Environment, e.g. `publish production`; it reads the version from package.json.');
197
+ }
198
+ return {
199
+ command: 'publish',
200
+ env: positionals[0],
201
+ ...(json ? { json: true } : {}),
202
+ ...(allowDirty ? { allowDirty: true } : {}),
203
+ ...(identity === undefined ? {} : { identity }),
204
+ };
205
+ }
147
206
  case 'test':
148
207
  if (positionals.length > 0) {
149
208
  throw new Error('`test` does not accept positional arguments; use `--environment`.');
@@ -153,6 +212,18 @@ export function parseArgs(argv) {
153
212
  env: environment ?? '',
154
213
  ...(suite === undefined ? {} : { suite }),
155
214
  };
215
+ case 'yank': {
216
+ if (positionals.length !== 1) {
217
+ throw new Error('`yank` takes exactly one published version, e.g. `yank 1.5.0`.');
218
+ }
219
+ return {
220
+ command: 'yank',
221
+ env: '',
222
+ version: acceptVersion(positionals[0]),
223
+ ...(undo ? { undo: true } : {}),
224
+ ...(json ? { json: true } : {}),
225
+ };
226
+ }
156
227
  case 'lint':
157
228
  if (positionals.length > 0)
158
229
  throw new Error('`lint` does not accept positional arguments.');
@@ -21,3 +21,15 @@ export declare function deploymentCommit(input: {
21
21
  readonly runtimeModule: string;
22
22
  readonly signal: AbortSignal;
23
23
  }): Promise<DeploymentCommitV1 | null>;
24
+ /**
25
+ * Whether a remote-tracking branch of the repository already holds a commit. A Publication records
26
+ * the commit it was built from, and release-please computes the next number from the pushed
27
+ * history: publishing a commit nobody pushed risks the same number being computed again (tech
28
+ * Versioning [.73872]). Nothing when git cannot tell. Reads the local remote-tracking refs only,
29
+ * never the network.
30
+ */
31
+ export declare function commitPushed(input: {
32
+ readonly projectDir: string;
33
+ readonly sha: string;
34
+ readonly signal: AbortSignal;
35
+ }): Promise<boolean | undefined>;
@@ -46,6 +46,25 @@ export async function deploymentCommit(input) {
46
46
  ...(base === undefined ? {} : { base }),
47
47
  });
48
48
  }
49
+ /**
50
+ * Whether a remote-tracking branch of the repository already holds a commit. A Publication records
51
+ * the commit it was built from, and release-please computes the next number from the pushed
52
+ * history: publishing a commit nobody pushed risks the same number being computed again (tech
53
+ * Versioning [.73872]). Nothing when git cannot tell. Reads the local remote-tracking refs only,
54
+ * never the network.
55
+ */
56
+ export async function commitPushed(input) {
57
+ if (!SHA.test(input.sha))
58
+ return undefined;
59
+ const branches = await git(input.projectDir, input.signal, [
60
+ 'branch',
61
+ '--remotes',
62
+ '--contains',
63
+ input.sha,
64
+ '--format=%(refname)',
65
+ ]);
66
+ return branches === undefined ? undefined : branches.length > 0;
67
+ }
49
68
  /**
50
69
  * The prefixes of the release tags of the package the Domain is released from, or nothing when
51
70
  * no release line holds the Domain.
@@ -1,4 +1,3 @@
1
- import type { LoadedEnvironment } from './project.js';
2
1
  /**
3
2
  * The deploy mode of one Environment for one command (CT33), chosen by the configuration itself:
4
3
  * an unchanged configuration keeps its mode, and so its behaviour, across an SDK upgrade, as long as
@@ -41,12 +40,15 @@ export type LegacyDirectReason =
41
40
  };
42
41
  export type CloudflareDirectParameter = 'route' | 'workerName' | 'identityIssuer' | 'addressing' | 'signingIdentity';
43
42
  export interface DeployRefusal {
44
- readonly reason: 'astrale-instance' | 'astrale-signing-identity' | 'mixed-parameters' | 'scheduled-triggers' | 'development-canonical';
43
+ readonly reason: 'astrale-instance' | 'astrale-signing-identity' | 'mixed-parameters' | 'scheduled-triggers' | 'development-canonical' | 'publish-legacy-direct';
45
44
  /** The configuration or command to write instead, for the author. */
46
45
  readonly guidance: string;
47
46
  }
48
- /** The commands that select a deploy mode. `publish` refuses every Environment that is not canonical. */
49
- export type DeployModeCommand = 'deploy' | 'dev';
47
+ /**
48
+ * The commands that select a deploy mode. `publish` names only an immutable deployment, so it
49
+ * refuses every Environment that is not canonical (CT33, R-G03).
50
+ */
51
+ export type DeployModeCommand = 'deploy' | 'dev' | 'publish';
50
52
  /**
51
53
  * Select the deploy mode of one Environment (CT33). It reads the configuration and the command
52
54
  * only, and has no effect.
@@ -56,7 +58,8 @@ export type DeployModeCommand = 'deploy' | 'dev';
56
58
  * before any effect, since their one consumer pins an older SDK (AM-72). Otherwise the Environment
57
59
  * is on the legacy direct-mode path when adapter-cloudflare names a direct-mode parameter or no
58
60
  * `namespace`, or when its adapter declares no `deployRelease`. Everything else is canonical,
59
- * except under `dev`, which serves the legacy direct-mode path only.
61
+ * except under `dev`, which serves the legacy direct-mode path only. `publish` refuses the legacy
62
+ * direct-mode path: a stable target serves no release, so no version can name it.
60
63
  *
61
64
  * Within canonical adapter-cloudflare (with `namespace`), a direct-mode parameter is refused
62
65
  * rather than silently selecting either mode, and so is `wrangler.triggers`: an immutable
@@ -66,5 +69,14 @@ export type DeployModeCommand = 'deploy' | 'dev';
66
69
  export declare function selectDeployMode(input: {
67
70
  readonly command: DeployModeCommand;
68
71
  readonly environment: string;
69
- readonly definition: LoadedEnvironment;
72
+ /** The Environment's definition, of which selection reads only the adapter and its parameters. */
73
+ readonly definition: {
74
+ readonly deployment: {
75
+ readonly adapter: {
76
+ readonly name: string;
77
+ readonly deployRelease?: unknown;
78
+ };
79
+ readonly parameters: object;
80
+ };
81
+ };
70
82
  }): DeployMode;
@@ -22,7 +22,8 @@ const CLOUDFLARE_DIRECT_PARAMETERS = [
22
22
  * before any effect, since their one consumer pins an older SDK (AM-72). Otherwise the Environment
23
23
  * is on the legacy direct-mode path when adapter-cloudflare names a direct-mode parameter or no
24
24
  * `namespace`, or when its adapter declares no `deployRelease`. Everything else is canonical,
25
- * except under `dev`, which serves the legacy direct-mode path only.
25
+ * except under `dev`, which serves the legacy direct-mode path only. `publish` refuses the legacy
26
+ * direct-mode path: a stable target serves no release, so no version can name it.
26
27
  *
27
28
  * Within canonical adapter-cloudflare (with `namespace`), a direct-mode parameter is refused
28
29
  * rather than silently selecting either mode, and so is `wrangler.triggers`: an immutable
@@ -83,6 +84,14 @@ export function selectDeployMode(input) {
83
84
  reasons.push({ reason: 'no-deploy-release' });
84
85
  if (refusals.length > 0)
85
86
  return Object.freeze({ kind: 'refused', refusals: freeze(refusals) });
87
+ if (reasons.length > 0 && command === 'publish') {
88
+ return Object.freeze({
89
+ kind: 'refused',
90
+ refusals: freeze([
91
+ { reason: 'publish-legacy-direct', guidance: publishGuidance(environment, reasons) },
92
+ ]),
93
+ });
94
+ }
86
95
  if (reasons.length > 0)
87
96
  return Object.freeze({ kind: 'legacy-direct', reasons: freeze(reasons) });
88
97
  if (command === 'dev') {
@@ -124,6 +133,24 @@ function organizationGuidance(parameters, environment) {
124
133
  ? ''
125
134
  : ", and name the organisation that holds the deployment line in `astrale({ organization: '<Identity id>' })`";
126
135
  }
136
+ /**
137
+ * Why a legacy direct-mode Environment cannot be published, and the configuration that makes it
138
+ * publishable: a version names an immutable deployment's release, which a stable target never
139
+ * serves.
140
+ */
141
+ function publishGuidance(environment, reasons) {
142
+ const deploys = `\`astrale-domain publish ${environment}\` names an immutable deployment's release, and ${environment} deploys`;
143
+ if (reasons.some(({ reason }) => reason === 'no-deploy-release')) {
144
+ return (`${deploys} through an adapter without deployRelease, which replaces a stable target in ` +
145
+ 'place and serves no release. Use an adapter that implements deployRelease (adapter ' +
146
+ "interface v2), such as astrale() on Admin's Services.");
147
+ }
148
+ const direct = reasons.flatMap((reason) => reason.reason === 'direct-parameter' ? [reason.parameter] : []);
149
+ return (`${deploys} on cloudflare()'s legacy direct mode, which serves no release. Deploy it into a ` +
150
+ 'dispatch namespace with cloudflare({ namespace })' +
151
+ (direct.length === 0 ? '' : ` and remove ${direct.join(', ')}`) +
152
+ ", or on Admin's Services with astrale().");
153
+ }
127
154
  /** A parameter is declared when its value is not `undefined`, as optional adapter parameters read. */
128
155
  function declares(parameters, name) {
129
156
  return Object.hasOwn(parameters, name) && parameters[name] !== undefined;
@@ -0,0 +1,7 @@
1
+ import type { Version } from '../../../platform/versioning/index.js';
2
+ /**
3
+ * The version the author declares for the next release: `package.json` `version` in the Project
4
+ * directory, as release-please and `npm version` write it (tech Versioning [.21210]). No command
5
+ * takes a version number.
6
+ */
7
+ export declare function declaredVersion(projectDir: string): Promise<Version>;
@@ -0,0 +1,27 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { acceptVersion } from '../../../platform/versioning/index.js';
4
+ /**
5
+ * The version the author declares for the next release: `package.json` `version` in the Project
6
+ * directory, as release-please and `npm version` write it (tech Versioning [.21210]). No command
7
+ * takes a version number.
8
+ */
9
+ export async function declaredVersion(projectDir) {
10
+ const path = join(projectDir, 'package.json');
11
+ let manifest;
12
+ try {
13
+ manifest = JSON.parse(await readFile(path, 'utf8'));
14
+ }
15
+ catch (cause) {
16
+ throw new TypeError(`Cannot read the Domain's version: ${path} is not a readable JSON file.`, {
17
+ cause,
18
+ });
19
+ }
20
+ const version = typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)
21
+ ? manifest.version
22
+ : undefined;
23
+ if (typeof version !== 'string') {
24
+ throw new TypeError(`${path} declares no version. The Domain's version is the package.json version, for example written by npm version minor.`);
25
+ }
26
+ return acceptVersion(version);
27
+ }
@@ -0,0 +1,30 @@
1
+ import type { Build } from '../../../deployment/index.js';
2
+ import type { DomainRegistry } from '../registry/index.js';
3
+ export type { DiffChangeV1, DiffReportV1 } from './report.js';
4
+ export interface DiffCommandInput {
5
+ readonly projectDir: string;
6
+ readonly build: Build;
7
+ /** The build digest of every distinct bundler of the Project, bundled on demand. */
8
+ readonly builds: () => Promise<readonly string[]>;
9
+ readonly identity?: string;
10
+ readonly json: boolean;
11
+ readonly signal: AbortSignal;
12
+ }
13
+ export interface DiffCommandDependencies {
14
+ readonly open: (input: {
15
+ readonly projectDir: string;
16
+ readonly identity?: string;
17
+ readonly signal: AbortSignal;
18
+ }) => Promise<DomainRegistry>;
19
+ readonly stdout: (text: string) => void;
20
+ readonly stderr: (text: string) => void;
21
+ }
22
+ /**
23
+ * `astrale-domain diff [--as <identity>] [--json]`: the changes since the highest published stable
24
+ * version up to the declared one, and the smallest version they admit (tech Versioning [.72565]).
25
+ * On a feature branch the declared version is still the published one and the report is
26
+ * informational (exit 0); on a release PR an unpublished version that is too low fails (exit 1)
27
+ * with the [.105820] message. Reads the registry through the Astrale CLI under the caller's own
28
+ * credential and changes nothing.
29
+ */
30
+ export declare function runDiff(input: DiffCommandInput, dependencies?: DiffCommandDependencies): Promise<number>;
@@ -0,0 +1,58 @@
1
+ import * as bundle from '@astrale-os/kernel-dsl/v1/bundle/transport';
2
+ import { DIM, RED, RESET } from '../log.js';
3
+ import { RegistryCliError, openDomainRegistry } from '../registry/index.js';
4
+ import { declaredVersion } from './declared.js';
5
+ import { diffExitCode, renderDiff } from './render.js';
6
+ import { diffReport, publishedSchema } from './report.js';
7
+ /**
8
+ * `astrale-domain diff [--as <identity>] [--json]`: the changes since the highest published stable
9
+ * version up to the declared one, and the smallest version they admit (tech Versioning [.72565]).
10
+ * On a feature branch the declared version is still the published one and the report is
11
+ * informational (exit 0); on a release PR an unpublished version that is too low fails (exit 1)
12
+ * with the [.105820] message. Reads the registry through the Astrale CLI under the caller's own
13
+ * credential and changes nothing.
14
+ */
15
+ export async function runDiff(input, dependencies = {
16
+ open: openDomainRegistry,
17
+ stdout: (text) => void process.stdout.write(text),
18
+ stderr: (text) => void process.stderr.write(text),
19
+ }) {
20
+ const root = input.build.schema.compiled.root;
21
+ let report;
22
+ try {
23
+ const declared = await declaredVersion(input.projectDir);
24
+ const registry = await dependencies.open({
25
+ projectDir: input.projectDir,
26
+ ...(input.identity === undefined ? {} : { identity: input.identity }),
27
+ signal: input.signal,
28
+ });
29
+ report = await diffReport({
30
+ origin: root.origin,
31
+ declared,
32
+ revision: root.revision,
33
+ index: await registry.index(root.origin),
34
+ current: () => bundle.decode(input.build.schema.bundle.bytes()).root,
35
+ builds: input.builds,
36
+ published: async (publication) => publishedSchema(await registry.bundle(root.origin, publication), publication, root.origin),
37
+ });
38
+ }
39
+ catch (cause) {
40
+ if (!(cause instanceof RegistryCliError))
41
+ throw cause;
42
+ // Nothing on stdout: a JSON consumer reads one report or nothing.
43
+ dependencies.stderr(`${RED}✗${RESET} Domain registry: ${cause.code}: ${cause.message}\n`);
44
+ return 1;
45
+ }
46
+ const rendered = renderDiff(report);
47
+ // With --json, stdout carries only the report and the summary moves to stderr, as for deploy.
48
+ const summary = input.json ? dependencies.stderr : dependencies.stdout;
49
+ // The sentence takes the progress mark; the member rows below it keep their indentation.
50
+ for (const line of rendered.summary) {
51
+ summary(line.startsWith(' ') ? `${line}\n` : `${DIM}›${RESET} ${line}\n`);
52
+ }
53
+ if (rendered.refusal.length > 0)
54
+ dependencies.stderr(`${rendered.refusal.join('\n')}\n`);
55
+ if (input.json)
56
+ dependencies.stdout(`${JSON.stringify(report, null, 2)}\n`);
57
+ return diffExitCode(report);
58
+ }
@@ -0,0 +1,15 @@
1
+ import type { DiffReportV1 } from './report.js';
2
+ /**
3
+ * 0 when the declared version is accepted, already published (the report is then informational:
4
+ * a feature branch keeps the last published number until its release), or has nothing to compare
5
+ * with; 1 when an unpublished declared version is too low.
6
+ */
7
+ export declare function diffExitCode(report: DiffReportV1): 0 | 1;
8
+ /**
9
+ * The report for a person. `summary` describes the comparison; `refusal` is the [.105820] message
10
+ * for an unpublished version that is too low (tech Versioning), printed as an error.
11
+ */
12
+ export declare function renderDiff(report: DiffReportV1): {
13
+ readonly summary: readonly string[];
14
+ readonly refusal: readonly string[];
15
+ };