orbit-thread 0.0.0-stage → 1.2.0

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,501 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { ensureProjectConfig, readProjectConfig, renderSubagentBlock } from './project_config.mjs';
7
+
8
+ const begin = '<!-- orbit-thread:start -->';
9
+ const end = '<!-- orbit-thread:end -->';
10
+ const legacyBegin = '<!-- orbit-handoff:start -->';
11
+ const legacyEnd = '<!-- orbit-handoff:end -->';
12
+ const ignoreBegin = '# orbit-thread:start';
13
+ const ignoreEnd = '# orbit-thread:end';
14
+ const legacyIgnoreBegin = '# orbit-handoff:start';
15
+ const legacyIgnoreEnd = '# orbit-handoff:end';
16
+
17
+ const agentsBlock = config => `${begin}
18
+ ## Orbit Thread workflow
19
+
20
+ - Durable truth is the tracked source plus current living docs. \`HANDOFF-STATE.md\` is concise execution state only, never project history.
21
+ - Before substantial work, inspect current Git state and load context selectively:
22
+ - product behaviour, scope or copy → \`PRODUCT.md\`;
23
+ - UI/UX → \`DESIGN.md\`;
24
+ - runtime, data flow, infrastructure or integrations → \`ARCHITECTURE.md\`;
25
+ - auth, tenancy, secrets, public entry points or other security-sensitive work → \`SECURITY.md\`;
26
+ - future planning only → \`ROADMAP.md\`.
27
+ Use the matching file in \`docs/\` or the project root. For cross-cutting work, read only the additional docs it actually touches.
28
+ - Inspect only source files needed for the task. Do not broadly scan generated, vendor, dependency, build, coverage or cache directories, and do not reread unchanged context without a reason.
29
+ - Living docs describe CURRENT truth. After verified work changes that truth, update affected existing sections in place during the same milestone. Never append session diaries or duplicate superseded sections; Git is history. Never rewrite an applied migration.
30
+ - Run the smallest relevant verification before claiming a milestone or task complete. Never claim a test, deployment, commit, push or external action that did not run.
31
+ - Start each new coding task in a fresh chat and invoke \`$orbit-resume\` once at session start. Do this automatically where the host supports it; otherwise the reliable fallback is to invoke it manually. Do not repeat Resume in the same session unless the user asks for a fresh recovery.
32
+ - Run the installed \`handoff\` skill after a meaningful verified milestone that changed repository state or durable project truth, and after each completed coding task with such changes before the final reply. Also checkpoint before \`/clear\`, session end, or a usage/context stop when warning is available. Skip read-only questions and trivial edits. Intermediate checkpoints must not stop unfinished work.
33
+ - Each checkpoint overwrites \`HANDOFF-STATE.md\`. Resume reads the handoff once if present, checks current Git state, then reads only task-relevant living docs and source. Do not reconstruct old conversation history or repeat completed work.
34
+ ${renderSubagentBlock(config)}
35
+ - Read-only Git inspection is allowed. Commit, push, merge, rebase, reset, branch deletion or other Git writes require explicit authority in the current request. Never force-push or destructively clean without explicit authority.
36
+ - Never place secrets, credentials, environment values, private keys, recovery codes, personal data or sensitive production data in living docs or \`HANDOFF-STATE.md\`.
37
+ ${end}
38
+ `;
39
+
40
+ const claudeBlock = `${begin}
41
+ ## Orbit Thread
42
+
43
+ Read and follow \`AGENTS.md\` for project workflow, selective context loading, subagents and checkpoints. Start each new coding task in a fresh chat and invoke \`/orbit-resume\` once at session start, automatically where supported. If automatic invocation is unavailable, manually run \`/orbit-resume\` (or \`/orbit-thread:orbit-resume\` from the marketplace plugin). Do not repeat Resume in the same session unless the user asks for a fresh recovery.
44
+ ${end}
45
+ `;
46
+ const templates = {
47
+ 'PRODUCT.md': `# Product
48
+
49
+ > Living current-state document. Replace placeholders as product truth becomes known; do not append session history.
50
+
51
+ ## Purpose
52
+ - TODO: what the product does and who it serves.
53
+
54
+ ## Core workflows
55
+ - TODO: the main user-visible workflows.
56
+
57
+ ## Scope and non-goals
58
+ - TODO: what is in scope and explicitly out of scope.
59
+
60
+ ## Current constraints
61
+ - TODO: material product limits that affect implementation.
62
+ `,
63
+ 'ARCHITECTURE.md': `# Architecture
64
+
65
+ > Living current-state document. Update existing sections in place when the system changes.
66
+
67
+ ## System shape
68
+ - TODO: major runtime components and responsibilities.
69
+
70
+ ## Boundaries and data flow
71
+ - TODO: important trust, service and module boundaries.
72
+
73
+ ## Persistence and integrations
74
+ - TODO: databases, storage, queues and external services.
75
+
76
+ ## Operational constraints
77
+ - TODO: deployment, runtime and reliability constraints.
78
+ `,
79
+ 'SECURITY.md': `# Security
80
+
81
+ > Living current-state document. Record actual controls, invariants and known risks only.
82
+
83
+ ## Trust boundaries
84
+ - TODO: authentication, authorisation, tenancy and public-entry boundaries.
85
+
86
+ ## Data and secrets
87
+ - TODO: sensitive data handling, encryption, secret storage and logging rules.
88
+
89
+ ## Abuse and failure controls
90
+ - TODO: rate limits, anti-abuse, validation and fail-closed behaviour.
91
+
92
+ ## Known risks
93
+ - TODO: unresolved security limitations or verification still required.
94
+ `,
95
+ 'ROADMAP.md': `# Roadmap
96
+
97
+ > Forward-looking current plan only. Replace completed or superseded items instead of appending history.
98
+
99
+ ## Current phase
100
+ - TODO: the active project phase and outcome.
101
+
102
+ ## Next
103
+ 1. TODO: highest-priority next milestone.
104
+
105
+ ## Later
106
+ - TODO: worthwhile later work.
107
+
108
+ ## Explicitly deferred
109
+ - TODO: work intentionally not being done now.
110
+ `,
111
+ 'DESIGN.md': `# Design
112
+
113
+ > Living current-state design truth. Keep this concise and update it in place.
114
+
115
+ ## Direction
116
+ - TODO: visual/product design direction.
117
+
118
+ ## System
119
+ - TODO: typography, colour, spacing, components and interaction conventions.
120
+
121
+ ## Accessibility and responsive behaviour
122
+ - TODO: current accessibility and device rules.
123
+
124
+ ## Surface-specific decisions
125
+ - TODO: only durable decisions that affect future UI work.
126
+ `,
127
+ };
128
+
129
+ const exists = p => {
130
+ try { fs.lstatSync(p); return true; }
131
+ catch (error) { if (error.code === 'ENOENT') return false; throw error; }
132
+ };
133
+ const read = p => exists(p) ? fs.readFileSync(p, 'utf8') : '';
134
+
135
+ function safePath(p) {
136
+ let cursor = path.resolve(p);
137
+ while (true) {
138
+ if (exists(cursor) && fs.lstatSync(cursor).isSymbolicLink()) throw new Error(`Refusing symlink: ${cursor}`);
139
+ const parent = path.dirname(cursor);
140
+ if (parent === cursor) break;
141
+ cursor = parent;
142
+ }
143
+ }
144
+
145
+ function atomicWrite(p, content) {
146
+ safePath(p);
147
+ fs.mkdirSync(path.dirname(p), { recursive: true });
148
+ const temporary = path.join(path.dirname(p), `.orbit-thread-${crypto.randomUUID()}.tmp`);
149
+ try {
150
+ fs.writeFileSync(temporary, content, { flag: 'wx', mode: exists(p) ? fs.statSync(p).mode & 0o777 : 0o644 });
151
+ fs.renameSync(temporary, p);
152
+ } finally {
153
+ if (exists(temporary)) fs.unlinkSync(temporary);
154
+ }
155
+ }
156
+
157
+ export function resolveProjectRoot(cwd = process.cwd()) {
158
+ try {
159
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
160
+ cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
161
+ }).trim();
162
+ } catch {
163
+ return path.resolve(cwd);
164
+ }
165
+ }
166
+
167
+ function statePath(root) {
168
+ return path.join(root, '.orbit-thread/state.json');
169
+ }
170
+
171
+ function legacyStatePath(root) {
172
+ return path.join(root, '.orbit-handoff/state.json');
173
+ }
174
+
175
+ function loadState(root) {
176
+ const current = statePath(root);
177
+ const legacy = legacyStatePath(root);
178
+ if (exists(current) && exists(legacy)) throw new Error('Both Orbit Thread and legacy Orbit Handoff state files exist; reconcile them before setup.');
179
+ const source = exists(current) ? current : exists(legacy) ? legacy : null;
180
+ if (!source) return { state: { product: 'orbit-thread', schema: 3, configuredAgents: [], additions: {} }, source: null };
181
+
182
+ const parsed = JSON.parse(read(source));
183
+ if (!parsed.additions || typeof parsed.additions !== 'object' || Array.isArray(parsed.additions)) throw new Error(`Invalid Orbit Thread state: ${source}`);
184
+ for (const [name, addition] of Object.entries(parsed.additions)) {
185
+ if (!['AGENTS.md', 'CLAUDE.md', '.gitignore'].includes(name) || typeof addition.inserted !== 'string' || typeof addition.created !== 'boolean') {
186
+ throw new Error(`Invalid Orbit Thread ownership entry: ${source}`);
187
+ }
188
+ }
189
+
190
+ if (parsed.product === 'orbit-thread' && parsed.schema === 3) {
191
+ if (!Array.isArray(parsed.configuredAgents) || parsed.configuredAgents.some(agent => !['codex', 'claude'].includes(agent))) throw new Error(`Invalid Orbit Thread state: ${source}`);
192
+ return { state: parsed, source };
193
+ }
194
+
195
+ if (parsed.product === 'orbit-handoff' && [1, 2].includes(parsed.schema)) {
196
+ const configuredAgents = parsed.schema === 1
197
+ ? [...(parsed.additions['AGENTS.md'] ? ['codex'] : []), ...(parsed.additions['CLAUDE.md'] ? ['claude'] : [])]
198
+ : parsed.configuredAgents;
199
+ if (!Array.isArray(configuredAgents) || configuredAgents.some(agent => !['codex', 'claude'].includes(agent))) throw new Error(`Invalid legacy Orbit Handoff state: ${source}`);
200
+ return {
201
+ state: { product: 'orbit-thread', schema: 3, configuredAgents: [...new Set(configuredAgents)], additions: parsed.additions },
202
+ source,
203
+ };
204
+ }
205
+
206
+ throw new Error(`Unsupported Orbit Thread state: ${source}`);
207
+ }
208
+
209
+ function persistState(root, state, source = null) {
210
+ const p = statePath(root);
211
+ if (Object.keys(state.additions).length || state.configuredAgents.length) {
212
+ atomicWrite(p, JSON.stringify(state, null, 2) + '\n');
213
+ } else if (exists(p)) {
214
+ fs.unlinkSync(p);
215
+ const dir = path.dirname(p);
216
+ if (exists(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
217
+ }
218
+ const legacy = legacyStatePath(root);
219
+ if (source === legacy && exists(legacy)) {
220
+ fs.unlinkSync(legacy);
221
+ const dir = path.dirname(legacy);
222
+ if (exists(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
223
+ }
224
+ }
225
+ function desiredIgnoreBlock(before) {
226
+ const lines = before.split(/\r?\n/);
227
+ const missing = [];
228
+ if (!lines.includes('HANDOFF-STATE.md') && !lines.includes('/HANDOFF-STATE.md')) missing.push('HANDOFF-STATE.md');
229
+ if (!lines.includes('.orbit-thread/state.json') && !lines.includes('/.orbit-thread/state.json')) missing.push('.orbit-thread/state.json');
230
+ return missing.length ? `${ignoreBegin}\n${missing.join('\n')}\n${ignoreEnd}\n` : null;
231
+ }
232
+
233
+ function desiredBlock(name, config, before = '') {
234
+ if (name === 'AGENTS.md') return agentsBlock(config);
235
+ if (name === 'CLAUDE.md') return claudeBlock;
236
+ if (name === '.gitignore') return desiredIgnoreBlock(before);
237
+ throw new Error(`Unknown managed file: ${name}`);
238
+ }
239
+
240
+ function markersForInserted(name, inserted) {
241
+ if (name === '.gitignore') {
242
+ return inserted.includes(legacyIgnoreBegin) ? [legacyIgnoreBegin, legacyIgnoreEnd] : [ignoreBegin, ignoreEnd];
243
+ }
244
+ return inserted.includes(legacyBegin) ? [legacyBegin, legacyEnd] : [begin, end];
245
+ }
246
+
247
+ function hasAnyManagedMarker(name, text) {
248
+ const values = name === '.gitignore'
249
+ ? [ignoreBegin, ignoreEnd, legacyIgnoreBegin, legacyIgnoreEnd]
250
+ : [begin, end, legacyBegin, legacyEnd];
251
+ return values.some(value => text.includes(value));
252
+ }
253
+
254
+ function planManaged(root, state, name, config) {
255
+ const p = path.join(root, name);
256
+ safePath(p);
257
+ const before = read(p);
258
+ const owned = state.additions[name];
259
+
260
+ if (owned) {
261
+ if (!before.includes(owned.inserted) || before.indexOf(owned.inserted) !== before.lastIndexOf(owned.inserted)) {
262
+ throw new Error(`Managed Orbit Thread block was edited or duplicated in ${name}; review it before running setup.`);
263
+ }
264
+ const [first, last] = markersForInserted(name, owned.inserted);
265
+ if (before.split(first).length !== 2 || before.split(last).length !== 2) {
266
+ throw new Error(`Managed Orbit Thread block was edited or duplicated in ${name}; review it before running setup.`);
267
+ }
268
+ const previousInserted = owned.inserted;
269
+ const leading = previousInserted.match(/^(?:\r?\n)*/)?.[0] ?? '';
270
+ const baseText = before.slice(0, before.indexOf(previousInserted)) + before.slice(before.indexOf(previousInserted) + previousInserted.length);
271
+ const rawDesired = desiredBlock(name, config, baseText);
272
+ if (!rawDesired) {
273
+ delete state.additions[name];
274
+ return { p, name, content: baseText, action: 'updated' };
275
+ }
276
+ const inserted = leading + rawDesired;
277
+ if (inserted === previousInserted) return null;
278
+ state.additions[name].inserted = inserted;
279
+ return { p, name, content: before.replace(previousInserted, inserted), action: 'updated' };
280
+ }
281
+
282
+
283
+ if (hasAnyManagedMarker(name, before)) {
284
+ throw new Error(`Unowned or malformed Orbit Thread/legacy block in ${name}; refusing to overwrite it.`);
285
+ }
286
+
287
+ const rawDesired = desiredBlock(name, config, before);
288
+ if (!rawDesired) return null;
289
+ const inserted = (before ? (before.endsWith('\n') ? '\n' : '\n\n') : '') + rawDesired;
290
+ state.additions[name] = { inserted, created: !exists(p) };
291
+ return { p, name, content: before + inserted, action: exists(p) ? 'updated' : 'created' };
292
+ }
293
+
294
+ function ensureDocs(root) {
295
+ const results = [];
296
+ for (const [name, template] of Object.entries(templates)) {
297
+ const rootFile = path.join(root, name);
298
+ const docsFile = path.join(root, 'docs', name);
299
+ safePath(rootFile);
300
+ safePath(docsFile);
301
+ if (exists(rootFile)) {
302
+ results.push({ name: path.relative(root, rootFile), action: 'preserved' });
303
+ continue;
304
+ }
305
+ if (exists(docsFile)) {
306
+ results.push({ name: path.relative(root, docsFile), action: 'preserved' });
307
+ continue;
308
+ }
309
+ atomicWrite(docsFile, template);
310
+ results.push({ name: path.relative(root, docsFile), action: 'created' });
311
+ }
312
+ return results;
313
+ }
314
+
315
+ export function setupProject(root, selected = ['codex', 'claude']) {
316
+ root = path.resolve(root);
317
+ safePath(root);
318
+ const agents = [...new Set(selected)];
319
+ if (!agents.length || agents.some(agent => !['codex', 'claude'].includes(agent))) throw new Error('Agent must be codex, claude or both');
320
+
321
+ const loaded = loadState(root);
322
+ const state = loaded.state;
323
+ const config = readProjectConfig(root);
324
+ state.configuredAgents = [...new Set([...state.configuredAgents, ...agents])].sort();
325
+
326
+ const names = ['AGENTS.md', '.gitignore'];
327
+ if (state.configuredAgents.includes('claude')) names.push('CLAUDE.md');
328
+
329
+ const planned = names.map(name => planManaged(root, state, name, config)).filter(Boolean);
330
+ const docs = ensureDocs(root);
331
+
332
+ ensureProjectConfig(root);
333
+ for (const change of planned) atomicWrite(change.p, change.content);
334
+ persistState(root, state, loaded.source);
335
+
336
+ return { root, managed: planned.map(({ name, action }) => ({ name, action })), docs };
337
+ }
338
+
339
+ function removeAddition(root, state, name) {
340
+ const addition = state.additions[name];
341
+ if (!addition) return { name, action: 'not-managed' };
342
+ const p = path.join(root, name);
343
+ safePath(p);
344
+ if (!exists(p)) {
345
+ delete state.additions[name];
346
+ return { name, action: 'missing' };
347
+ }
348
+
349
+ const before = read(p);
350
+ const index = before.indexOf(addition.inserted);
351
+ if (index < 0 || index !== before.lastIndexOf(addition.inserted)) {
352
+ return { name, action: 'preserved-edited' };
353
+ }
354
+
355
+ const after = before.slice(0, index) + before.slice(index + addition.inserted.length);
356
+ if (!after && addition.created) fs.unlinkSync(p);
357
+ else atomicWrite(p, after);
358
+ delete state.additions[name];
359
+ return { name, action: 'removed' };
360
+ }
361
+
362
+ export function removeProjectConfig(root, selected = ['codex', 'claude']) {
363
+ root = path.resolve(root);
364
+ const loaded = loadState(root);
365
+ const state = loaded.state;
366
+ const remove = new Set(selected);
367
+ state.configuredAgents = state.configuredAgents.filter(agent => !remove.has(agent));
368
+
369
+ const results = [];
370
+ if (!state.configuredAgents.includes('claude')) results.push(removeAddition(root, state, 'CLAUDE.md'));
371
+ if (!state.configuredAgents.length) {
372
+ results.push(removeAddition(root, state, 'AGENTS.md'));
373
+ results.push(removeAddition(root, state, '.gitignore'));
374
+ }
375
+
376
+ persistState(root, state, loaded.source);
377
+ return results;
378
+ }
379
+
380
+ function gitTracked(root, relative) {
381
+ try {
382
+ execFileSync('git', ['ls-files', '--error-unmatch', relative], {
383
+ cwd: root, stdio: ['ignore', 'ignore', 'ignore'],
384
+ });
385
+ return true;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+
391
+ export function checkProject(root, selected = ['codex', 'claude']) {
392
+ root = path.resolve(root);
393
+ const loaded = loadState(root);
394
+ const state = loaded.state;
395
+ const messages = [];
396
+ let ok = true;
397
+ if (loaded.source === legacyStatePath(root)) { messages.push('project state: legacy Orbit Handoff state detected; run setup to migrate'); ok = false; }
398
+
399
+ try {
400
+ const config = readProjectConfig(root);
401
+ messages.push(`config: valid (max subagents ${config.subagents.max})`);
402
+ if (!exists(path.join(root, '.orbit-thread/config.json'))) {
403
+ messages.push('config: default only; run setup to create .orbit-thread/config.json');
404
+ ok = false;
405
+ }
406
+ } catch (error) {
407
+ messages.push(`config: invalid (${error.message})`);
408
+ ok = false;
409
+ }
410
+
411
+ for (const agent of selected) {
412
+ const configured = state.configuredAgents.includes(agent);
413
+ messages.push(`${agent}: ${configured ? 'project workflow configured' : 'project workflow not configured'}`);
414
+ if (!configured) ok = false;
415
+ }
416
+
417
+ for (const name of ['AGENTS.md', ...(selected.includes('claude') ? ['CLAUDE.md'] : [])]) {
418
+ const addition = state.additions[name];
419
+ const intact = addition && read(path.join(root, name)).includes(addition.inserted);
420
+ messages.push(`${name}: ${intact ? 'managed block intact' : 'managed block missing or edited'}`);
421
+ if (!intact) ok = false;
422
+ }
423
+
424
+ const ignore = read(path.join(root, '.gitignore'));
425
+ const ignoreLines = ignore.split(/\r?\n/);
426
+ const handoffIgnored = ignoreLines.includes('HANDOFF-STATE.md') || ignoreLines.includes('/HANDOFF-STATE.md');
427
+ const stateIgnored = ignoreLines.includes('.orbit-thread/state.json') || ignoreLines.includes('/.orbit-thread/state.json');
428
+ messages.push(`HANDOFF-STATE.md: ${handoffIgnored ? 'ignored' : 'ignore rule missing'}`);
429
+ messages.push(`.orbit-thread/state.json: ${stateIgnored ? 'ignored' : 'ignore rule missing'}`);
430
+ if (!handoffIgnored || !stateIgnored) ok = false;
431
+
432
+ if (gitTracked(root, 'HANDOFF-STATE.md')) {
433
+ messages.push('HANDOFF-STATE.md: tracked by Git (should remain local)');
434
+ ok = false;
435
+ }
436
+
437
+ for (const name of Object.keys(templates)) {
438
+ const rootFile = exists(path.join(root, name));
439
+ const docsFile = exists(path.join(root, 'docs', name));
440
+ const present = rootFile || docsFile;
441
+ messages.push(`${name}: ${present ? 'present' : 'missing'}${rootFile && docsFile ? ' (duplicate root/docs copies)' : ''}`);
442
+ if (!present || (rootFile && docsFile)) ok = false;
443
+ }
444
+
445
+ const handoff = path.join(root, 'HANDOFF-STATE.md');
446
+ if (exists(handoff)) {
447
+ const text = read(handoff);
448
+ const lines = text ? text.split(/\r?\n/).length - (text.endsWith('\n') ? 1 : 0) : 0;
449
+ messages.push(`handoff: ${lines} line${lines === 1 ? '' : 's'}`);
450
+ if (!text || lines > 50) ok = false;
451
+ } else {
452
+ messages.push('handoff: not created yet');
453
+ }
454
+
455
+ return { ok, messages };
456
+ }
457
+ function parseAgent(value) {
458
+ if (value === 'both') return ['codex', 'claude'];
459
+ if (['codex', 'claude'].includes(value)) return [value];
460
+ throw new Error('Agent must be codex, claude or both');
461
+ }
462
+
463
+ async function runCli(argv) {
464
+ let command = 'setup';
465
+ let agent = 'both';
466
+ if (argv[0] && !argv[0].startsWith('-')) command = argv.shift();
467
+ while (argv.length) {
468
+ const flag = argv.shift();
469
+ if (flag === '--agent') agent = argv.shift();
470
+ else throw new Error(`Unknown option: ${flag}`);
471
+ }
472
+
473
+ const root = resolveProjectRoot();
474
+ const selected = parseAgent(agent);
475
+
476
+ if (command === 'setup') {
477
+ const result = setupProject(root, selected);
478
+ for (const item of [...result.managed, ...result.docs]) console.log(`${item.action}: ${item.name}`);
479
+ console.log('Orbit Thread setup complete. HANDOFF-STATE.md will be created by the handoff skill at the first checkpoint.');
480
+ return;
481
+ }
482
+ if (command === 'check') {
483
+ const result = checkProject(root, selected);
484
+ result.messages.forEach(message => console.log(message));
485
+ if (!result.ok) process.exitCode = 1;
486
+ return;
487
+ }
488
+ if (command === 'remove') {
489
+ removeProjectConfig(root, selected).forEach(item => console.log(`${item.action}: ${item.name}`));
490
+ return;
491
+ }
492
+ throw new Error(`Unknown setup command: ${command}`);
493
+ }
494
+
495
+ const direct = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
496
+ if (direct) {
497
+ runCli(process.argv.slice(2)).catch(error => {
498
+ console.error(error.message);
499
+ process.exitCode = 1;
500
+ });
501
+ }