@astrale-os/sdk 0.5.0-beta.80 → 0.5.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/deployment/adapter/development/context.d.ts +2 -0
- package/dist/deployment/adapter/development/index.d.ts +1 -1
- package/dist/deployment/adapter/development/placement.d.ts +10 -0
- package/dist/tooling/cli/arguments.d.ts +1 -0
- package/dist/tooling/cli/arguments.js +19 -2
- package/dist/tooling/cli/development/develop.d.ts +1 -0
- package/dist/tooling/cli/development/develop.js +115 -2
- package/dist/tooling/cli/development/rebuild.d.ts +1 -1
- package/dist/tooling/cli/development/rebuild.js +1 -1
- package/dist/tooling/cli/development/session-file.d.ts +53 -0
- package/dist/tooling/cli/development/session-file.js +337 -0
- package/dist/tooling/cli/help.js +5 -0
- package/dist/tooling/cli/index.d.ts +4 -1
- package/dist/tooling/cli/index.js +3 -1
- package/dist/tooling/cli/orchestrate.js +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0-beta.81](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.80...sdk-v0.5.0-beta.81) (2026-08-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **app:** add development session file ([#339](https://github.com/astrale-os/sdk/issues/339)) ([f28e9c6](https://github.com/astrale-os/sdk/commit/f28e9c6fb707b8791a5bc63eba3b616aa7b02342))
|
|
9
|
+
|
|
3
10
|
## [0.5.0-beta.80](https://github.com/astrale-os/sdk/compare/sdk-v0.5.0-beta.79...sdk-v0.5.0-beta.80) (2026-08-29)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -10,5 +10,7 @@ export interface DevelopmentContext extends DevelopmentInput {
|
|
|
10
10
|
readonly environment: string;
|
|
11
11
|
readonly overrides: DevelopmentOverrides;
|
|
12
12
|
readonly signal: AbortSignal;
|
|
13
|
+
/** Report loss of the current ready placement before provider recovery starts. */
|
|
14
|
+
onRecovering?(): Promise<void>;
|
|
13
15
|
onPlacement(placement: DevelopmentPlacement): Promise<void>;
|
|
14
16
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { DevelopmentContext, DevelopmentOverrides } from './context.js';
|
|
2
2
|
export type { DevelopmentInput } from './input.js';
|
|
3
|
-
export type { DevelopmentPlacement } from './placement.js';
|
|
3
|
+
export type { DevelopmentInstalledRelease, DevelopmentPlacement } from './placement.js';
|
|
4
4
|
export type { DevelopmentSession } from './session.js';
|
|
@@ -2,4 +2,14 @@
|
|
|
2
2
|
export interface DevelopmentPlacement {
|
|
3
3
|
readonly releaseUrl: string;
|
|
4
4
|
readonly localUrl?: string;
|
|
5
|
+
/** Exact Kernel evidence when this adapter owns installation. */
|
|
6
|
+
readonly installedRelease?: DevelopmentInstalledRelease;
|
|
7
|
+
}
|
|
8
|
+
/** Exact installed Release evidence supplied only by an installation-owning adapter. */
|
|
9
|
+
export interface DevelopmentInstalledRelease {
|
|
10
|
+
readonly origin: string;
|
|
11
|
+
readonly revision: string;
|
|
12
|
+
readonly issuer: string;
|
|
13
|
+
readonly etag: string;
|
|
14
|
+
readonly generation: string;
|
|
5
15
|
}
|
|
@@ -8,6 +8,7 @@ export interface ParsedArgs {
|
|
|
8
8
|
readonly format?: LintReportFormat;
|
|
9
9
|
readonly port?: number;
|
|
10
10
|
readonly host?: string;
|
|
11
|
+
readonly sessionFile?: string;
|
|
11
12
|
}
|
|
12
13
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
13
14
|
export declare function parseArgs(argv: readonly string[]): ParsedArgs;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isIP } from 'node:net';
|
|
2
|
+
import { isAbsolute, normalize } from 'node:path';
|
|
2
3
|
export const COMMANDS = ['dev', 'build', 'deploy', 'lint', 'package'];
|
|
3
4
|
/** Parse the frozen `astrale-domain` command grammar without performing effects. */
|
|
4
5
|
export function parseArgs(argv) {
|
|
@@ -6,6 +7,7 @@ export function parseArgs(argv) {
|
|
|
6
7
|
const fix = rest.includes('--fix');
|
|
7
8
|
let port;
|
|
8
9
|
let host;
|
|
10
|
+
let sessionFile;
|
|
9
11
|
let format;
|
|
10
12
|
let environment;
|
|
11
13
|
const cleaned = [];
|
|
@@ -30,6 +32,12 @@ export function parseArgs(argv) {
|
|
|
30
32
|
else if (argument.startsWith('--host=')) {
|
|
31
33
|
host = normalizeHost(argument.slice('--host='.length));
|
|
32
34
|
}
|
|
35
|
+
else if (argument === '--session-file') {
|
|
36
|
+
sessionFile = sessionFilePath(rest[++index]);
|
|
37
|
+
}
|
|
38
|
+
else if (argument.startsWith('--session-file=')) {
|
|
39
|
+
sessionFile = sessionFilePath(argument.slice('--session-file='.length));
|
|
40
|
+
}
|
|
33
41
|
else if (argument === '--format') {
|
|
34
42
|
format = lintFormat(rest[++index]);
|
|
35
43
|
}
|
|
@@ -57,8 +65,9 @@ export function parseArgs(argv) {
|
|
|
57
65
|
if (format !== undefined && command !== 'lint') {
|
|
58
66
|
throw new Error('`--format` is only valid for `lint`.');
|
|
59
67
|
}
|
|
60
|
-
if ((port !== undefined || host !== undefined
|
|
61
|
-
|
|
68
|
+
if ((port !== undefined || host !== undefined || sessionFile !== undefined) &&
|
|
69
|
+
command !== 'dev') {
|
|
70
|
+
throw new Error('`--port`, `--host`, and `--session-file` are only valid for `dev`.');
|
|
62
71
|
}
|
|
63
72
|
if (environment !== undefined && !['dev', 'deploy'].includes(command ?? '')) {
|
|
64
73
|
throw new Error('`--environment` is only valid for dev or deploy.');
|
|
@@ -76,6 +85,7 @@ export function parseArgs(argv) {
|
|
|
76
85
|
env: environment ?? positionals[0] ?? 'dev',
|
|
77
86
|
...(port !== undefined ? { port } : {}),
|
|
78
87
|
...(host !== undefined ? { host } : {}),
|
|
88
|
+
...(sessionFile !== undefined ? { sessionFile } : {}),
|
|
79
89
|
};
|
|
80
90
|
case 'deploy': {
|
|
81
91
|
if (positionals.length > 1)
|
|
@@ -165,3 +175,10 @@ function requiredValue(flag, input) {
|
|
|
165
175
|
throw new Error(`${flag} needs a value.`);
|
|
166
176
|
return input;
|
|
167
177
|
}
|
|
178
|
+
function sessionFilePath(input) {
|
|
179
|
+
const value = requiredValue('--session-file', input);
|
|
180
|
+
if (!isAbsolute(value) || normalize(value) !== value) {
|
|
181
|
+
throw new Error(`--session-file needs a normalized absolute path, got "${value}"`);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
@@ -3,15 +3,18 @@ import { mkdir, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { admitRuntime, compile, isDeployment, resolveRuntime, verifyPreflight, } from '../../../deployment/index.js';
|
|
6
|
+
import { addressing, assemble } from '../../../deployment/release/index.js';
|
|
6
7
|
import { loadDeclaredSecrets } from '../dotenv.js';
|
|
7
8
|
import { error, info } from '../log.js';
|
|
8
9
|
import { DevelopmentLifecycle } from './lifecycle.js';
|
|
9
10
|
import { acquireProjectLock } from './project-lock.js';
|
|
10
11
|
import { LatestRebuild } from './rebuild.js';
|
|
12
|
+
import { DevelopmentSessionFileError, DevelopmentSessionFileWriter } from './session-file.js';
|
|
11
13
|
import { watchSources } from './source-watch.js';
|
|
12
14
|
/** Run one complete provider-neutral development session until interruption. */
|
|
13
15
|
export async function develop(input) {
|
|
14
16
|
const lifecycle = new DevelopmentLifecycle();
|
|
17
|
+
let sessionFile;
|
|
15
18
|
let lock;
|
|
16
19
|
let session;
|
|
17
20
|
let sourceWatch;
|
|
@@ -20,6 +23,9 @@ export async function develop(input) {
|
|
|
20
23
|
let failed = false;
|
|
21
24
|
let watcherFailure;
|
|
22
25
|
try {
|
|
26
|
+
if (input.sessionFile !== undefined) {
|
|
27
|
+
sessionFile = await DevelopmentSessionFileWriter.create(input.sessionFile);
|
|
28
|
+
}
|
|
23
29
|
lock = await acquireProjectLock({
|
|
24
30
|
projectRoot: input.projectDir,
|
|
25
31
|
environment: input.environment,
|
|
@@ -27,6 +33,16 @@ export async function develop(input) {
|
|
|
27
33
|
const runtimePath = await discoverRuntimePath(input.configPath);
|
|
28
34
|
const builder = new ProjectBuilder({ ...input, runtimePath });
|
|
29
35
|
let current = await builder.prepare(lifecycle.signal);
|
|
36
|
+
let sessionEvidence;
|
|
37
|
+
let placementReady = false;
|
|
38
|
+
let rebuildActive = false;
|
|
39
|
+
let updatePrevious;
|
|
40
|
+
let placementSequence = 0;
|
|
41
|
+
const publishSessionPhase = async () => {
|
|
42
|
+
if (sessionFile === undefined || sessionEvidence === undefined)
|
|
43
|
+
return;
|
|
44
|
+
await sessionFile.transition(placementReady ? (rebuildActive ? 'updating' : 'ready') : 'recovering', sessionEvidence);
|
|
45
|
+
};
|
|
30
46
|
const adapter = current.adapter;
|
|
31
47
|
const parameters = current.parameters;
|
|
32
48
|
let restartReported = false;
|
|
@@ -38,14 +54,28 @@ export async function develop(input) {
|
|
|
38
54
|
secrets: current.secrets,
|
|
39
55
|
overrides: input.overrides,
|
|
40
56
|
signal: lifecycle.signal,
|
|
57
|
+
async onRecovering() {
|
|
58
|
+
placementReady = false;
|
|
59
|
+
await publishSessionPhase();
|
|
60
|
+
},
|
|
41
61
|
async onPlacement(placement) {
|
|
62
|
+
if (sessionFile !== undefined) {
|
|
63
|
+
sessionEvidence = coherentSessionEvidence(updatePrevious === undefined ? [current.artifact] : [current.artifact, updatePrevious], placement);
|
|
64
|
+
placementReady = true;
|
|
65
|
+
placementSequence += 1;
|
|
66
|
+
await publishSessionPhase();
|
|
67
|
+
}
|
|
42
68
|
info(`Public: ${placement.releaseUrl}`);
|
|
43
69
|
if (placement.localUrl !== undefined)
|
|
44
70
|
info(`Runtime: ${placement.localUrl}`);
|
|
45
71
|
},
|
|
46
72
|
});
|
|
47
73
|
rebuild = new LatestRebuild({
|
|
48
|
-
prepare
|
|
74
|
+
async prepare(_ignored, signal) {
|
|
75
|
+
rebuildActive = true;
|
|
76
|
+
await publishSessionPhase();
|
|
77
|
+
return builder.prepare(signal);
|
|
78
|
+
},
|
|
49
79
|
async commit(candidate) {
|
|
50
80
|
if (candidate.adapter.name !== adapter.name ||
|
|
51
81
|
candidate.adapter.version !== adapter.version ||
|
|
@@ -54,19 +84,50 @@ export async function develop(input) {
|
|
|
54
84
|
throw new Error('Adapter configuration changed; restart pnpm dev to apply it.');
|
|
55
85
|
}
|
|
56
86
|
const previous = current;
|
|
87
|
+
const beforePlacement = placementSequence;
|
|
88
|
+
updatePrevious = previous.artifact;
|
|
57
89
|
current = candidate;
|
|
58
90
|
try {
|
|
59
91
|
await session.update({ artifact: candidate.artifact, secrets: candidate.secrets });
|
|
92
|
+
if (sessionFile !== undefined) {
|
|
93
|
+
if (placementSequence === beforePlacement || sessionEvidence === undefined) {
|
|
94
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter completed an update without reporting installed Release evidence.');
|
|
95
|
+
}
|
|
96
|
+
assertEvidenceMatches(candidate.artifact, sessionEvidence);
|
|
97
|
+
}
|
|
98
|
+
rebuildActive = false;
|
|
99
|
+
await publishSessionPhase();
|
|
60
100
|
sourceWatch?.update(candidate.sources);
|
|
61
101
|
info(`Updated ${candidate.artifact.build.schema.compiled.root.revision}.`);
|
|
62
102
|
}
|
|
63
103
|
catch (cause) {
|
|
64
104
|
current = previous;
|
|
105
|
+
if (sessionFile !== undefined &&
|
|
106
|
+
sessionEvidence !== undefined &&
|
|
107
|
+
!evidenceMatches(previous.artifact, sessionEvidence)) {
|
|
108
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter rejected an update without restoring prior installed Release evidence.', { cause });
|
|
109
|
+
}
|
|
65
110
|
throw cause;
|
|
66
111
|
}
|
|
112
|
+
finally {
|
|
113
|
+
updatePrevious = undefined;
|
|
114
|
+
}
|
|
67
115
|
},
|
|
68
|
-
failed(cause) {
|
|
116
|
+
async failed(cause) {
|
|
117
|
+
rebuildActive = false;
|
|
118
|
+
if (cause instanceof DevelopmentSessionFileError) {
|
|
119
|
+
watcherFailure = cause;
|
|
120
|
+
lifecycle.stop(cause);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
69
123
|
error(cause instanceof Error ? cause.message : String(cause));
|
|
124
|
+
try {
|
|
125
|
+
await publishSessionPhase();
|
|
126
|
+
}
|
|
127
|
+
catch (sessionCause) {
|
|
128
|
+
watcherFailure = sessionCause;
|
|
129
|
+
lifecycle.stop(sessionCause);
|
|
130
|
+
}
|
|
70
131
|
},
|
|
71
132
|
});
|
|
72
133
|
sourceWatch = watchSources({
|
|
@@ -103,6 +164,13 @@ export async function develop(input) {
|
|
|
103
164
|
}
|
|
104
165
|
lifecycle.stop(failure);
|
|
105
166
|
const cleanup = [];
|
|
167
|
+
try {
|
|
168
|
+
await sessionFile?.transition('stopping');
|
|
169
|
+
}
|
|
170
|
+
catch (cause) {
|
|
171
|
+
failed = true;
|
|
172
|
+
failure ??= cause;
|
|
173
|
+
}
|
|
106
174
|
for (const operation of [
|
|
107
175
|
() => sourceWatch?.stop(),
|
|
108
176
|
() => rebuild?.stop(failure),
|
|
@@ -133,6 +201,12 @@ export async function develop(input) {
|
|
|
133
201
|
cleanup.push(cause);
|
|
134
202
|
}
|
|
135
203
|
}
|
|
204
|
+
try {
|
|
205
|
+
await sessionFile?.transition(failed || cleanup.length > 0 ? 'failed' : 'stopped');
|
|
206
|
+
}
|
|
207
|
+
catch (cause) {
|
|
208
|
+
cleanup.push(cause);
|
|
209
|
+
}
|
|
136
210
|
lifecycle.dispose();
|
|
137
211
|
if (failed) {
|
|
138
212
|
if (cleanup.length > 0)
|
|
@@ -143,6 +217,45 @@ export async function develop(input) {
|
|
|
143
217
|
throw new AggregateError(cleanup, 'Development cleanup failed.');
|
|
144
218
|
return 0;
|
|
145
219
|
}
|
|
220
|
+
function coherentSessionEvidence(artifacts, placement) {
|
|
221
|
+
const evidence = placement.installedRelease;
|
|
222
|
+
if (evidence === undefined) {
|
|
223
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', '--session-file requires an adapter that reports exact installed Release evidence.');
|
|
224
|
+
}
|
|
225
|
+
let issuer;
|
|
226
|
+
try {
|
|
227
|
+
issuer = new URL(placement.releaseUrl).origin;
|
|
228
|
+
}
|
|
229
|
+
catch (cause) {
|
|
230
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter reported an invalid Release URL.', { cause });
|
|
231
|
+
}
|
|
232
|
+
if (placement.releaseUrl !== issuer) {
|
|
233
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter reported a non-canonical Release URL.');
|
|
234
|
+
}
|
|
235
|
+
let matches = false;
|
|
236
|
+
try {
|
|
237
|
+
matches = artifacts.some((artifact) => evidenceMatches(artifact, evidence));
|
|
238
|
+
}
|
|
239
|
+
catch (cause) {
|
|
240
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter reported invalid installed Release evidence.', { cause });
|
|
241
|
+
}
|
|
242
|
+
if (evidence.issuer !== issuer || !matches) {
|
|
243
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter installed Release evidence does not match the current artifact and placement.');
|
|
244
|
+
}
|
|
245
|
+
return evidence;
|
|
246
|
+
}
|
|
247
|
+
function assertEvidenceMatches(artifact, evidence) {
|
|
248
|
+
if (!evidenceMatches(artifact, evidence)) {
|
|
249
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EVIDENCE_INCOHERENT', 'Development adapter installed Release evidence does not match the committed artifact.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function evidenceMatches(artifact, evidence) {
|
|
253
|
+
const root = artifact.build.schema.compiled.root;
|
|
254
|
+
const release = assemble(artifact.build, addressing(evidence.issuer));
|
|
255
|
+
return (evidence.origin === root.origin &&
|
|
256
|
+
evidence.revision === root.revision &&
|
|
257
|
+
evidence.etag === release.publication.etag);
|
|
258
|
+
}
|
|
146
259
|
class ProjectBuilder {
|
|
147
260
|
#configPath;
|
|
148
261
|
#projectDir;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface RebuildOperations<Input, Candidate> {
|
|
2
2
|
prepare(input: Input, signal: AbortSignal): Promise<Candidate>;
|
|
3
3
|
commit(candidate: Candidate, signal: AbortSignal): Promise<void>;
|
|
4
|
-
failed(cause: unknown): void
|
|
4
|
+
failed(cause: unknown): void | Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
/** Serialize rebuilds while retaining only the newest pending invalidation. */
|
|
7
7
|
export declare class LatestRebuild<Input, Candidate> {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const DEVELOPMENT_SESSION_FILE_VERSION: 1;
|
|
2
|
+
export type DevelopmentSessionPhaseV1 = 'starting' | 'ready' | 'updating' | 'recovering' | 'stopping' | 'stopped' | 'failed';
|
|
3
|
+
export interface DevelopmentSessionProjectV1 {
|
|
4
|
+
readonly origin: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DevelopmentSessionReleaseV1 {
|
|
7
|
+
readonly revision: string;
|
|
8
|
+
readonly issuer: string;
|
|
9
|
+
readonly etag: string;
|
|
10
|
+
readonly generation: string;
|
|
11
|
+
}
|
|
12
|
+
interface DevelopmentSessionFileBaseV1 {
|
|
13
|
+
readonly version: typeof DEVELOPMENT_SESSION_FILE_VERSION;
|
|
14
|
+
readonly sessionId: string;
|
|
15
|
+
readonly sequence: number;
|
|
16
|
+
readonly phase: DevelopmentSessionPhaseV1;
|
|
17
|
+
}
|
|
18
|
+
interface DevelopmentSessionStartingFileV1 extends DevelopmentSessionFileBaseV1 {
|
|
19
|
+
readonly phase: 'starting';
|
|
20
|
+
}
|
|
21
|
+
interface DevelopmentSessionActiveFileV1 extends DevelopmentSessionFileBaseV1 {
|
|
22
|
+
readonly phase: 'ready' | 'updating' | 'recovering';
|
|
23
|
+
readonly project: DevelopmentSessionProjectV1;
|
|
24
|
+
readonly release: DevelopmentSessionReleaseV1;
|
|
25
|
+
}
|
|
26
|
+
interface DevelopmentSessionTerminalFileBaseV1 extends DevelopmentSessionFileBaseV1 {
|
|
27
|
+
readonly phase: 'stopping' | 'stopped' | 'failed';
|
|
28
|
+
}
|
|
29
|
+
type DevelopmentSessionTerminalFileV1 = DevelopmentSessionTerminalFileBaseV1 & ({
|
|
30
|
+
readonly project: DevelopmentSessionProjectV1;
|
|
31
|
+
readonly release: DevelopmentSessionReleaseV1;
|
|
32
|
+
} | {
|
|
33
|
+
readonly project?: never;
|
|
34
|
+
readonly release?: never;
|
|
35
|
+
});
|
|
36
|
+
export type DevelopmentSessionFileV1 = DevelopmentSessionStartingFileV1 | DevelopmentSessionActiveFileV1 | DevelopmentSessionTerminalFileV1;
|
|
37
|
+
export interface DevelopmentSessionReleaseEvidenceV1 extends DevelopmentSessionProjectV1, DevelopmentSessionReleaseV1 {
|
|
38
|
+
}
|
|
39
|
+
export declare class DevelopmentSessionFileError extends Error {
|
|
40
|
+
readonly code: 'INVALID_SESSION_FILE_PATH' | 'SESSION_FILE_PATH_UNSAFE' | 'SESSION_FILE_EXISTS' | 'SESSION_FILE_TAMPERED' | 'SESSION_FILE_TRANSITION_INVALID' | 'SESSION_FILE_EVIDENCE_INCOHERENT';
|
|
41
|
+
readonly name = "DevelopmentSessionFileError";
|
|
42
|
+
constructor(code: 'INVALID_SESSION_FILE_PATH' | 'SESSION_FILE_PATH_UNSAFE' | 'SESSION_FILE_EXISTS' | 'SESSION_FILE_TAMPERED' | 'SESSION_FILE_TRANSITION_INVALID' | 'SESSION_FILE_EVIDENCE_INCOHERENT', message: string, options?: ErrorOptions);
|
|
43
|
+
}
|
|
44
|
+
export declare function acceptDevelopmentSessionFileV1(input: unknown): DevelopmentSessionFileV1;
|
|
45
|
+
/** SDK-private sole writer for one caller-selected machine-integration file. */
|
|
46
|
+
export declare class DevelopmentSessionFileWriter {
|
|
47
|
+
#private;
|
|
48
|
+
private constructor();
|
|
49
|
+
static create(path: string): Promise<DevelopmentSessionFileWriter>;
|
|
50
|
+
transition(phase: Exclude<DevelopmentSessionPhaseV1, 'starting'>, evidence?: DevelopmentSessionReleaseEvidenceV1): Promise<DevelopmentSessionFileV1>;
|
|
51
|
+
}
|
|
52
|
+
export declare function developmentSessionTransitionAllowed(previous: DevelopmentSessionPhaseV1, next: Exclude<DevelopmentSessionPhaseV1, 'starting'>): boolean;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, lstat, open, readFile, realpath, rename, unlink } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, normalize, parse, sep } from 'node:path';
|
|
4
|
+
import { patterns } from '../../../platform/schema/index.js';
|
|
5
|
+
export const DEVELOPMENT_SESSION_FILE_VERSION = 1;
|
|
6
|
+
export class DevelopmentSessionFileError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
name = 'DevelopmentSessionFileError';
|
|
9
|
+
constructor(code, message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function acceptDevelopmentSessionFileV1(input) {
|
|
15
|
+
const value = record(input, 'Development session file');
|
|
16
|
+
if (value.metadata !== undefined ||
|
|
17
|
+
value.extensions !== undefined ||
|
|
18
|
+
value.vendor !== undefined) {
|
|
19
|
+
invalid('Development session file cannot contain a metadata, extensions, or vendor bag.');
|
|
20
|
+
}
|
|
21
|
+
if (value.version !== DEVELOPMENT_SESSION_FILE_VERSION) {
|
|
22
|
+
invalid(`Unsupported development session file version: ${String(value.version)}.`);
|
|
23
|
+
}
|
|
24
|
+
const sessionId = text(value.sessionId, 'Development session id');
|
|
25
|
+
const sequence = integer(value.sequence, 'Development session sequence');
|
|
26
|
+
const phase = admitPhase(value.phase);
|
|
27
|
+
const base = Object.freeze({ version: 1, sessionId, sequence, phase });
|
|
28
|
+
const hasProject = value.project !== undefined;
|
|
29
|
+
const hasRelease = value.release !== undefined;
|
|
30
|
+
if (phase === 'starting') {
|
|
31
|
+
if (hasProject || hasRelease) {
|
|
32
|
+
invalid('Development session starting phase cannot contain Release evidence.');
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze({ ...base, phase: 'starting' });
|
|
35
|
+
}
|
|
36
|
+
if (phase === 'ready' || phase === 'updating' || phase === 'recovering') {
|
|
37
|
+
if (!hasProject || !hasRelease)
|
|
38
|
+
invalid(`Development session phase ${phase} requires a Release.`);
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
...base,
|
|
41
|
+
phase,
|
|
42
|
+
project: project(value.project),
|
|
43
|
+
release: release(value.release),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (hasProject !== hasRelease) {
|
|
47
|
+
invalid('Development session terminal project and Release must be present together.');
|
|
48
|
+
}
|
|
49
|
+
if (!hasProject)
|
|
50
|
+
return Object.freeze({ ...base, phase });
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
...base,
|
|
53
|
+
phase,
|
|
54
|
+
project: project(value.project),
|
|
55
|
+
release: release(value.release),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** SDK-private sole writer for one caller-selected machine-integration file. */
|
|
59
|
+
export class DevelopmentSessionFileWriter {
|
|
60
|
+
#path;
|
|
61
|
+
#parent;
|
|
62
|
+
#sessionId;
|
|
63
|
+
#sequence = 0;
|
|
64
|
+
#phase = 'starting';
|
|
65
|
+
#last;
|
|
66
|
+
#tail = Promise.resolve();
|
|
67
|
+
constructor(path, parent, sessionId) {
|
|
68
|
+
this.#path = path;
|
|
69
|
+
this.#parent = parent;
|
|
70
|
+
this.#sessionId = sessionId;
|
|
71
|
+
}
|
|
72
|
+
static async create(path) {
|
|
73
|
+
const admitted = await admitSessionFilePath(path);
|
|
74
|
+
const writer = new DevelopmentSessionFileWriter(admitted.path, admitted.parent, randomUUID());
|
|
75
|
+
await writer.#create();
|
|
76
|
+
return writer;
|
|
77
|
+
}
|
|
78
|
+
transition(phase, evidence) {
|
|
79
|
+
const operation = this.#tail.then(async () => {
|
|
80
|
+
if (!developmentSessionTransitionAllowed(this.#phase, phase)) {
|
|
81
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_TRANSITION_INVALID', `Development session cannot transition from ${this.#phase} to ${phase}.`);
|
|
82
|
+
}
|
|
83
|
+
if (evidence !== undefined)
|
|
84
|
+
this.#last = admitEvidence(evidence);
|
|
85
|
+
if (['ready', 'updating', 'recovering'].includes(phase) && this.#last === undefined) {
|
|
86
|
+
throw new TypeError(`Development session phase ${phase} requires Release evidence.`);
|
|
87
|
+
}
|
|
88
|
+
const snapshot = this.#snapshot(phase, this.#sequence + 1);
|
|
89
|
+
await this.#replace(snapshot);
|
|
90
|
+
this.#sequence = snapshot.sequence;
|
|
91
|
+
this.#phase = snapshot.phase;
|
|
92
|
+
return snapshot;
|
|
93
|
+
});
|
|
94
|
+
this.#tail = operation.then(() => undefined, () => undefined);
|
|
95
|
+
return operation;
|
|
96
|
+
}
|
|
97
|
+
async #create() {
|
|
98
|
+
const snapshot = this.#snapshot('starting', 1);
|
|
99
|
+
const staging = await writeStaging(this.#parent, this.#path, this.#sessionId, snapshot);
|
|
100
|
+
try {
|
|
101
|
+
await link(staging, this.#path);
|
|
102
|
+
}
|
|
103
|
+
catch (cause) {
|
|
104
|
+
if (errorCode(cause) === 'EEXIST') {
|
|
105
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EXISTS', `Development session file already exists: ${this.#path}`, { cause });
|
|
106
|
+
}
|
|
107
|
+
throw cause;
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await unlink(staging).catch((cause) => {
|
|
111
|
+
if (errorCode(cause) !== 'ENOENT')
|
|
112
|
+
throw cause;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
await syncDirectory(this.#parent);
|
|
116
|
+
this.#sequence = snapshot.sequence;
|
|
117
|
+
}
|
|
118
|
+
#snapshot(phase, sequence) {
|
|
119
|
+
const base = {
|
|
120
|
+
version: DEVELOPMENT_SESSION_FILE_VERSION,
|
|
121
|
+
sessionId: this.#sessionId,
|
|
122
|
+
sequence,
|
|
123
|
+
};
|
|
124
|
+
if (phase === 'starting')
|
|
125
|
+
return Object.freeze({ ...base, phase: 'starting' });
|
|
126
|
+
if (this.#last === undefined) {
|
|
127
|
+
if (phase === 'ready' || phase === 'updating' || phase === 'recovering') {
|
|
128
|
+
throw new TypeError(`Development session phase ${phase} requires Release evidence.`);
|
|
129
|
+
}
|
|
130
|
+
return Object.freeze({ ...base, phase });
|
|
131
|
+
}
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
...base,
|
|
134
|
+
phase,
|
|
135
|
+
project: Object.freeze({ origin: this.#last.origin }),
|
|
136
|
+
release: Object.freeze({
|
|
137
|
+
revision: this.#last.revision,
|
|
138
|
+
issuer: this.#last.issuer,
|
|
139
|
+
etag: this.#last.etag,
|
|
140
|
+
generation: this.#last.generation,
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async #replace(snapshot) {
|
|
145
|
+
await assertParent(this.#parent);
|
|
146
|
+
const current = await readCurrent(this.#path);
|
|
147
|
+
if (current.sessionId !== this.#sessionId || current.sequence !== snapshot.sequence - 1) {
|
|
148
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_TAMPERED', `Development session file ownership changed: ${this.#path}`);
|
|
149
|
+
}
|
|
150
|
+
const staging = await writeStaging(this.#parent, this.#path, this.#sessionId, snapshot);
|
|
151
|
+
try {
|
|
152
|
+
await rename(staging, this.#path);
|
|
153
|
+
await syncDirectory(this.#parent);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await unlink(staging).catch((cause) => {
|
|
157
|
+
if (errorCode(cause) !== 'ENOENT')
|
|
158
|
+
throw cause;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function admitEvidence(input) {
|
|
164
|
+
const admittedProject = project({ origin: input.origin });
|
|
165
|
+
const admittedRelease = release(input);
|
|
166
|
+
return Object.freeze({ origin: admittedProject.origin, ...admittedRelease });
|
|
167
|
+
}
|
|
168
|
+
async function admitSessionFilePath(input) {
|
|
169
|
+
if (!isAbsolute(input) || normalize(input) !== input || basename(input).length === 0) {
|
|
170
|
+
throw new DevelopmentSessionFileError('INVALID_SESSION_FILE_PATH', '--session-file requires one normalized absolute file path.');
|
|
171
|
+
}
|
|
172
|
+
const parent = dirname(input);
|
|
173
|
+
await assertParent(parent);
|
|
174
|
+
try {
|
|
175
|
+
await lstat(input);
|
|
176
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_EXISTS', `Development session file path must not exist: ${input}`);
|
|
177
|
+
}
|
|
178
|
+
catch (cause) {
|
|
179
|
+
if (errorCode(cause) !== 'ENOENT')
|
|
180
|
+
throw cause;
|
|
181
|
+
}
|
|
182
|
+
return Object.freeze({ path: input, parent });
|
|
183
|
+
}
|
|
184
|
+
async function assertParent(parent) {
|
|
185
|
+
const root = parse(parent).root;
|
|
186
|
+
let cursor = root;
|
|
187
|
+
for (const segment of parent.slice(root.length).split(sep).filter(Boolean)) {
|
|
188
|
+
cursor = join(cursor, segment);
|
|
189
|
+
const entry = await lstat(cursor);
|
|
190
|
+
if (entry.isSymbolicLink())
|
|
191
|
+
unsafe(`Development session file path crosses a symlink: ${cursor}`);
|
|
192
|
+
}
|
|
193
|
+
const canonical = await realpath(parent);
|
|
194
|
+
if (canonical !== parent)
|
|
195
|
+
unsafe(`Development session file parent is not canonical: ${parent}`);
|
|
196
|
+
const entry = await lstat(parent);
|
|
197
|
+
if (!entry.isDirectory())
|
|
198
|
+
unsafe(`Development session file parent is not a directory: ${parent}`);
|
|
199
|
+
const uid = process.getuid?.();
|
|
200
|
+
if (uid !== undefined && entry.uid !== uid) {
|
|
201
|
+
unsafe(`Development session file parent is not owned by the current user: ${parent}`);
|
|
202
|
+
}
|
|
203
|
+
if ((entry.mode & 0o077) !== 0) {
|
|
204
|
+
unsafe(`Development session file parent must be owner-only: ${parent}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function writeStaging(parent, target, sessionId, snapshot) {
|
|
208
|
+
const path = join(parent, `.${basename(target)}.${sessionId}.${randomUUID()}.next`);
|
|
209
|
+
const file = await open(path, 'wx', 0o600);
|
|
210
|
+
try {
|
|
211
|
+
await file.writeFile(`${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
|
212
|
+
await file.sync();
|
|
213
|
+
}
|
|
214
|
+
catch (cause) {
|
|
215
|
+
await file.close();
|
|
216
|
+
await unlink(path).catch(() => undefined);
|
|
217
|
+
throw cause;
|
|
218
|
+
}
|
|
219
|
+
await file.close();
|
|
220
|
+
return path;
|
|
221
|
+
}
|
|
222
|
+
async function syncDirectory(path) {
|
|
223
|
+
const directory = await open(path, 'r');
|
|
224
|
+
try {
|
|
225
|
+
await directory.sync();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await directory.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function readCurrent(path) {
|
|
232
|
+
const entry = await lstat(path);
|
|
233
|
+
if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o077) !== 0) {
|
|
234
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_TAMPERED', `Development session file is no longer one owner-only regular file: ${path}`);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
return acceptDevelopmentSessionFileV1(JSON.parse(await readFile(path, 'utf8')));
|
|
238
|
+
}
|
|
239
|
+
catch (cause) {
|
|
240
|
+
if (cause instanceof DevelopmentSessionFileError)
|
|
241
|
+
throw cause;
|
|
242
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_TAMPERED', `Development session file is unreadable or invalid: ${path}`, { cause });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function project(input) {
|
|
246
|
+
const value = record(input, 'Development session project');
|
|
247
|
+
rejectBags(value, 'Development session project');
|
|
248
|
+
const origin = text(value.origin, 'Development session project origin');
|
|
249
|
+
if (!patterns.origin.test(origin))
|
|
250
|
+
invalid('Development session project origin is invalid.');
|
|
251
|
+
return Object.freeze({ origin });
|
|
252
|
+
}
|
|
253
|
+
function release(input) {
|
|
254
|
+
const value = record(input, 'Development session Release');
|
|
255
|
+
rejectBags(value, 'Development session Release');
|
|
256
|
+
const revision = digest(value.revision, 'Development session Release revision');
|
|
257
|
+
const etag = digest(value.etag, 'Development session Release etag');
|
|
258
|
+
const generation = digest(value.generation, 'Development session Release generation');
|
|
259
|
+
const issuer = origin(value.issuer);
|
|
260
|
+
return Object.freeze({ revision, issuer, etag, generation });
|
|
261
|
+
}
|
|
262
|
+
function origin(input) {
|
|
263
|
+
const value = text(input, 'Development session Release issuer');
|
|
264
|
+
let url;
|
|
265
|
+
try {
|
|
266
|
+
url = new URL(value);
|
|
267
|
+
}
|
|
268
|
+
catch (cause) {
|
|
269
|
+
throw new TypeError('Development session Release issuer must be an HTTP(S) origin.', { cause });
|
|
270
|
+
}
|
|
271
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.href !== `${url.origin}/`) {
|
|
272
|
+
invalid('Development session Release issuer must be an HTTP(S) origin.');
|
|
273
|
+
}
|
|
274
|
+
return url.origin;
|
|
275
|
+
}
|
|
276
|
+
function digest(input, label) {
|
|
277
|
+
const value = text(input, label);
|
|
278
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(value))
|
|
279
|
+
invalid(`${label} must be a sha256 digest.`);
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
function admitPhase(input) {
|
|
283
|
+
if (input !== 'starting' &&
|
|
284
|
+
input !== 'ready' &&
|
|
285
|
+
input !== 'updating' &&
|
|
286
|
+
input !== 'recovering' &&
|
|
287
|
+
input !== 'stopping' &&
|
|
288
|
+
input !== 'stopped' &&
|
|
289
|
+
input !== 'failed') {
|
|
290
|
+
invalid(`Unknown development session phase: ${String(input)}.`);
|
|
291
|
+
}
|
|
292
|
+
return input;
|
|
293
|
+
}
|
|
294
|
+
export function developmentSessionTransitionAllowed(previous, next) {
|
|
295
|
+
if (previous === 'stopped' || previous === 'failed')
|
|
296
|
+
return false;
|
|
297
|
+
if (previous === 'stopping')
|
|
298
|
+
return next === 'stopped' || next === 'failed';
|
|
299
|
+
if (next === 'stopping' || next === 'failed')
|
|
300
|
+
return true;
|
|
301
|
+
if (previous === 'starting')
|
|
302
|
+
return next === 'ready';
|
|
303
|
+
return next === 'ready' || next === 'updating' || next === 'recovering';
|
|
304
|
+
}
|
|
305
|
+
function rejectBags(value, label) {
|
|
306
|
+
if (value.metadata !== undefined ||
|
|
307
|
+
value.extensions !== undefined ||
|
|
308
|
+
value.vendor !== undefined) {
|
|
309
|
+
invalid(`${label} cannot contain a metadata, extensions, or vendor bag.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function record(input, label) {
|
|
313
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
314
|
+
invalid(`${label} must be an object.`);
|
|
315
|
+
return input;
|
|
316
|
+
}
|
|
317
|
+
function text(input, label) {
|
|
318
|
+
if (typeof input !== 'string' || input.length === 0)
|
|
319
|
+
invalid(`${label} must be text.`);
|
|
320
|
+
return input;
|
|
321
|
+
}
|
|
322
|
+
function integer(input, label) {
|
|
323
|
+
if (!Number.isSafeInteger(input) || Number(input) <= 0)
|
|
324
|
+
invalid(`${label} must be a positive integer.`);
|
|
325
|
+
return Number(input);
|
|
326
|
+
}
|
|
327
|
+
function invalid(message) {
|
|
328
|
+
throw new TypeError(message);
|
|
329
|
+
}
|
|
330
|
+
function unsafe(message) {
|
|
331
|
+
throw new DevelopmentSessionFileError('SESSION_FILE_PATH_UNSAFE', message);
|
|
332
|
+
}
|
|
333
|
+
function errorCode(input) {
|
|
334
|
+
return input !== null && typeof input === 'object' && 'code' in input
|
|
335
|
+
? String(Reflect.get(input, 'code'))
|
|
336
|
+
: undefined;
|
|
337
|
+
}
|
package/dist/tooling/cli/help.js
CHANGED
|
@@ -43,6 +43,8 @@ Options:
|
|
|
43
43
|
--port <port> Use a strict local Worker port (default: OS-allocated).
|
|
44
44
|
--host <url> Use an externally managed public origin; requires a strict port
|
|
45
45
|
from --port or adapter configuration.
|
|
46
|
+
--session-file <path> Write one retained DevelopmentSessionFileV1 snapshot at an
|
|
47
|
+
absolute path in a caller-owned private directory.
|
|
46
48
|
-h, --help Show help for dev.
|
|
47
49
|
|
|
48
50
|
Behavior:
|
|
@@ -51,6 +53,9 @@ Behavior:
|
|
|
51
53
|
OS-allocated loopback port.
|
|
52
54
|
Watches imported authored sources and declared secrets. Configuration changes
|
|
53
55
|
require restarting the command.
|
|
56
|
+
The session file is machine-readable and authoritative when requested; human
|
|
57
|
+
stdout/stderr remains diagnostic. It requires an adapter that reports exact
|
|
58
|
+
installed Release evidence.
|
|
54
59
|
|
|
55
60
|
Adapters:
|
|
56
61
|
cloudflare Runs the local Worker and optional Vite frontend only;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@astrale-os/sdk/cli` — the `astrale-domain` CLI
|
|
3
|
-
* (dev | build | deploy | lint | package)
|
|
3
|
+
* (dev | build | deploy | lint | package), explicit dotenv boundary, and admitted
|
|
4
|
+
* development-session file representation.
|
|
4
5
|
*
|
|
5
6
|
* Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
|
|
6
7
|
* Bun (it imports the project's `astrale.config.ts` directly). This subpath is
|
|
@@ -11,3 +12,5 @@
|
|
|
11
12
|
*/
|
|
12
13
|
export { run } from './run.js';
|
|
13
14
|
export { loadDeclaredSecrets } from './dotenv.js';
|
|
15
|
+
export { DEVELOPMENT_SESSION_FILE_VERSION, acceptDevelopmentSessionFileV1, } from './development/session-file.js';
|
|
16
|
+
export type { DevelopmentSessionFileV1, DevelopmentSessionPhaseV1, DevelopmentSessionProjectV1, DevelopmentSessionReleaseV1, } from './development/session-file.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@astrale-os/sdk/cli` — the `astrale-domain` CLI
|
|
3
|
-
* (dev | build | deploy | lint | package)
|
|
3
|
+
* (dev | build | deploy | lint | package), explicit dotenv boundary, and admitted
|
|
4
|
+
* development-session file representation.
|
|
4
5
|
*
|
|
5
6
|
* Node-only: the CLI imports `node:fs`/`node:module`/`node:url` and runs under
|
|
6
7
|
* Bun (it imports the project's `astrale.config.ts` directly). This subpath is
|
|
@@ -11,3 +12,4 @@
|
|
|
11
12
|
*/
|
|
12
13
|
export { run } from './run.js';
|
|
13
14
|
export { loadDeclaredSecrets } from './dotenv.js';
|
|
15
|
+
export { DEVELOPMENT_SESSION_FILE_VERSION, acceptDevelopmentSessionFileV1, } from './development/session-file.js';
|
|
@@ -49,6 +49,7 @@ export async function run(argv) {
|
|
|
49
49
|
...(parsed.host === undefined ? {} : { publicUrl: parsed.host }),
|
|
50
50
|
...(parsed.port === undefined ? {} : { localPort: parsed.port }),
|
|
51
51
|
}),
|
|
52
|
+
...(parsed.sessionFile === undefined ? {} : { sessionFile: parsed.sessionFile }),
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
const deployment = await loadDeployment(configPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/sdk",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.81",
|
|
4
4
|
"description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -287,7 +287,7 @@
|
|
|
287
287
|
"build": "node scripts/clean-dist.mjs && tsgo && node scripts/verify-no-source-maps.mjs dist && pnpm run verify:package",
|
|
288
288
|
"e2e": "node __e2e__/flagship/runner.mjs",
|
|
289
289
|
"typecheck": "tsgo --noEmit && pnpm run typecheck:specs && pnpm run typecheck:types && pnpm run typecheck:tests && pnpm run typecheck:e2e && pnpm run typecheck:knowledge",
|
|
290
|
-
"typecheck:specs": "tsgo --noEmit -p src/application/query/.spec/tsconfig.json && tsgo --noEmit -p src/platform/schema/.spec/tsconfig.json && tsgo --noEmit -p src/application/mutation/.spec/tsconfig.json",
|
|
290
|
+
"typecheck:specs": "tsgo --noEmit -p src/application/query/.spec/tsconfig.json && tsgo --noEmit -p src/platform/schema/.spec/tsconfig.json && tsgo --noEmit -p src/application/mutation/.spec/tsconfig.json && tsgo --noEmit -p src/deployment/adapter/development/.spec/tsconfig.json && tsgo --noEmit -p src/tooling/cli/.spec/tsconfig.json",
|
|
291
291
|
"typecheck:types": "tsgo --noEmit -p __tests__/tsconfig.types.json",
|
|
292
292
|
"typecheck:tests": "tsgo --noEmit -p tsconfig.test.json",
|
|
293
293
|
"typecheck:e2e": "tsgo --noEmit -p __e2e__/flagship/fixtures/tsconfig.v1.json && tsgo --noEmit -p __e2e__/flagship/fixtures/tsconfig.json",
|