@omnitron-dev/omnitron 0.3.2 → 0.3.3

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.
@@ -0,0 +1,35 @@
1
+ // pnpm runs this hook before the first install, when project dependencies are unavailable.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = fs.realpathSync(path.dirname(fileURLToPath(import.meta.url)));
7
+ const profileDirectory = path.join(root, '.omnitron', 'dev-sdk');
8
+
9
+ export function developmentPackages() {
10
+ let profile;
11
+ try {
12
+ profile = JSON.parse(fs.readFileSync(path.join(profileDirectory, 'install.json'), 'utf8'));
13
+ } catch (error) {
14
+ if (error.code === 'ENOENT') return {};
15
+ throw error;
16
+ }
17
+ if (profile.schema !== 1 || profile.root !== root || !path.isAbsolute(profile.source))
18
+ throw Error('Invalid Omnitron development SDK selection');
19
+ for (const [name, directory] of Object.entries(profile.links)) {
20
+ if (!/^@omnitron-dev\/[a-z0-9-]+$/.test(name) || !path.isAbsolute(directory) ||
21
+ !directory.startsWith(profile.source + path.sep) || fs.realpathSync(directory) !== directory)
22
+ throw Error('Invalid Omnitron SDK package location for ' + name);
23
+ }
24
+ return profile.links;
25
+ }
26
+
27
+ export const hooks = {
28
+ updateConfig(config) {
29
+ const links = Object.entries(developmentPackages());
30
+ if (!links.length) return config;
31
+ const overrides = { ...config.overrides };
32
+ for (const [name, directory] of links) overrides[name] = 'link:' + directory;
33
+ return { ...config, lockfileDir: profileDirectory, overrides };
34
+ },
35
+ };
package/bin/sdk.d.mts ADDED
@@ -0,0 +1,8 @@
1
+ export interface DevelopmentSdkStatus {
2
+ mode: 'development' | 'registry';
3
+ verifiedImports: number;
4
+ source?: string;
5
+ distributionLockChanged?: boolean;
6
+ }
7
+ export function sdkCommand(args: string[]): Promise<void>;
8
+ export function inspectSdk(projectDirectory?: string): Promise<DevelopmentSdkStatus>;
package/bin/sdk.mjs ADDED
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import fs from 'node:fs/promises';
5
+ import { watch } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import { setTimeout as delay } from 'node:timers/promises';
10
+ import { parseArgs } from 'node:util';
11
+ import { parseAllDocuments } from 'yaml';
12
+ import { readFrameworkLayout } from '@omnitron-dev/omnitron/source-layout';
13
+
14
+ const help = `omnitron sdk init [--project-dir PATH]
15
+ omnitron sdk dev [SDK_CHECKOUT] [--project-dir PATH] [--stack NAME] [--no-watch]
16
+ omnitron sdk registry [--project-dir PATH]
17
+ omnitron sdk status [--project-dir PATH]
18
+
19
+ dev selects the local SDK and watches its sources. Start again without a path to
20
+ reuse the selection. --no-watch installs the profile for a separate watch session.
21
+ --stack also refreshes running, watched applications in that local source stack.
22
+ registry restores the frozen distribution dependencies. Releases always read Git.`;
23
+
24
+ const scope = '@omnitron-dev/';
25
+ const statePath = (root) => path.join(root, '.omnitron', 'dev-sdk');
26
+ const sha256 = (value) => createHash('sha256').update(value).digest('hex');
27
+ const inside = (parent, child) => {
28
+ const rel = path.relative(parent, child);
29
+ return !path.isAbsolute(rel) && rel !== '..' && !rel.startsWith('..' + path.sep);
30
+ };
31
+ const readJson = async (file) => JSON.parse(await fs.readFile(file, 'utf8'));
32
+ async function optionalJson(file) {
33
+ try {
34
+ return await readJson(file);
35
+ } catch (error) {
36
+ if (error.code === 'ENOENT') return null;
37
+ throw error;
38
+ }
39
+ }
40
+ async function atomic(file, value) {
41
+ const tmp = file + '.' + randomUUID() + '.tmp';
42
+ try {
43
+ await fs.writeFile(tmp, value, { flag: 'wx', mode: 0o600 });
44
+ await fs.rename(tmp, file);
45
+ } finally {
46
+ await fs.rm(tmp, { force: true });
47
+ }
48
+ }
49
+
50
+ function command(program, args, cwd, capture = false, signal) {
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn(program, args, {
53
+ cwd,
54
+ shell: false,
55
+ signal,
56
+ stdio: capture ? ['ignore', 'pipe', 'inherit'] : 'inherit',
57
+ });
58
+ let result = '';
59
+ child.stdout?.setEncoding('utf8').on('data', (chunk) => {
60
+ result += chunk;
61
+ });
62
+ child.on('error', reject);
63
+ child.on('close', (code, sig) =>
64
+ code === 0 ? resolve(result.trim()) : reject(new Error(`${program} ${args[0]} exited with ${sig ?? code}`))
65
+ );
66
+ });
67
+ }
68
+
69
+ async function inventory(root) {
70
+ const rows = JSON.parse(
71
+ await command(
72
+ 'pnpm',
73
+ ['--config.lockfile=false', '--config.ignore-pnpmfile=true', '-r', 'list', '--depth', '-1', '--json'],
74
+ root,
75
+ true
76
+ )
77
+ );
78
+ const entries = new Map();
79
+ for (const row of rows) {
80
+ const directory = await fs.realpath(row.path);
81
+ if (!inside(root, directory)) throw Error('Workspace package is outside the selected checkout: ' + directory);
82
+ const manifest = await readJson(path.join(directory, 'package.json'));
83
+ if (!manifest.name) continue;
84
+ if (entries.has(manifest.name)) throw Error('Duplicate workspace package: ' + manifest.name);
85
+ entries.set(manifest.name, { directory, manifest });
86
+ }
87
+ return entries;
88
+ }
89
+ async function desiredProfile(root, source, distribution) {
90
+ const layout = readFrameworkLayout(distribution);
91
+ if (layout.kind !== 'registry')
92
+ throw Error('Commit an npm registry lockfile for the project before selecting a development SDK');
93
+ const packages = await inventory(source);
94
+ const links = {};
95
+ for (const { name } of layout.packages) {
96
+ const pkg = packages.get(name);
97
+ if (!pkg || pkg.manifest.private) throw Error('SDK checkout does not provide ' + name);
98
+ links[name] = pkg.directory;
99
+ }
100
+ return { schema: 1, root, source, lockSha256: sha256(distribution), links };
101
+ }
102
+
103
+ async function prepareState(root) {
104
+ const dir = statePath(root);
105
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
106
+ if ((await fs.realpath(dir)) !== dir) throw Error('SDK state directory must be local to this project');
107
+ await fs.writeFile(path.join(dir, '.gitignore'), '*\n', { mode: 0o600 });
108
+ return dir;
109
+ }
110
+
111
+ // The OS releases the SQLite writer lock if the process dies, including on Windows.
112
+ async function exclusively(dir, body, waitSignal) {
113
+ const { DatabaseSync } = await import('node:sqlite');
114
+ const db = new DatabaseSync(path.join(dir, 'operation.sqlite'));
115
+ try {
116
+ for (;;) {
117
+ waitSignal?.throwIfAborted();
118
+ try {
119
+ db.exec('BEGIN IMMEDIATE');
120
+ break;
121
+ } catch (error) {
122
+ if (error.errcode !== 5) throw error;
123
+ if (!waitSignal) throw Error('Another development SDK operation is active in ' + dir);
124
+ await delay(200, undefined, { signal: waitSignal });
125
+ }
126
+ }
127
+ return await body();
128
+ } finally {
129
+ db.close();
130
+ }
131
+ }
132
+
133
+ async function seedLock(root, dir, distribution) {
134
+ const documents = parseAllDocuments(distribution);
135
+ const app = documents.at(-1);
136
+ const importers = app.get('importers');
137
+ const prefix = path.relative(dir, root).split(path.sep).join('/');
138
+ for (const entry of importers.items) entry.key.value = path.posix.normalize(path.posix.join(prefix, entry.key.value));
139
+ await atomic(path.join(dir, 'pnpm-lock.yaml'), documents.map((doc) => doc.toString()).join('---\n'));
140
+ }
141
+
142
+ async function installDevelopment(root, profile) {
143
+ const dir = statePath(root);
144
+ const distribution = await fs.readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8');
145
+ if (profile.lockSha256 !== sha256(distribution))
146
+ throw Error('Distribution lock changed while preparing the development profile');
147
+ await seedLock(root, dir, distribution);
148
+ // The workspace hook selects the same graph for normal pnpm install, run and exec.
149
+ await atomic(path.join(dir, 'install.json'), JSON.stringify(profile, null, 2) + '\n');
150
+ await command('pnpm', ['install', '--force', '--no-frozen-lockfile', '--config.confirm-modules-purge=false'], root);
151
+ if (sha256(await fs.readFile(path.join(root, 'pnpm-lock.yaml'))) !== profile.lockSha256)
152
+ throw Error('Distributed lockfile changed during the local installation');
153
+ await verifyLinks(root, profile);
154
+ }
155
+ async function verifyLinks(root, profile) {
156
+ const importers = await inventory(root);
157
+ const locked = profile ? null : readFrameworkLayout(await fs.readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8'));
158
+ if (locked && locked.kind !== 'registry') throw Error('The distribution lock must select registry packages');
159
+ let verified = 0;
160
+ for (const { directory, manifest } of importers.values()) {
161
+ for (const name of Object.keys({
162
+ ...manifest.dependencies,
163
+ ...manifest.devDependencies,
164
+ ...manifest.peerDependencies,
165
+ ...manifest.optionalDependencies,
166
+ })) {
167
+ if (!name.startsWith(scope)) continue;
168
+ if (
169
+ manifest.peerDependenciesMeta?.[name]?.optional &&
170
+ !manifest.dependencies?.[name] &&
171
+ !manifest.devDependencies?.[name]
172
+ )
173
+ continue;
174
+ const actual = await fs.realpath(path.join(directory, 'node_modules', name));
175
+ if (!profile) {
176
+ const identity = await readJson(path.join(actual, 'package.json'));
177
+ const entry = locked.packages.find((pkg) => pkg.name === name);
178
+ if (
179
+ !entry ||
180
+ identity.name !== name ||
181
+ identity.version !== entry.version ||
182
+ !inside(path.join(root, 'node_modules'), actual)
183
+ )
184
+ throw Error('Registry installation does not match the distribution lock for ' + name);
185
+ } else if (actual !== profile.links[name])
186
+ throw Error(`${manifest.name} resolves ${name} from ${actual}, expected ${profile.links[name]}`);
187
+ verified++;
188
+ }
189
+ }
190
+ return verified;
191
+ }
192
+ async function frozen(root) {
193
+ await fs.rm(path.join(statePath(root), 'install.json'), { force: true });
194
+ await command('pnpm', ['install', '--force', '--frozen-lockfile', '--config.confirm-modules-purge=false'], root);
195
+ }
196
+
197
+ async function build(source, names, signal) {
198
+ if (!names.length) return;
199
+ const dir = await prepareState(source);
200
+ await exclusively(
201
+ dir,
202
+ () =>
203
+ command(
204
+ 'pnpm',
205
+ ['exec', 'turbo', 'run', 'build', ...names.map((n) => '--filter=' + n + '...')],
206
+ source,
207
+ false,
208
+ signal
209
+ ),
210
+ signal
211
+ );
212
+ }
213
+
214
+ async function profileStatus(root) {
215
+ const active = await optionalJson(path.join(statePath(root), 'active.json'));
216
+ if (active && (active.schema !== 1 || active.root !== root || typeof active.source !== 'string'))
217
+ throw Error('Invalid local SDK selection');
218
+ return active;
219
+ }
220
+
221
+ async function localStack(root, stack) {
222
+ const { createDaemonClient, LONG_REQUEST_TIMEOUT } = await import('@omnitron-dev/omnitron/operations');
223
+ const client = createDaemonClient(undefined, LONG_REQUEST_TIMEOUT);
224
+ try {
225
+ const projects = await client.service('OmnitronProject');
226
+ const candidates = [];
227
+ for (const p of await projects.listProjects())
228
+ if ((await fs.realpath(p.path).catch(() => null)) === root) candidates.push(p.name);
229
+ if (candidates.length !== 1) throw Error('Expected exactly one registered project for ' + root);
230
+ const state = await projects.getStack({ project: candidates[0], stack });
231
+ if (state.type !== 'local' || state.selectedRelease || state.config.watch === false)
232
+ throw Error('SDK refresh requires an unpinned local development stack with watching enabled');
233
+ return { client, state, project: candidates[0] };
234
+ } catch (error) {
235
+ await client.disconnect();
236
+ throw error;
237
+ }
238
+ }
239
+
240
+ async function requireStopped(root, stack) {
241
+ const socket = path.join(process.env.OMNITRON_HOME ?? path.join(os.homedir(), '.omnitron'), 'daemon.sock');
242
+ if (
243
+ !stack &&
244
+ !(await fs
245
+ .lstat(socket)
246
+ .then(() => true)
247
+ .catch((error) => {
248
+ if (error.code === 'ENOENT') return false;
249
+ throw error;
250
+ }))
251
+ )
252
+ return;
253
+ const { createDaemonClient, LONG_REQUEST_TIMEOUT } = await import('@omnitron-dev/omnitron/operations');
254
+ const client = createDaemonClient(undefined, LONG_REQUEST_TIMEOUT);
255
+ try {
256
+ const service = await client.service('OmnitronProject');
257
+ let found = false;
258
+ for (const project of await service.listProjects()) {
259
+ if ((await fs.realpath(project.path).catch(() => null)) !== root) continue;
260
+ for (const active of await service.listStacks({ project: project.name })) {
261
+ if (active.type !== 'local' || active.selectedRelease) continue;
262
+ if (active.name === stack) found = true;
263
+ if (active.apps.some((app) => !['stopped', 'offline', 'not-started'].includes(app.status)))
264
+ throw Error(`Stop ${project.name}/${active.name} before switching its working-tree dependencies`);
265
+ }
266
+ }
267
+ if (stack && !found) throw Error('No local source stack named ' + stack + ' is registered for this checkout');
268
+ } finally {
269
+ await client.disconnect();
270
+ }
271
+ }
272
+
273
+ function dependsOn(manifest, changed, packages, seen = new Set()) {
274
+ for (const name of Object.keys({
275
+ ...manifest.dependencies,
276
+ ...manifest.peerDependencies,
277
+ ...manifest.optionalDependencies,
278
+ })) {
279
+ if (changed.has(name)) return true;
280
+ if (seen.has(name)) continue;
281
+ seen.add(name);
282
+ const pkg = packages.get(name);
283
+ if (pkg && dependsOn(pkg.manifest, changed, packages, seen)) return true;
284
+ }
285
+ return false;
286
+ }
287
+ async function refresh(root, stack, changed, packages) {
288
+ if (!stack) return;
289
+ const { client, state, project } = await localStack(root, stack);
290
+ try {
291
+ const watched = await client.getWatchStatus();
292
+ if (!watched.enabled || !watched.watching) return;
293
+ for (const app of state.apps) {
294
+ if (app.status !== 'online' || !app.handleKey) continue;
295
+ const info = watched.apps.find((entry) => entry.name === app.handleKey);
296
+ if (!info) continue;
297
+ const directory = await fs.realpath(info.directory).catch(() => null);
298
+ if (!directory || !inside(root, directory)) continue;
299
+ const pkg = [...packages.values()]
300
+ .filter((p) => inside(p.directory, directory))
301
+ .sort((a, b) => b.directory.length - a.directory.length)[0];
302
+ if (!pkg || !dependsOn(pkg.manifest, changed, packages)) continue;
303
+ const current = await client.service('OmnitronProject').then((s) => s.getStack({ project, stack }));
304
+ const latestWatch = await client.getWatchStatus();
305
+ if (
306
+ !latestWatch.enabled ||
307
+ !latestWatch.watching ||
308
+ !latestWatch.apps.some((w) => w.name === app.handleKey && w.directory === info.directory)
309
+ )
310
+ continue;
311
+ if (
312
+ current.selectedRelease ||
313
+ current.type !== 'local' ||
314
+ current.config.watch === false ||
315
+ !current.apps.some((a) => a.handleKey === app.handleKey && a.status === 'online')
316
+ )
317
+ continue;
318
+ await client.restartApp({ name: app.handleKey });
319
+ console.log('Refreshed ' + app.handleKey);
320
+ }
321
+ } finally {
322
+ await client.disconnect();
323
+ }
324
+ }
325
+
326
+ async function runtimeDigest(directory) {
327
+ const root = path.join(directory, 'dist'),
328
+ hash = createHash('sha256');
329
+ async function visit(relative) {
330
+ const entries = await fs.readdir(path.join(root, relative), { withFileTypes: true });
331
+ for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
332
+ const file = path.join(relative, e.name);
333
+ if (e.isDirectory()) await visit(file);
334
+ else if (e.isFile() && /\.(?:(?:c|m)?js|json|css|wasm)$/.test(file)) {
335
+ hash
336
+ .update(file)
337
+ .update('\0')
338
+ .update(
339
+ createHash('sha256')
340
+ .update(await fs.readFile(path.join(root, file)))
341
+ .digest()
342
+ );
343
+ }
344
+ }
345
+ }
346
+ await visit('');
347
+ return hash.digest('hex');
348
+ }
349
+
350
+ export async function watchSdk(profile, stack, signal) {
351
+ const packages = new Map([...(await inventory(profile.source)), ...(await inventory(profile.root))]);
352
+ const checkpoint = path.join(statePath(profile.root), 'built.json');
353
+ const prior = await optionalJson(checkpoint);
354
+ const hashes = prior?.source === profile.source ? prior.hashes : {};
355
+ const sdkPackages = new Map([...packages].filter(([name]) => Object.hasOwn(profile.links, name)));
356
+ const pending = new Set(Object.keys(profile.links));
357
+ const handles = [];
358
+ let timer, running;
359
+ async function rebuild() {
360
+ const edits = new Set(pending);
361
+ pending.clear();
362
+ const names = [...sdkPackages]
363
+ .filter(([name, pkg]) => edits.has(name) || dependsOn(pkg.manifest, edits, sdkPackages))
364
+ .map(([name]) => name);
365
+ await build(profile.source, names, signal);
366
+ const changed = new Set();
367
+ const next = { ...hashes };
368
+ for (const name of names) {
369
+ next[name] = await runtimeDigest(profile.links[name]);
370
+ if (hashes[name] !== next[name]) changed.add(name);
371
+ }
372
+ if (signal.aborted) return;
373
+ if (changed.size) await refresh(profile.root, stack, changed, packages);
374
+ Object.assign(hashes, next);
375
+ await atomic(checkpoint, JSON.stringify({ source: profile.source, hashes }) + '\n');
376
+ console.log(changed.size ? 'Development SDK ready: ' + [...changed].join(', ') : 'Development SDK is current');
377
+ }
378
+ const flush = async () => {
379
+ if (running || signal.aborted || !pending.size) return;
380
+ const attempted = [...pending];
381
+ running = rebuild();
382
+ let replay = true;
383
+ try {
384
+ await running;
385
+ } catch (error) {
386
+ if (!signal.aborted) {
387
+ replay = pending.size > 0;
388
+ for (const name of attempted) pending.add(name);
389
+ console.error('SDK build or refresh failed:', error.message);
390
+ }
391
+ } finally {
392
+ running = undefined;
393
+ if (!signal.aborted && pending.size && replay) queueMicrotask(flush);
394
+ }
395
+ };
396
+ function schedule(names) {
397
+ for (const name of names) pending.add(name);
398
+ clearTimeout(timer);
399
+ timer = setTimeout(flush, 150);
400
+ }
401
+ try {
402
+ for (const [name, dir] of Object.entries(profile.links)) {
403
+ handles.push(watch(path.join(dir, 'src'), { recursive: true }, () => schedule([name])));
404
+ handles.push(
405
+ watch(dir, (_event, file) => {
406
+ if (file && /^(?:package\.json|tsconfig.*\.json|(?:tsup|vite)\.config\..+)$/.test(String(file)))
407
+ schedule([name]);
408
+ })
409
+ );
410
+ }
411
+ handles.push(
412
+ watch(profile.source, (_event, file) => {
413
+ if (['pnpm-lock.yaml', 'pnpm-workspace.yaml', 'turbo.json', 'tsconfig.json'].includes(String(file)))
414
+ schedule(Object.keys(profile.links));
415
+ })
416
+ );
417
+ console.log('Watching SDK sources at ' + profile.source);
418
+ void flush();
419
+ if (!signal.aborted)
420
+ await new Promise((resolve, reject) => {
421
+ signal.addEventListener('abort', resolve, { once: true });
422
+ for (const handle of handles) handle.once('error', reject);
423
+ });
424
+ } finally {
425
+ clearTimeout(timer);
426
+ for (const handle of handles) handle.close();
427
+ await running?.catch(() => {});
428
+ }
429
+ }
430
+
431
+ export async function inspectSdk(projectDirectory = process.cwd()) {
432
+ const root = await fs.realpath(projectDirectory);
433
+ const dir = statePath(root);
434
+ const active = await profileStatus(root);
435
+ const distribution = await fs.readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8');
436
+ const selected = await optionalJson(path.join(dir, 'install.json'));
437
+ if (JSON.stringify(selected) !== JSON.stringify(active))
438
+ throw Error('An SDK switch was interrupted. Run sdk registry or sdk dev to reconcile the installation.');
439
+ return active
440
+ ? {
441
+ mode: 'development',
442
+ source: active.source,
443
+ verifiedImports: await verifyLinks(root, active),
444
+ distributionLockChanged: sha256(distribution) !== active.lockSha256,
445
+ }
446
+ : { mode: readFrameworkLayout(distribution).kind, verifiedImports: await verifyLinks(root, null) };
447
+ }
448
+
449
+ export async function sdkCommand(argv) {
450
+ const { values, positionals } = parseArgs({
451
+ args: argv,
452
+ allowPositionals: true,
453
+ options: {
454
+ 'project-dir': { type: 'string' },
455
+ stack: { type: 'string' },
456
+ 'no-watch': { type: 'boolean' },
457
+ json: { type: 'boolean' },
458
+ help: { type: 'boolean', short: 'h' },
459
+ },
460
+ });
461
+ const [action, sourceArg] = positionals;
462
+ if (values.help || !action) {
463
+ console.log(help);
464
+ return;
465
+ }
466
+ if (!['init', 'dev', 'status', 'registry'].includes(action) || positionals.length > (action === 'dev' ? 2 : 1))
467
+ throw Error(help);
468
+ const root = await fs.realpath(values['project-dir'] ?? process.cwd()),
469
+ dir = statePath(root);
470
+ const bridge = path.join(root, '.pnpmfile.mjs');
471
+ const template = await fs.readFile(new URL('./pnpmfile-sdk.mjs', import.meta.url), 'utf8');
472
+ if (action === 'init') {
473
+ for (const existing of ['.pnpmfile.cjs', '.pnpmfile.js']) {
474
+ if (
475
+ await fs.stat(path.join(root, existing)).then(
476
+ () => true,
477
+ (error) => {
478
+ if (error.code === 'ENOENT') return false;
479
+ throw error;
480
+ }
481
+ )
482
+ )
483
+ throw Error('An existing pnpm hook owns ' + existing + ' in ' + root);
484
+ }
485
+ if (await optionalJson(path.join(dir, 'install.json')))
486
+ throw Error('Restore the registry profile before initializing its distribution hook');
487
+ try {
488
+ await fs.writeFile(bridge, template, { flag: 'wx' });
489
+ } catch (error) {
490
+ if (error.code !== 'EEXIST') throw error;
491
+ if ((await fs.readFile(bridge, 'utf8')) !== template)
492
+ throw Error(
493
+ 'An existing pnpm hook owns ' +
494
+ bridge +
495
+ '. Preserve that hook and integrate the SDK selection before enabling development.'
496
+ );
497
+ }
498
+ await command('pnpm', ['install', '--lockfile-only', '--no-frozen-lockfile'], root);
499
+ console.log('Commit the pnpm hook and distribution lockfile.');
500
+ return;
501
+ }
502
+ const distribution = await fs.readFile(path.join(root, 'pnpm-lock.yaml'), 'utf8');
503
+ if (action === 'status') {
504
+ console.log(JSON.stringify(await inspectSdk(root), null, 2));
505
+ return;
506
+ }
507
+ if (action === 'dev' && (await fs.readFile(bridge, 'utf8').catch(() => null)) !== template)
508
+ throw Error('Initialize the supported local SDK bridge with omnitron sdk init');
509
+ await prepareState(root);
510
+ await exclusively(dir, async () => {
511
+ const previous = await profileStatus(root);
512
+ const selection = path.join(dir, 'active.json');
513
+ const selected = await optionalJson(path.join(dir, 'install.json'));
514
+ const interrupted = JSON.stringify(selected) !== JSON.stringify(previous);
515
+ if (action === 'registry') {
516
+ if (!previous && !interrupted) {
517
+ await verifyLinks(root, null);
518
+ return;
519
+ }
520
+ await requireStopped(root, values.stack);
521
+ try {
522
+ await frozen(root);
523
+ await verifyLinks(root, null);
524
+ await fs.rm(selection, { force: true });
525
+ } catch (error) {
526
+ try {
527
+ if (previous) await installDevelopment(root, previous);
528
+ } catch (rollback) {
529
+ throw new AggregateError([error, rollback], 'Registry switch and recovery failed');
530
+ }
531
+ throw error;
532
+ }
533
+ return;
534
+ }
535
+ const source = await fs.realpath(
536
+ sourceArg ?? previous?.source ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')
537
+ );
538
+ if (inside(root, source) || inside(source, root)) throw Error('Select separate project and SDK workspaces');
539
+ const desired = await desiredProfile(root, source, distribution);
540
+ const controller = new AbortController();
541
+ const stop = () => controller.abort();
542
+ process.once('SIGINT', stop);
543
+ process.once('SIGTERM', stop);
544
+ try {
545
+ if (interrupted || JSON.stringify(previous) !== JSON.stringify(desired)) {
546
+ await requireStopped(root, values.stack);
547
+ await build(source, Object.keys(desired.links), controller.signal);
548
+ try {
549
+ await installDevelopment(root, desired);
550
+ const hashes = {};
551
+ for (const [name, directory] of Object.entries(desired.links)) hashes[name] = await runtimeDigest(directory);
552
+ await atomic(path.join(dir, 'built.json'), JSON.stringify({ source: desired.source, hashes }) + '\n');
553
+ await atomic(selection, JSON.stringify(desired, null, 2) + '\n');
554
+ } catch (error) {
555
+ try {
556
+ if (previous) await installDevelopment(root, previous);
557
+ else await frozen(root);
558
+ } catch (rollback) {
559
+ throw new AggregateError([error, rollback], 'SDK switch and recovery failed');
560
+ }
561
+ throw error;
562
+ }
563
+ } else await verifyLinks(root, desired);
564
+ if (!values['no-watch']) await watchSdk(desired, values.stack, controller.signal);
565
+ } finally {
566
+ process.off('SIGINT', stop);
567
+ process.off('SIGTERM', stop);
568
+ }
569
+ });
570
+ }
571
+
572
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url)
573
+ sdkCommand(process.argv.slice(2)).catch((error) => {
574
+ console.error(error.message);
575
+ process.exitCode = 1;
576
+ });
package/dist/BUILD.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "ecf537907f8e7efa75a56012a96c84c451ced77b",
2
+ "commit": "a26d58fe130b02bab92cf7d568e977e9b4e85b54",
3
3
  "dirty": false,
4
- "builtAt": "2026-11-09T19:37:09.000Z",
4
+ "builtAt": "2026-11-10T09:13:25.000Z",
5
5
  "timeBasis": "source"
6
6
  }
@@ -27,6 +27,32 @@ program.name('omnitron').description('Production-grade Titan application supervi
27
27
  // for environments where flag plumbing is awkward (CI templates, hooks).
28
28
  program.option('--json', 'Emit machine-readable JSON output (no spinners, no styling)');
29
29
  program.hook('preAction', (_root, actionCommand) => prepareCommandOutput(actionCommand));
30
+ const sdk = program.command('sdk').description('Select the framework SDK for local development');
31
+ sdk.command('init').description('Initialize the pnpm bridge for a project')
32
+ .option('--project-dir <path>', 'Consumer workspace directory', process.cwd())
33
+ .action(async ({ projectDir }) => (await import('../../bin/sdk.mjs')).sdkCommand(['init', '--project-dir', projectDir]));
34
+ sdk.command('dev [checkout]').description('Select a local SDK and rebuild when it changes')
35
+ .option('--project-dir <path>', 'Consumer workspace directory', process.cwd())
36
+ .option('--stack <name>', 'Refresh watched applications in this local source stack')
37
+ .option('--no-watch', 'Install the SDK without starting a watch session')
38
+ .action(async (checkout, options) => (await import('../../bin/sdk.mjs')).sdkCommand([
39
+ 'dev', ...(checkout ? [checkout] : []), '--project-dir', options.projectDir,
40
+ ...(options.stack ? ['--stack', options.stack] : []), ...(options.watch === false ? ['--no-watch'] : []),
41
+ ]));
42
+ sdk.command('registry').description('Restore dependencies from the frozen distribution lock')
43
+ .option('--project-dir <path>', 'Consumer workspace directory', process.cwd())
44
+ .action(async ({ projectDir }) => (await import('../../bin/sdk.mjs')).sdkCommand(['registry', '--project-dir', projectDir]));
45
+ sdk.command('status').description('Verify the workspace imports and report their SDK origin')
46
+ .option('--project-dir <path>', 'Consumer workspace directory', process.cwd())
47
+ .structuredAction(async ({ projectDir }) => {
48
+ const { inspectSdk } = await import('../../bin/sdk.mjs');
49
+ const { emitJson, emitInfo } = await import('../commands/output.js');
50
+ const status = await inspectSdk(projectDir);
51
+ if (!emitJson(status)) {
52
+ emitInfo([status.mode, status.source, status.verifiedImports + ' verified imports',
53
+ ...(status.distributionLockChanged ? ['distribution lock changed'] : [])].filter(Boolean).join(' · '));
54
+ }
55
+ });
30
56
  // ============================================================================
31
57
  // Daemon Lifecycle
32
58
  // ============================================================================