@astrale-os/sdk 0.5.0-beta.132 → 0.5.0-beta.134

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/README.md CHANGED
@@ -31,6 +31,21 @@ npx -y create-astrale-domain@beta my-domain --yes
31
31
  See [the release lifecycle](docs/release.md) for package-set versioning, verification, and stable
32
32
  promotion.
33
33
 
34
+ ### Update a pnpm project
35
+
36
+ ```sh
37
+ pnpm exec astrale-domain upgrade --check # Preview; no project changes
38
+ pnpm exec astrale-domain upgrade # Update existing Astrale dependencies to beta
39
+ ```
40
+
41
+ The command covers the workspace root and its packages, pins exact versions, and updates the
42
+ lockfile through pnpm. Use `--channel latest` to select that tag instead. Missing tags and local,
43
+ catalog, or aliased dependencies are left unchanged and reported; peers and overrides are not edited.
44
+ For an older SDK without this command, start with `pnpm dlx @astrale-os/sdk@beta upgrade`.
45
+ Stop development watchers first, then run the project's checks and validate on development.
46
+ Nothing is committed or deployed. A Shell update also requires checking the installed Shell revision.
47
+ Rerunning an upgrade synchronizes installation even when the manifest versions are already current.
48
+
34
49
  ## Handler authority and dependencies
35
50
 
36
51
  Authenticated handlers use Domain self authority for `kernel`, `graph`, `query`, and `mutate`.
@@ -110,10 +125,10 @@ The repository installs autonomously from its versioned npm-only lockfile:
110
125
  ```bash
111
126
  git clone https://github.com/astrale-os/sdk.git
112
127
  cd sdk
113
- AGENT_HARNESSES=codex bash scripts/agent_setup/setup.sh
128
+ AGENT_HARNESSES=codex bash scripts/setup/setup.sh
114
129
  ```
115
130
 
116
- See [agent setup](scripts/agent_setup/README.md) for Claude Cloud and validation.
131
+ See [agent setup](scripts/setup/README.md) for Claude Cloud and validation.
117
132
 
118
133
  It can also be consumed as a submodule of the main Astrale workspace:
119
134
 
@@ -37,15 +37,21 @@ function mutationCode(input) {
37
37
  const value = input;
38
38
  if (value.code === 'STATE_TRANSITION_ILLEGAL')
39
39
  return value.code;
40
- for (const candidate of [value.code, nestedCode(value.reason), nestedCode(value.data)]) {
40
+ for (const candidate of [
41
+ value.code,
42
+ directOrNestedCode(value.reason),
43
+ directOrNestedCode(value.data),
44
+ ]) {
41
45
  if (typeof candidate === 'string' && candidate.startsWith('MUTATION_')) {
42
46
  return candidate;
43
47
  }
44
48
  }
45
49
  return undefined;
46
50
  }
47
- function nestedCode(input) {
48
- return input !== null && typeof input === 'object' && !Array.isArray(input)
49
- ? Reflect.get(input, 'code')
50
- : undefined;
51
+ function directOrNestedCode(input) {
52
+ return typeof input === 'string'
53
+ ? input
54
+ : input !== null && typeof input === 'object' && !Array.isArray(input)
55
+ ? Reflect.get(input, 'code')
56
+ : undefined;
51
57
  }
@@ -1,5 +1,5 @@
1
1
  import type { LintReportFormat } from '../linter/index.js';
2
- export declare const COMMANDS: readonly ['dev', 'build', 'deploy', 'test', 'lint', 'package'];
2
+ export declare const COMMANDS: readonly ['dev', 'build', 'deploy', 'test', 'lint', 'package', 'upgrade'];
3
3
  export type Command = (typeof COMMANDS)[number];
4
4
  export interface ParsedArgs {
5
5
  readonly command: Command;
@@ -12,6 +12,8 @@ export interface ParsedArgs {
12
12
  readonly suite?: string;
13
13
  readonly sessionFile?: string;
14
14
  readonly secretsFile?: string;
15
+ readonly channel?: string;
16
+ readonly check?: boolean;
15
17
  }
16
18
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
17
19
  export declare function parseArgs(argv: readonly string[]): ParsedArgs;
@@ -1,8 +1,10 @@
1
1
  import { isAbsolute, normalize } from 'node:path';
2
- export const COMMANDS = ['dev', 'build', 'deploy', 'test', 'lint', 'package'];
2
+ export const COMMANDS = ['dev', 'build', 'deploy', 'test', 'lint', 'package', 'upgrade'];
3
3
  /** Parse the frozen `astrale-domain` command grammar without performing effects. */
4
4
  export function parseArgs(argv) {
5
5
  const [command, ...rest] = argv;
6
+ if (command === 'upgrade')
7
+ return parseUpgrade(rest);
6
8
  const fix = rest.includes('--fix');
7
9
  const deployOnly = rest.includes('--deploy-only');
8
10
  let sessionFile;
@@ -142,6 +144,24 @@ export function parseArgs(argv) {
142
144
  throw new Error(command ? `Unknown command "${command}".` : 'Missing command.');
143
145
  }
144
146
  }
147
+ function parseUpgrade(args) {
148
+ let channel = 'beta';
149
+ let check = false;
150
+ for (let index = 0; index < args.length; index++) {
151
+ const argument = args[index];
152
+ if (argument === '--check')
153
+ check = true;
154
+ else if (argument === '--channel' || argument.startsWith('--channel=')) {
155
+ channel = requiredValue('--channel', argument === '--channel' ? args[++index] : argument.slice(10));
156
+ if (!/^[A-Za-z][A-Za-z0-9._-]*$/u.test(channel)) {
157
+ throw new Error('--channel must be a registry dist-tag, such as beta or latest.');
158
+ }
159
+ }
160
+ else
161
+ throw new Error(`Unknown upgrade argument "${argument}".`);
162
+ }
163
+ return { command: 'upgrade', env: '', channel, check };
164
+ }
145
165
  function lintFormat(input) {
146
166
  if (input !== 'stylish' && input !== 'json') {
147
167
  throw new Error(`--format must be "stylish" or "json", got "${input ?? ''}"`);
@@ -24,6 +24,7 @@ Commands:
24
24
  test Run Project tests, optionally against one Environment.
25
25
  lint Check Domain source and architecture.
26
26
  package Prepare public declarations for publication.
27
+ upgrade Update Astrale dependencies through pnpm (default: beta).
27
28
 
28
29
  Options:
29
30
  -h, --help Show help.
@@ -31,6 +32,29 @@ Options:
31
32
  Run "astrale-domain <command> --help" for command-specific help.
32
33
  `;
33
34
  const COMMAND_HELP = Object.freeze({
35
+ upgrade: `Update existing Astrale dependencies in a pnpm project or workspace.
36
+
37
+ Usage:
38
+ astrale-domain upgrade [--channel <tag>] [--check]
39
+
40
+ Options:
41
+ --channel <tag> Registry dist-tag to select (default: beta).
42
+ --check Show changes without modifying project files or installing.
43
+ -h, --help Show help for upgrade.
44
+
45
+ Behavior:
46
+ Updates dependencies, devDependencies, and optionalDependencies under @astrale-os/
47
+ and @astrale-domains/, including the workspace root. Writes exact versions and
48
+ runs pnpm install once. The selected tag is authoritative, even if older than
49
+ the current version. Installation is synchronized even when versions are unchanged,
50
+ so retrying can finish a previously failed install. Packages without that tag remain
51
+ unchanged with a warning.
52
+ Local links, catalogs, aliases, peers, and overrides are not rewritten.
53
+ Does not load astrale.config.ts, require Bun, run project checks, commit, or deploy.
54
+ Stop development watchers before upgrading. Review changes and run your project's
55
+ checks afterwards. Failed installations leave local changes available for repair.
56
+ For an older SDK without this command: pnpm dlx @astrale-os/sdk@beta upgrade.
57
+ `,
34
58
  dev: `Build, verify, remotely deploy, and watch one Project Environment.
35
59
 
36
60
  Usage:
@@ -29,6 +29,15 @@ export async function run(argv) {
29
29
  }
30
30
  if (parsed.command === 'lint')
31
31
  return lint(parsed);
32
+ if (parsed.command === 'upgrade') {
33
+ const { upgrade } = await import('./upgrade/index.js');
34
+ await upgrade({
35
+ cwd: process.cwd(),
36
+ channel: parsed.channel ?? 'beta',
37
+ check: parsed.check ?? false,
38
+ });
39
+ return 0;
40
+ }
32
41
  if (parsed.command === 'package') {
33
42
  const result = await packageDomain({ projectDir: process.cwd() });
34
43
  info(`Packaged ${result.declarations} public declarations (${result.replacements} references normalized).`);
@@ -0,0 +1,6 @@
1
+ export interface UpgradeOptions {
2
+ readonly cwd: string;
3
+ readonly channel: string;
4
+ readonly check: boolean;
5
+ }
6
+ export declare function upgrade(options: UpgradeOptions): Promise<void>;
@@ -0,0 +1,115 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, realpath, writeFile } from 'node:fs/promises';
3
+ import { isAbsolute, join, relative } from 'node:path';
4
+ import { failureMessage } from '../failure.js';
5
+ import { projectRoot, readManifest, record, registryDependency, updatedManifest, } from './manifest.js';
6
+ import { runPnpm } from './process.js';
7
+ export async function upgrade(options) {
8
+ const root = await realpath(projectRoot(options.cwd));
9
+ const rootManifest = await readManifest(root);
10
+ const rootValue = JSON.parse(rootManifest.source);
11
+ const manager = record(rootValue) ? rootValue.packageManager : undefined;
12
+ if (manager !== undefined
13
+ ? typeof manager !== 'string' || !manager.startsWith('pnpm@')
14
+ : !existsSync(join(root, 'pnpm-workspace.yaml')) && !existsSync(join(root, 'pnpm-lock.yaml'))) {
15
+ throw new Error('Upgrade currently supports pnpm projects only. Declare packageManager: "pnpm@<version>" in package.json.');
16
+ }
17
+ const directories = new Set([root]);
18
+ if (existsSync(join(root, 'pnpm-workspace.yaml'))) {
19
+ const inventory = JSON.parse(await runPnpm(root, [
20
+ '--recursive',
21
+ '--include-workspace-root',
22
+ 'list',
23
+ '--depth',
24
+ '-1',
25
+ '--json',
26
+ ]));
27
+ if (!Array.isArray(inventory))
28
+ throw new Error('Invalid pnpm workspace inventory.');
29
+ for (const entry of inventory) {
30
+ if (!record(entry) || typeof entry.path !== 'string' || !isAbsolute(entry.path))
31
+ throw new Error('Invalid pnpm workspace path.');
32
+ const directory = await realpath(entry.path);
33
+ const offset = relative(root, directory);
34
+ if (offset === '..' ||
35
+ offset.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
36
+ isAbsolute(offset)) {
37
+ throw new Error(`Workspace package is outside ${root}: ${entry.path}.`);
38
+ }
39
+ directories.add(directory);
40
+ }
41
+ }
42
+ const manifests = await Promise.all([...directories].sort().map(readManifest));
43
+ const names = new Set();
44
+ const skipped = new Set();
45
+ for (const manifest of manifests) {
46
+ for (const dependency of manifest.dependencies) {
47
+ if (registryDependency(dependency.value))
48
+ names.add(dependency.name);
49
+ else
50
+ skipped.add(`${dependency.name}: ${dependency.value} (left unchanged)`);
51
+ }
52
+ }
53
+ const versions = new Map();
54
+ const queue = [...names].sort();
55
+ // Resolve the complete plan before writing. Each package's tag is observed once.
56
+ for (let offset = 0; offset < queue.length; offset += 4) {
57
+ await Promise.all(queue.slice(offset, offset + 4).map(async (name) => {
58
+ const tags = JSON.parse(await runPnpm(root, ['view', name, 'dist-tags', '--json']));
59
+ if (!record(tags))
60
+ throw new Error(`Invalid registry dist-tags for ${name}.`);
61
+ const version = tags[options.channel];
62
+ if (version === undefined)
63
+ skipped.add(`${name}: no ${options.channel} tag (left unchanged)`);
64
+ else if (typeof version !== 'string' ||
65
+ !/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/u.test(version)) {
66
+ throw new Error(`Invalid ${options.channel} version for ${name}.`);
67
+ }
68
+ else
69
+ versions.set(name, version);
70
+ }));
71
+ }
72
+ process.stdout.write(`Astrale upgrade · ${options.channel}\nWorkspace: ${root}\n`);
73
+ for (const warning of [...skipped].sort())
74
+ process.stdout.write(`Skipped: ${warning}\n`);
75
+ const changes = manifests
76
+ .map((manifest) => ({ manifest, source: updatedManifest(manifest, versions) }))
77
+ .filter(({ manifest, source }) => manifest.source !== source);
78
+ for (const { manifest } of changes) {
79
+ for (const dependency of manifest.dependencies) {
80
+ const next = registryDependency(dependency.value) ? versions.get(dependency.name) : undefined;
81
+ if (next !== undefined && next !== dependency.value)
82
+ process.stdout.write(`${relative(root, manifest.path)} · ${dependency.name}: ${dependency.value} → ${next}\n`);
83
+ }
84
+ }
85
+ if (changes.length === 0) {
86
+ process.stdout.write('No dependency changes.\n');
87
+ }
88
+ if (options.check) {
89
+ process.stdout.write('Check only: no project files changed; nothing installed.\n');
90
+ return;
91
+ }
92
+ if (versions.size === 0) {
93
+ process.stdout.write('No matching registry versions; nothing installed.\n');
94
+ return;
95
+ }
96
+ for (const { manifest } of changes) {
97
+ if ((await readFile(manifest.path, 'utf8')) !== manifest.source)
98
+ throw new Error(`Manifest changed during upgrade: ${manifest.path}. Retry against the current files.`);
99
+ }
100
+ for (const { manifest, source } of changes)
101
+ await writeFile(manifest.path, source);
102
+ try {
103
+ await runPnpm(root, ['install', '--no-frozen-lockfile'], true);
104
+ }
105
+ catch (cause) {
106
+ throw new Error(`${failureMessage(cause)}\nDependency installation failed. Local changes remain; repair and run pnpm install. Nothing committed or deployed.`, { cause });
107
+ }
108
+ process.stdout.write("Dependencies installed. Run your project's typecheck, lint, tests, and build before deploying. Nothing committed or deployed.\n");
109
+ if (changes.some(({ manifest }) => manifest.dependencies.some(({ name, value }) => name === '@astrale-domains/shell' &&
110
+ registryDependency(value) &&
111
+ versions.has(name) &&
112
+ versions.get(name) !== value))) {
113
+ process.stdout.write('Shell changed: verify the installed Shell revision before deploying dependent Domains.\n');
114
+ }
115
+ }
@@ -0,0 +1,18 @@
1
+ export interface Dependency {
2
+ readonly name: string;
3
+ readonly value: string;
4
+ readonly start: number;
5
+ readonly end: number;
6
+ }
7
+ export interface Manifest {
8
+ readonly path: string;
9
+ readonly source: string;
10
+ readonly dependencies: readonly Dependency[];
11
+ }
12
+ export declare function record(value: unknown): value is Record<string, unknown>;
13
+ /** Find a declared workspace without escaping an enclosing repository. */
14
+ export declare function projectRoot(cwd: string): string;
15
+ export declare function readManifest(directory: string): Promise<Manifest>;
16
+ export declare function registryDependency(value: string): boolean;
17
+ /** Replace only dependency value tokens; preserve formatting and all unrelated content. */
18
+ export declare function updatedManifest(manifest: Manifest, versions: ReadonlyMap<string, string>): string;
@@ -0,0 +1,77 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import ts from 'typescript';
5
+ const SECTIONS = new Set(['dependencies', 'devDependencies', 'optionalDependencies']);
6
+ export function record(value) {
7
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+ /** Find a declared workspace without escaping an enclosing repository. */
10
+ export function projectRoot(cwd) {
11
+ let directory = resolve(cwd);
12
+ let nearest;
13
+ for (;;) {
14
+ if (existsSync(join(directory, 'pnpm-workspace.yaml')))
15
+ return directory;
16
+ if (nearest === undefined && existsSync(join(directory, 'package.json')))
17
+ nearest = directory;
18
+ if (existsSync(join(directory, '.git')) || dirname(directory) === directory)
19
+ break;
20
+ directory = dirname(directory);
21
+ }
22
+ if (nearest === undefined)
23
+ throw new Error(`No package.json found from ${cwd}.`);
24
+ return nearest;
25
+ }
26
+ export async function readManifest(directory) {
27
+ const path = join(directory, 'package.json');
28
+ const source = await readFile(path, 'utf8');
29
+ const value = JSON.parse(source);
30
+ if (!record(value))
31
+ throw new Error(`Invalid package manifest: ${path}.`);
32
+ const tree = ts.parseJsonText(path, source);
33
+ const statement = tree.statements[0];
34
+ if (!statement ||
35
+ !ts.isExpressionStatement(statement) ||
36
+ !ts.isObjectLiteralExpression(statement.expression)) {
37
+ throw new Error(`Invalid package manifest: ${path}.`);
38
+ }
39
+ const dependencies = [];
40
+ for (const section of statement.expression.properties) {
41
+ if (!ts.isPropertyAssignment(section) || !SECTIONS.has(section.name.getText(tree).slice(1, -1)))
42
+ continue;
43
+ if (!ts.isObjectLiteralExpression(section.initializer))
44
+ throw new Error(`Invalid dependency section in ${path}.`);
45
+ for (const property of section.initializer.properties) {
46
+ if (!ts.isPropertyAssignment(property) || !ts.isStringLiteral(property.name))
47
+ continue;
48
+ const name = property.name.text;
49
+ if (!name.startsWith('@astrale-os/') && !name.startsWith('@astrale-domains/'))
50
+ continue;
51
+ if (!ts.isStringLiteral(property.initializer))
52
+ throw new Error(`Invalid dependency ${name} in ${path}.`);
53
+ dependencies.push({
54
+ name,
55
+ value: property.initializer.text,
56
+ start: property.initializer.getStart(tree),
57
+ end: property.initializer.end,
58
+ });
59
+ }
60
+ }
61
+ return { path, source, dependencies };
62
+ }
63
+ export function registryDependency(value) {
64
+ return value.length > 0 && !/[:/\\#]/u.test(value) && !value.endsWith('.tgz');
65
+ }
66
+ /** Replace only dependency value tokens; preserve formatting and all unrelated content. */
67
+ export function updatedManifest(manifest, versions) {
68
+ let result = manifest.source;
69
+ for (const dependency of [...manifest.dependencies].reverse()) {
70
+ const version = registryDependency(dependency.value) ? versions.get(dependency.name) : undefined;
71
+ if (version !== undefined && version !== dependency.value) {
72
+ result =
73
+ result.slice(0, dependency.start) + JSON.stringify(version) + result.slice(dependency.end);
74
+ }
75
+ }
76
+ return result;
77
+ }
@@ -0,0 +1,2 @@
1
+ /** pnpm owns registry configuration, installation policy, and lockfile resolution. */
2
+ export declare function runPnpm(cwd: string, args: readonly string[], install?: boolean): Promise<string>;
@@ -0,0 +1,57 @@
1
+ import spawn from 'cross-spawn';
2
+ /** pnpm owns registry configuration, installation policy, and lockfile resolution. */
3
+ export function runPnpm(cwd, args, install = false) {
4
+ return new Promise((resolve, reject) => {
5
+ const child = spawn('pnpm', [...args], {
6
+ cwd,
7
+ stdio: install ? 'inherit' : ['ignore', 'pipe', 'pipe'],
8
+ shell: false,
9
+ });
10
+ let stdout = '';
11
+ let stderr = '';
12
+ let failure;
13
+ let escalation;
14
+ const stop = (reason) => {
15
+ if (failure)
16
+ return;
17
+ failure = new Error(reason);
18
+ child.kill('SIGTERM');
19
+ escalation = setTimeout(() => child.kill('SIGKILL'), 1_000);
20
+ };
21
+ const interrupt = () => stop('Upgrade interrupted; inspect local changes before retrying.');
22
+ process.once('SIGINT', interrupt);
23
+ process.once('SIGTERM', interrupt);
24
+ const timer = setTimeout(() => stop('pnpm timed out.'), install ? 600_000 : 60_000);
25
+ child.stdout?.on('data', (chunk) => {
26
+ stdout += chunk.toString('utf8');
27
+ if (stdout.length > 4 * 1024 * 1024)
28
+ stop('pnpm output exceeded the supported limit.');
29
+ });
30
+ child.stderr?.on('data', (chunk) => {
31
+ stderr += chunk.toString('utf8');
32
+ if (stderr.length > 4 * 1024 * 1024)
33
+ stop('pnpm output exceeded the supported limit.');
34
+ });
35
+ const cleanup = () => {
36
+ clearTimeout(timer);
37
+ if (escalation)
38
+ clearTimeout(escalation);
39
+ process.removeListener('SIGINT', interrupt);
40
+ process.removeListener('SIGTERM', interrupt);
41
+ };
42
+ child.once('error', (cause) => {
43
+ cleanup();
44
+ reject(new Error("Could not start pnpm. Install the project's declared pnpm version.", { cause }));
45
+ });
46
+ child.once('close', (code, signal) => {
47
+ cleanup();
48
+ if (failure)
49
+ reject(failure);
50
+ else if (code !== 0) {
51
+ reject(new Error(`pnpm ${args[0]} failed (${signal ?? code}).${stderr ? `\n${stderr.trim()}` : ''}`));
52
+ }
53
+ else
54
+ resolve(stdout);
55
+ });
56
+ });
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/sdk",
3
- "version": "0.5.0-beta.132",
3
+ "version": "0.5.0-beta.134",
4
4
  "description": "Schema-first SDK for defining, composing, and deploying Astrale domains",
5
5
  "keywords": [
6
6
  "astrale",
@@ -244,6 +244,7 @@
244
244
  "@astrale-os/kernel-server": "0.5.0-beta.52",
245
245
  "@lucide/icons": "1.34.0",
246
246
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
247
+ "cross-spawn": "7.0.6",
247
248
  "hono": "^4.13.2",
248
249
  "jose": "^6.2.9",
249
250
  "typescript": "~6.0.3"
@@ -256,6 +257,7 @@
256
257
  "@commitlint/config-conventional": "~21.2.2",
257
258
  "@hono/node-server": "^2.1.1",
258
259
  "@hono/node-server-v1": "npm:@hono/node-server@1.19.0",
260
+ "@types/cross-spawn": "6.0.6",
259
261
  "@types/node": "^22.20.1",
260
262
  "husky": "~9.1.7",
261
263
  "lint-staged": "~17.3.0",
@@ -329,10 +331,11 @@
329
331
  "qualify:publications": "node scripts/qualify-publications.mjs",
330
332
  "qualify": "node scripts/qualify.mjs",
331
333
  "sync:linter-policy": "node scripts/sync-domain-policy.mjs",
332
- "setup": "bash scripts/agent_setup/setup.sh",
333
- "setup:codex": "AGENT_HARNESSES=codex bash scripts/agent_setup/setup.sh",
334
- "setup:claude": "AGENT_HARNESSES=claude bash scripts/agent_setup/setup.sh",
335
- "check:agent-setup": "bash scripts/agent_setup/verify.sh",
336
- "test:agent-setup": "node --test scripts/agent_setup/*.test.cjs"
334
+ "setup": "bash scripts/setup/setup.sh",
335
+ "setup:codex": "AGENT_HARNESSES=codex bash scripts/setup/setup.sh",
336
+ "setup:claude": "AGENT_HARNESSES=claude bash scripts/setup/setup.sh",
337
+ "check:agent-setup": "bash scripts/setup/verify.sh",
338
+ "test:agent-setup": "node --test scripts/setup/*.test.cjs",
339
+ "setup:local": "AGENT_SETUP_TOOLS=check bash scripts/setup/setup.sh"
337
340
  }
338
341
  }