@atolljs/cli 0.0.0-stage → 0.1.4

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/dist/cli.js ADDED
@@ -0,0 +1,2261 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/io.ts
4
+ import { createInterface } from "node:readline/promises";
5
+ import { stdin, stdout, stderr } from "node:process";
6
+ import { styleText } from "node:util";
7
+ var PLAIN_FMT = { strong: (s) => s, dim: (s) => s, accent: (s) => s };
8
+ function consoleIo(interactive) {
9
+ const color = process.env.NO_COLOR === void 0 && (process.env.FORCE_COLOR !== void 0 || stdout.isTTY === true);
10
+ const style = (f) => (s) => color ? styleText(f, s) : s;
11
+ const fmt = color ? { strong: style("bold"), dim: style("dim"), accent: style("cyan") } : PLAIN_FMT;
12
+ const okMark = style("green")(" \u2713 ");
13
+ const warnMark = style("yellow")(" ! ");
14
+ const errorMark = style("red")(" \u2717 ");
15
+ const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
16
+ return {
17
+ interactive,
18
+ fmt,
19
+ print: (msg = "") => stdout.write(msg + "\n"),
20
+ ok: (msg) => stdout.write(`${okMark} ${msg}
21
+ `),
22
+ warn: (msg) => stdout.write(`${warnMark} ${msg}
23
+ `),
24
+ error: (msg) => stderr.write(`${errorMark} ${msg}
25
+ `),
26
+ async ask(question, fallback = "") {
27
+ if (!rl) return fallback;
28
+ const answer = (await rl.question(`${fmt.strong(question)} `)).trim();
29
+ return answer || fallback;
30
+ },
31
+ async confirm(question, def = false) {
32
+ if (!rl) return def;
33
+ const hint = fmt.dim(`[${def ? "Y/n" : "y/N"}]`);
34
+ const answer = (await rl.question(`${fmt.strong(question)} ${hint} `)).trim().toLowerCase();
35
+ return answer === "" ? def : answer === "y" || answer === "yes";
36
+ },
37
+ async select(question, choices, def) {
38
+ if (!rl) return def;
39
+ stdout.write(fmt.strong(question) + "\n");
40
+ choices.forEach(
41
+ (c, i) => stdout.write(
42
+ ` ${fmt.accent(String(i + 1))}) ${c}${c === def ? fmt.dim(" (default)") : ""}
43
+ `
44
+ )
45
+ );
46
+ const answer = (await rl.question(`${fmt.dim(`choose [1-${choices.length}]:`)} `)).trim();
47
+ if (answer === "") return def;
48
+ const pick = choices[Number(answer) - 1] ?? choices.find((c) => c === answer);
49
+ return pick ?? def;
50
+ },
51
+ close: () => rl?.close()
52
+ };
53
+ }
54
+
55
+ // src/parse.ts
56
+ function parseArgs(argv) {
57
+ const _ = [];
58
+ const flags = {};
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const a = argv[i];
61
+ if (a === "--") {
62
+ _.push(...argv.slice(i + 1));
63
+ break;
64
+ }
65
+ if (a.startsWith("--")) {
66
+ const eq = a.indexOf("=");
67
+ if (eq !== -1) {
68
+ flags[a.slice(2, eq)] = a.slice(eq + 1);
69
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
70
+ flags[a.slice(2)] = argv[++i];
71
+ } else {
72
+ flags[a.slice(2)] = true;
73
+ }
74
+ } else if (a.startsWith("-") && a.length > 1) {
75
+ for (const ch of a.slice(1)) flags[ch] = true;
76
+ } else {
77
+ _.push(a);
78
+ }
79
+ }
80
+ return { _, flags };
81
+ }
82
+ var flag = (args, name) => typeof args.flags[name] === "string" ? args.flags[name] : void 0;
83
+ var hasFlag = (args, ...names) => names.some((n) => args.flags[n] === true || typeof args.flags[n] === "string");
84
+ var camel = (name) => name.replace(/[-_](.)/g, (_m, c) => c.toUpperCase());
85
+ var pascal = (name) => {
86
+ const c = camel(name);
87
+ return c.charAt(0).toUpperCase() + c.slice(1);
88
+ };
89
+ var validName = (name) => /^[a-z][a-z0-9]*([-_][a-z0-9]+)*$/.test(name);
90
+
91
+ // src/commands.ts
92
+ import { spawnSync } from "node:child_process";
93
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
94
+ import { join as join3, relative } from "node:path";
95
+
96
+ // src/project.ts
97
+ import { existsSync, readFileSync } from "node:fs";
98
+ import { dirname, join } from "node:path";
99
+ function findRoot(cwd) {
100
+ let dir = cwd;
101
+ for (; ; ) {
102
+ if (existsSync(join(dir, "package.json"))) return dir;
103
+ const parent = dirname(dir);
104
+ if (parent === dir) return null;
105
+ dir = parent;
106
+ }
107
+ }
108
+ function readPkg(dir) {
109
+ try {
110
+ return JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ var DEPS_OF = (pkg) => ({
116
+ ...pkg?.dependencies ?? {},
117
+ ...pkg?.devDependencies ?? {}
118
+ });
119
+ var DETECT = [
120
+ ["nestjs", ["@nestjs/core", "@nestjs/common"]],
121
+ ["nextjs", ["next"]],
122
+ ["angular", ["@angular/core"]],
123
+ ["svelte", ["svelte"]],
124
+ ["solid", ["solid-js"]],
125
+ ["vue", ["vue"]],
126
+ ["react", ["react", "react-dom"]],
127
+ ["node", ["@atolljs/node", "express", "fastify", "hono", "koa", "@nestjs/core"]]
128
+ ];
129
+ function detectFramework(pkg) {
130
+ const deps = DEPS_OF(pkg);
131
+ for (const [fw, names] of DETECT) {
132
+ if (names.some((n) => deps[n] !== void 0)) return fw;
133
+ }
134
+ return "vanilla";
135
+ }
136
+ function detectPm(root) {
137
+ if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
138
+ if (existsSync(join(root, "yarn.lock"))) return "yarn";
139
+ if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock"))) return "bun";
140
+ return "npm";
141
+ }
142
+ function detectProject(cwd) {
143
+ const root = findRoot(cwd);
144
+ if (!root) {
145
+ return { root: null, pkg: null, framework: "vanilla", pm: "npm", srcDir: cwd };
146
+ }
147
+ const pkg = readPkg(root);
148
+ return {
149
+ root,
150
+ pkg,
151
+ framework: detectFramework(pkg),
152
+ pm: detectPm(root),
153
+ srcDir: existsSync(join(root, "src")) ? join(root, "src") : root
154
+ };
155
+ }
156
+ var ATOLL_DEPS = {
157
+ react: ["@atolljs/core", "@atolljs/react", "@atolljs/react-island", "zod"],
158
+ vue: ["@atolljs/core", "@atolljs/vue", "@atolljs/vue-island", "zod"],
159
+ solid: ["@atolljs/core", "@atolljs/solidjs", "@atolljs/solid-island", "zod"],
160
+ svelte: ["@atolljs/core", "@atolljs/svelte", "@atolljs/svelte-island", "zod"],
161
+ angular: ["@atolljs/core", "@atolljs/angular", "@atolljs/angular-island", "zod"],
162
+ nextjs: ["@atolljs/core", "@atolljs/nextjs", "zod"],
163
+ nestjs: ["@atolljs/core", "@atolljs/nestjs", "@atolljs/node", "zod"],
164
+ node: ["@atolljs/core", "@atolljs/node", "zod"],
165
+ vanilla: ["@atolljs/core", "zod"]
166
+ };
167
+ var ISLAND_FRAMEWORKS = [
168
+ "react",
169
+ "vue",
170
+ "solid",
171
+ "svelte",
172
+ "angular"
173
+ ];
174
+ var pmInstallCmd = (pm, pkgs) => `${pm} ${pm === "npm" ? "install" : "add"} ${pkgs.join(" ")}`;
175
+ function missingDeps(pkg, want) {
176
+ const deps = DEPS_OF(pkg);
177
+ return want.filter((d) => deps[d] === void 0);
178
+ }
179
+
180
+ // src/files.ts
181
+ import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
182
+ import { dirname as dirname2, join as join2, normalize } from "node:path";
183
+ async function writeTree(io, cwd, files, opts = {}) {
184
+ for (const f of files) {
185
+ const abs = normalize(join2(cwd, f.path));
186
+ if (!abs.startsWith(normalize(cwd))) {
187
+ io.error(`refusing to write outside the project: ${io.fmt.accent(f.path)}`);
188
+ continue;
189
+ }
190
+ if (existsSync2(abs) && !opts.force) {
191
+ const overwrite = io.interactive ? await io.confirm(`${io.fmt.accent(f.path)} exists \u2014 overwrite?`, false) : false;
192
+ if (!overwrite) {
193
+ io.warn(
194
+ `skipped ${io.fmt.accent(f.path)} (exists \u2014 pass ${io.fmt.accent("--force")} to overwrite)`
195
+ );
196
+ continue;
197
+ }
198
+ }
199
+ mkdirSync(dirname2(abs), { recursive: true });
200
+ writeFileSync(abs, f.content);
201
+ io.ok(`created ${io.fmt.accent(f.path)}`);
202
+ }
203
+ }
204
+
205
+ // src/templates.ts
206
+ var NPMRC = `# Supply-chain hardening \u2014 read by every npm command run in this directory.
207
+ #
208
+ # Newly published versions must age 7 days before they can enter the
209
+ # lockfile (requires npm >= 11.10 \u2014 shipped with Node 26; older npm
210
+ # silently ignores this key).
211
+ min-release-age=7
212
+ # Install lifecycle scripts are the dominant malware vector \u2014 none run
213
+ # during install/ci. If a native dep ever legitimately needs one:
214
+ # npm rebuild <pkg> --ignore-scripts=false
215
+ ignore-scripts=true
216
+ # \`npm install <pkg>\` writes exact pins, not ^ ranges.
217
+ save-exact=true
218
+ fund=false
219
+ `;
220
+ var GITIGNORE = `node_modules/
221
+ dist/
222
+ `;
223
+ function memoryContract(name) {
224
+ const c = camel(name);
225
+ return `import { defineSharedMemory, field, mz } from '@atolljs/core';
226
+
227
+ /**
228
+ * ${c} shared memory \u2014 the single source of truth for state both threads
229
+ * touch. The spec compiles to one deterministic fixed-width layout; main
230
+ * thread and workers bind the same declaration, so the two sides can never
231
+ * disagree on byte offsets.
232
+ */
233
+ export const ${c}Memory = defineSharedMemory({
234
+ // Versioned scalars \u2014 observe()/watch() park on Atomics; zero postMessage.
235
+ signals: {
236
+ progress: field.number(),
237
+ },
238
+ // Structured snapshots \u2014 one inline record, rewritten wholesale.
239
+ state: {
240
+ summary: field.object({
241
+ schema: mz.object({
242
+ total: mz.f64(),
243
+ errors: mz.f64(),
244
+ updatedAt: mz.u32(),
245
+ }),
246
+ }),
247
+ },
248
+ });
249
+ export type ${pascal(name)}Memory = typeof ${c}Memory;
250
+ `;
251
+ }
252
+ function workerEntry(name, opts) {
253
+ const c = camel(name);
254
+ const memImport = opts.memoryImport ? `import { ${opts.memoryName} } from '${opts.memoryImport}';
255
+ ` : "";
256
+ const memLine = opts.memoryName ? ` sharedMemory: ${opts.memoryName},
257
+ ` : "";
258
+ const shim = opts.host === "node" ? `// Node worker entry \u2014 the shim binds self = parentPort BEFORE
259
+ // defineWorker's bootstrap evaluates; import order is the contract.
260
+ import '@atolljs/node/shim';
261
+ ` : "";
262
+ return `${shim}import { defineWorker, serviceMethod } from '@atolljs/core';
263
+ import { z } from 'zod';
264
+ ${memImport}
265
+ const echo = serviceMethod({
266
+ def: { argsSchema: z.tuple([z.string()]), resultSchema: z.string() },
267
+ run: (message) => {${opts.demoMemoryWrite && opts.memoryName ? `
268
+ // Shared memory crosses the boundary without a message \u2014 the main
269
+ // thread can read these the moment the task resolves.
270
+ ${opts.memoryName}.signals.progress.write(100);
271
+ ${opts.memoryName}.state.summary.write({
272
+ total: 1,
273
+ errors: 0,
274
+ updatedAt: Math.floor(Date.now() / 1000),
275
+ });` : ""}
276
+ return \`worker heard: \${message}\`;
277
+ },
278
+ });
279
+
280
+ /**
281
+ * ${c} worker entry \u2014 defineWorker installs the message loop and registers
282
+ * every method. The main thread imports only the TYPE (typeof ${c}Worker)
283
+ * and drives calls through the pool client.
284
+ */
285
+ export const ${c}Worker = defineWorker({
286
+ ${memLine} methods: { echo },
287
+ });
288
+ export type ${pascal(name)}Worker = typeof ${c}Worker;
289
+ `;
290
+ }
291
+ function workerClientFile(name, opts) {
292
+ const c = camel(name);
293
+ const P = pascal(name);
294
+ const memImport = opts.memoryImport ? `import { ${opts.memoryName} } from '${opts.memoryImport}';
295
+ ` : "";
296
+ const memLine = opts.memoryName ? ` sharedMemory: ${opts.memoryName},
297
+ ` : "";
298
+ if (opts.host === "node") {
299
+ return `import { Worker } from 'node:worker_threads';
300
+ import { workerClient, defineTask } from '@atolljs/core';
301
+ import { createNodePool } from '@atolljs/node';
302
+ ${memImport}import type { ${P}Worker } from './${name}.worker';
303
+
304
+ /**
305
+ * ${c} pool \u2014 node:worker_threads workers adapted to the pool surface.
306
+ * The worker entry must be bundled before spawning (Node workers run
307
+ * outside tsx's loader):
308
+ *
309
+ * esbuild src/atoll/${name}.worker.ts --bundle --platform=node \\
310
+ * --format=esm --packages=external --outfile=dist/${name}.worker.js
311
+ */
312
+ export const ${c}Pool = createNodePool({
313
+ worker: () => new Worker(new URL('${opts.distUrl ?? `./dist/${name}.worker.js`}', import.meta.url)),
314
+ ${memLine} poolSize: 'auto',
315
+ });
316
+
317
+ /** Typed client \u2014 calls read like the worker's own method names. */
318
+ export const ${c} = workerClient<${P}Worker>(${c}Pool);
319
+
320
+ /** Latest-wins task over a call \u2014 { data, pending, settled, elapsedMs }. */
321
+ export const ${c}EchoTask = defineTask((message: string) => ${c}.echo(message));
322
+ `;
323
+ }
324
+ return `import { connectWorker, defineTask } from '@atolljs/core';
325
+ ${memImport}import type { ${P}Worker } from './${name}.worker';
326
+
327
+ /**
328
+ * ${c} \u2014 typed client over the worker pool. Spawning is lazy, so importing
329
+ * this module never touches Worker (SSR-safe); the first method call (or
330
+ * ${c}.start()) brings the pool up. Keep the new Worker(new URL(...)) call
331
+ * inline \u2014 bundlers detect worker entries by that exact shape.
332
+ */
333
+ export const ${c} = connectWorker<${P}Worker>({
334
+ worker: () =>
335
+ new Worker(new URL('./${name}.worker.ts', import.meta.url), { type: 'module' }),
336
+ ${memLine} poolSize: 'auto',
337
+ });
338
+ export type ${P}Client = typeof ${c};
339
+
340
+ /** Latest-wins task over a call \u2014 { data, pending, settled, elapsedMs }. */
341
+ export const ${c}EchoTask = defineTask((message: string) => ${c}.echo(message));
342
+ `;
343
+ }
344
+ var ISLAND_EXT = {
345
+ react: "tsx",
346
+ vue: "ts",
347
+ solid: "tsx",
348
+ svelte: "ts",
349
+ angular: "ts"
350
+ };
351
+ function islandFiles(name, fw, dir = "src/islands") {
352
+ const P = pascal(name);
353
+ switch (fw) {
354
+ case "react":
355
+ return [
356
+ {
357
+ path: `${dir}/${name}.worker.tsx`,
358
+ content: `import { useState } from 'react';
359
+ import { defineReactMonoWorker } from '@atolljs/react-island/worker';
360
+ import { emit } from '@atolljs/islands/worker';
361
+
362
+ /**
363
+ * ${P} \u2014 a real React component rendered INSIDE the worker. Mutations
364
+ * serialize to ops; the shell driver replays them into real DOM.
365
+ * Handlers receive the plain wire payload, not a SyntheticEvent.
366
+ */
367
+ function ${P}({ label = '${name}' }: { label?: string }) {
368
+ const [count, setCount] = useState(0);
369
+ return (
370
+ <div>
371
+ <span>
372
+ {label}: {count}
373
+ </span>
374
+ <button
375
+ onClick={() => {
376
+ const n = count + 1;
377
+ setCount(n);
378
+ emit('incremented', { count: n });
379
+ }}
380
+ >
381
+ increment
382
+ </button>
383
+ </div>
384
+ );
385
+ }
386
+
387
+ export const worker = defineReactMonoWorker(${P});
388
+ `
389
+ }
390
+ ];
391
+ case "vue":
392
+ return [
393
+ {
394
+ path: `${dir}/${P}.vue`,
395
+ content: `<script setup lang="ts">
396
+ import { ref } from 'vue';
397
+ import { emit } from '@atolljs/islands/worker';
398
+
399
+ // Rendered inside the worker \u2014 DOM ops stream to the shell; emit()
400
+ // sends app events back to it.
401
+ defineProps<{ label?: string }>();
402
+ const count = ref(0);
403
+ function increment() {
404
+ count.value += 1;
405
+ emit('incremented', { count: count.value });
406
+ }
407
+ </script>
408
+
409
+ <template>
410
+ <div>
411
+ <span>{{ label ?? '${name}' }}: {{ count }}</span>
412
+ <button @click="increment">increment</button>
413
+ </div>
414
+ </template>
415
+ `
416
+ },
417
+ {
418
+ path: `${dir}/${name}.worker.ts`,
419
+ content: `import { defineVueMonoWorker } from '@atolljs/vue-island/worker';
420
+ import ${P} from './${P}.vue';
421
+
422
+ export const worker = defineVueMonoWorker(${P});
423
+ `
424
+ }
425
+ ];
426
+ case "solid":
427
+ return [
428
+ {
429
+ path: `${dir}/${name}.worker.tsx`,
430
+ content: `import { createSignal } from 'solid-js';
431
+ import { defineSolidMonoWorker } from '@atolljs/solid-island/worker';
432
+ import { emit } from '@atolljs/islands/worker';
433
+
434
+ /** ${P} \u2014 Solid rendering inside the worker; ops stream to the shell. */
435
+ function ${P}(props: { label?: string }) {
436
+ const [count, setCount] = createSignal(0);
437
+ return (
438
+ <div>
439
+ <span>
440
+ {props.label ?? '${name}'}: {count()}
441
+ </span>
442
+ <button
443
+ onClick={() => {
444
+ const n = count() + 1;
445
+ setCount(n);
446
+ emit('incremented', { count: n });
447
+ }}
448
+ >
449
+ increment
450
+ </button>
451
+ </div>
452
+ );
453
+ }
454
+
455
+ export const worker = defineSolidMonoWorker(${P});
456
+ `
457
+ }
458
+ ];
459
+ case "svelte":
460
+ return [
461
+ {
462
+ path: `${dir}/${P}.svelte`,
463
+ content: `<script lang="ts">
464
+ import { emit } from '@atolljs/islands/worker';
465
+
466
+ // Rendered inside the worker \u2014 DOM ops stream to the shell.
467
+ let { label = '${name}' }: { label?: string } = $props();
468
+ let count = $state(0);
469
+ function increment() {
470
+ count += 1;
471
+ emit('incremented', { count });
472
+ }
473
+ </script>
474
+
475
+ <div>
476
+ <span>{label}: {count}</span>
477
+ <button onclick={increment}>increment</button>
478
+ </div>
479
+ `
480
+ },
481
+ {
482
+ path: `${dir}/${name}.worker.ts`,
483
+ content: `import { defineSvelteMonoWorker } from '@atolljs/svelte-island/worker';
484
+ import ${P} from './${P}.svelte';
485
+
486
+ export const worker = defineSvelteMonoWorker(${P});
487
+ `
488
+ }
489
+ ];
490
+ case "angular":
491
+ return [
492
+ {
493
+ path: `${dir}/${name}.worker.ts`,
494
+ content: `import { Component } from '@angular/core';
495
+ import { defineMonoWorker } from '@atolljs/islands/worker';
496
+ import { angularIslandApp } from '@atolljs/angular-island/worker';
497
+
498
+ /**
499
+ * ${P}Component \u2014 rendered inside the worker by Angular's own engine;
500
+ * ops stream to the shell driver on the main thread.
501
+ */
502
+ @Component({
503
+ template: \`
504
+ <div>
505
+ <span>{{ label }}: {{ count }}</span>
506
+ <button (click)="increment()">increment</button>
507
+ </div>
508
+ \`,
509
+ })
510
+ export class ${P}Component {
511
+ label = '${name}';
512
+ count = 0;
513
+ increment() {
514
+ this.count += 1;
515
+ }
516
+ }
517
+
518
+ export const worker = defineMonoWorker(angularIslandApp(${P}Component));
519
+ `
520
+ }
521
+ ];
522
+ default:
523
+ return [];
524
+ }
525
+ }
526
+ function islandUsage(name, fw) {
527
+ const P = pascal(name);
528
+ const ext = ISLAND_EXT[fw] ?? "ts";
529
+ const factory = `() => new Worker(new URL('./islands/${name}.worker.${ext}', import.meta.url), { type: 'module' })`;
530
+ switch (fw) {
531
+ case "react":
532
+ return `// mount it in your shell \u2014 worker factory stays inline so the bundler sees it
533
+ import { Island } from '@atolljs/react-island';
534
+
535
+ <Island app="${name}" worker={${factory}} onEvent={(n, p) => console.log(n, p)} />`;
536
+ case "vue":
537
+ return `<script setup>
538
+ import { AtollIsland } from '@atolljs/vue-island';
539
+ const worker = ${factory};
540
+ </script>
541
+
542
+ <template>
543
+ <AtollIsland app="${name}" :worker="worker" />
544
+ </template>`;
545
+ case "solid":
546
+ return `import { Island } from '@atolljs/solid-island';
547
+
548
+ <Island app="${name}" worker={${factory}} />`;
549
+ case "svelte":
550
+ return `<script lang="ts">
551
+ import { island } from '@atolljs/svelte-island';
552
+ const worker = ${factory};
553
+ </script>
554
+
555
+ <div use:island={{ app: '${name}', worker }} />`;
556
+ case "angular":
557
+ return `import { islandComponent } from '@atolljs/angular-island';
558
+ import type { ${P}Component } from './islands/${name}.worker';
559
+
560
+ export const ${P}Island = islandComponent<${P}Component>({
561
+ app: '${name}',
562
+ worker: ${factory},
563
+ });
564
+ // then use the generated component in a template \u2014 see docs/consumer Angular islands`;
565
+ default:
566
+ return "";
567
+ }
568
+ }
569
+ var HEADERS = ` // SharedArrayBuffer is gated behind cross-origin isolation \u2014 these
570
+ // headers unlock it; islands/pools degrade gracefully without them.
571
+ server: {
572
+ headers: {
573
+ 'Cross-Origin-Opener-Policy': 'same-origin',
574
+ 'Cross-Origin-Embedder-Policy': 'require-corp',
575
+ },
576
+ },`;
577
+ var SCAFFOLDS = {
578
+ react: {
579
+ deps: { react: "^19.3.0", "react-dom": "^19.3.0", "react-reconciler": "^0.34.0", "@atolljs/core": "^0.1.0", "@atolljs/react": "^0.1.0", "@atolljs/react-island": "^0.1.0", zod: "^4.6.5" },
580
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@vitejs/plugin-react": "^6.1.1", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0" },
581
+ vitePlugin: { pkg: "@vitejs/plugin-react", import_: `import react from '@vitejs/plugin-react';`, call: "react()" }
582
+ },
583
+ vue: {
584
+ deps: { vue: "^3.5.43", "@atolljs/core": "^0.1.0", "@atolljs/vue": "^0.1.0", "@atolljs/vue-island": "^0.1.0", zod: "^4.6.5" },
585
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@vitejs/plugin-vue": "^6.0.9" },
586
+ vitePlugin: { pkg: "@vitejs/plugin-vue", import_: `import vue from '@vitejs/plugin-vue';`, call: "vue()" }
587
+ },
588
+ solid: {
589
+ deps: { "solid-js": "^1.9.15", "@atolljs/core": "^0.1.0", "@atolljs/solidjs": "^0.1.0", "@atolljs/solid-island": "^0.1.0", zod: "^4.6.5" },
590
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "vite-plugin-solid": "^2.11.14" },
591
+ vitePlugin: { pkg: "vite-plugin-solid", import_: `import solid from 'vite-plugin-solid';`, call: "solid()" }
592
+ },
593
+ svelte: {
594
+ deps: { svelte: "^5.57.1", "@atolljs/core": "^0.1.0", "@atolljs/svelte": "^0.1.0", "@atolljs/svelte-island": "^0.1.0", zod: "^4.6.5" },
595
+ devDeps: { vite: "^8.3.1", typescript: "^7.0.2", "@sveltejs/vite-plugin-svelte": "^7.3.1" },
596
+ vitePlugin: { pkg: "@sveltejs/vite-plugin-svelte", import_: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: "svelte()" }
597
+ }
598
+ };
599
+ var SCAFFOLD_FRAMEWORKS = Object.keys(SCAFFOLDS);
600
+ function pkgJson(appName, spec) {
601
+ return JSON.stringify(
602
+ {
603
+ name: appName,
604
+ private: true,
605
+ type: "module",
606
+ scripts: { dev: "vite", build: "vite build", preview: "vite preview" },
607
+ dependencies: spec.deps,
608
+ devDependencies: spec.devDeps
609
+ },
610
+ null,
611
+ 2
612
+ ) + "\n";
613
+ }
614
+ var TSCONFIGS = {
615
+ react: {
616
+ compilerOptions: {
617
+ target: "ES2022",
618
+ module: "ESNext",
619
+ moduleResolution: "bundler",
620
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
621
+ jsx: "react-jsx",
622
+ strict: true,
623
+ noEmit: true,
624
+ isolatedModules: true,
625
+ skipLibCheck: true,
626
+ allowImportingTsExtensions: true
627
+ },
628
+ include: ["src"]
629
+ },
630
+ vue: {
631
+ compilerOptions: {
632
+ target: "ES2022",
633
+ module: "ESNext",
634
+ moduleResolution: "bundler",
635
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
636
+ strict: true,
637
+ noEmit: true,
638
+ isolatedModules: true,
639
+ skipLibCheck: true,
640
+ allowImportingTsExtensions: true
641
+ },
642
+ include: ["src"]
643
+ },
644
+ solid: {
645
+ compilerOptions: {
646
+ target: "ES2022",
647
+ module: "ESNext",
648
+ moduleResolution: "bundler",
649
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
650
+ jsx: "preserve",
651
+ jsxImportSource: "solid-js",
652
+ strict: true,
653
+ noEmit: true,
654
+ isolatedModules: true,
655
+ skipLibCheck: true,
656
+ allowImportingTsExtensions: true
657
+ },
658
+ include: ["src"]
659
+ },
660
+ svelte: {
661
+ compilerOptions: {
662
+ target: "ES2022",
663
+ module: "ESNext",
664
+ moduleResolution: "bundler",
665
+ lib: ["ESNext", "DOM", "DOM.Iterable"],
666
+ strict: true,
667
+ noEmit: true,
668
+ isolatedModules: true,
669
+ skipLibCheck: true,
670
+ allowImportingTsExtensions: true,
671
+ types: ["vite/client"]
672
+ },
673
+ include: ["src"]
674
+ },
675
+ node: {
676
+ compilerOptions: {
677
+ target: "ES2022",
678
+ module: "ESNext",
679
+ moduleResolution: "bundler",
680
+ // DOM+WebWorker libs — @atolljs/node types pool workers against the
681
+ // DOM Worker surface; without them its shipped source won't typecheck.
682
+ lib: ["ESNext", "DOM", "WebWorker"],
683
+ types: ["node"],
684
+ strict: true,
685
+ noEmit: true,
686
+ isolatedModules: true,
687
+ skipLibCheck: true,
688
+ allowImportingTsExtensions: true
689
+ },
690
+ include: ["src"]
691
+ }
692
+ };
693
+ function viteConfig(fw) {
694
+ const spec = SCAFFOLDS[fw];
695
+ const plugin = spec.vitePlugin;
696
+ return `import { defineConfig } from 'vite';
697
+ ${plugin?.import_ ?? ""}
698
+
699
+ export default defineConfig({
700
+ plugins: [${plugin?.call ?? ""}],
701
+ ${plugin ? ` // Worker bundles run their own rolldown pass \u2014 fresh plugin instances
702
+ // let the worker entry compile framework files too (worker-island apps).
703
+ worker: { plugins: () => [${plugin.call}] },
704
+ ` : ""}${HEADERS}
705
+ });
706
+ `;
707
+ }
708
+ var INDEX_HTML = (title, entry) => `<!doctype html>
709
+ <html lang="en">
710
+ <head>
711
+ <meta charset="UTF-8" />
712
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
713
+ <title>${title}</title>
714
+ </head>
715
+ <body>
716
+ <div id="root"></div>
717
+ <script type="module" src="/${entry}"></script>
718
+ </body>
719
+ </html>
720
+ `;
721
+ function scaffoldFiles(appName, fw) {
722
+ const spec = SCAFFOLDS[fw];
723
+ if (!spec) return [];
724
+ const files = [
725
+ { path: "package.json", content: pkgJson(appName, spec) },
726
+ { path: "tsconfig.json", content: JSON.stringify(TSCONFIGS[fw], null, 2) + "\n" },
727
+ { path: "vite.config.ts", content: viteConfig(fw) },
728
+ { path: ".npmrc", content: NPMRC },
729
+ { path: ".gitignore", content: GITIGNORE },
730
+ { path: "index.html", content: INDEX_HTML(appName, `src/main.${ISLAND_EXT[fw]}`) }
731
+ ];
732
+ const counter = islandFiles("counter", fw);
733
+ switch (fw) {
734
+ case "react":
735
+ files.push(
736
+ ...counter,
737
+ {
738
+ path: "src/main.tsx",
739
+ content: `import { createRoot } from 'react-dom/client';
740
+ import { App } from './App';
741
+
742
+ createRoot(document.getElementById('root')!).render(<App />);
743
+ `
744
+ },
745
+ {
746
+ path: "src/App.tsx",
747
+ content: `import { Island } from '@atolljs/react-island';
748
+
749
+ /**
750
+ * The counter island renders inside a worker \u2014 the main thread only replays
751
+ * its DOM ops. New islands: \`npx atoll add island <name>\`.
752
+ */
753
+ export function App() {
754
+ return (
755
+ <main style={{ fontFamily: 'system-ui', padding: '2rem' }}>
756
+ <h1>${appName}</h1>
757
+ <p>This counter renders in a worker thread.</p>
758
+ <Island
759
+ app="counter"
760
+ worker={() =>
761
+ new Worker(new URL('./islands/counter.worker.tsx', import.meta.url), { type: 'module' })
762
+ }
763
+ onEvent={(name, payload) => console.log('island event:', name, payload)}
764
+ />
765
+ </main>
766
+ );
767
+ }
768
+ `
769
+ }
770
+ );
771
+ break;
772
+ case "vue":
773
+ files.push(
774
+ ...counter,
775
+ { path: "src/env.d.ts", content: `declare module '*.vue';
776
+ ` },
777
+ {
778
+ path: "src/main.ts",
779
+ content: `import { createApp } from 'vue';
780
+ import App from './App.vue';
781
+
782
+ createApp(App).mount('#root');
783
+ `
784
+ },
785
+ {
786
+ path: "src/App.vue",
787
+ content: `<script setup lang="ts">
788
+ import { AtollIsland } from '@atolljs/vue-island';
789
+
790
+ // The counter island renders inside a worker \u2014 the main thread replays ops.
791
+ const worker = () =>
792
+ new Worker(new URL('./islands/counter.worker.ts', import.meta.url), { type: 'module' });
793
+ </script>
794
+
795
+ <template>
796
+ <main style="font-family: system-ui; padding: 2rem">
797
+ <h1>${appName}</h1>
798
+ <p>This counter renders in a worker thread.</p>
799
+ <AtollIsland app="counter" :worker="worker" />
800
+ </main>
801
+ </template>
802
+ `
803
+ }
804
+ );
805
+ break;
806
+ case "solid":
807
+ files.push(
808
+ ...counter,
809
+ {
810
+ path: "src/main.tsx",
811
+ content: `import { render } from 'solid-js/web';
812
+ import { App } from './App';
813
+
814
+ render(() => <App />, document.getElementById('root')!);
815
+ `
816
+ },
817
+ {
818
+ path: "src/App.tsx",
819
+ content: `import { Island } from '@atolljs/solid-island';
820
+
821
+ /** The counter island renders inside a worker \u2014 the main thread replays ops. */
822
+ export function App() {
823
+ return (
824
+ <main style={{ 'font-family': 'system-ui', padding: '2rem' }}>
825
+ <h1>${appName}</h1>
826
+ <p>This counter renders in a worker thread.</p>
827
+ <Island
828
+ app="counter"
829
+ worker={() =>
830
+ new Worker(new URL('./islands/counter.worker.tsx', import.meta.url), { type: 'module' })
831
+ }
832
+ />
833
+ </main>
834
+ );
835
+ }
836
+ `
837
+ }
838
+ );
839
+ break;
840
+ case "svelte":
841
+ files.push(
842
+ ...counter,
843
+ {
844
+ path: "src/main.ts",
845
+ content: `import { mount } from 'svelte';
846
+ import App from './App.svelte';
847
+
848
+ mount(App, { target: document.getElementById('root')! });
849
+ `
850
+ },
851
+ {
852
+ path: "src/App.svelte",
853
+ content: `<script lang="ts">
854
+ import { island } from '@atolljs/svelte-island';
855
+
856
+ // The counter island renders inside a worker \u2014 the main thread replays ops.
857
+ const worker = () =>
858
+ new Worker(new URL('./islands/counter.worker.ts', import.meta.url), { type: 'module' });
859
+ </script>
860
+
861
+ <main style="font-family: system-ui; padding: 2rem">
862
+ <h1>${appName}</h1>
863
+ <p>This counter renders in a worker thread.</p>
864
+ <div use:island={{ app: 'counter', worker }} />
865
+ </main>
866
+ `
867
+ }
868
+ );
869
+ break;
870
+ }
871
+ return files;
872
+ }
873
+ function nodeScaffoldFiles(appName) {
874
+ return [
875
+ {
876
+ path: "package.json",
877
+ content: JSON.stringify(
878
+ {
879
+ name: appName,
880
+ private: true,
881
+ type: "module",
882
+ scripts: {
883
+ dev: "tsx watch src/main.ts",
884
+ start: "tsx src/main.ts",
885
+ bundle: "esbuild src/atoll/tasks.worker.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/tasks.worker.js"
886
+ },
887
+ dependencies: { "@atolljs/core": "^0.1.0", "@atolljs/node": "^0.1.0", zod: "^4.6.5" },
888
+ devDependencies: { esbuild: "^0.28.2", tsx: "^4.23.15", typescript: "^7.0.2", "@types/node": "^26.6.3" }
889
+ },
890
+ null,
891
+ 2
892
+ ) + "\n"
893
+ },
894
+ { path: "tsconfig.json", content: JSON.stringify(TSCONFIGS.node, null, 2) + "\n" },
895
+ { path: ".npmrc", content: NPMRC },
896
+ { path: ".gitignore", content: GITIGNORE },
897
+ {
898
+ path: "src/atoll/tasks.memory.ts",
899
+ content: memoryContract("tasks")
900
+ },
901
+ {
902
+ path: "src/atoll/tasks.worker.ts",
903
+ content: workerEntry("tasks", {
904
+ host: "node",
905
+ memoryImport: "./tasks.memory",
906
+ memoryName: "tasksMemory",
907
+ demoMemoryWrite: true
908
+ })
909
+ },
910
+ {
911
+ path: "src/atoll/tasks.ts",
912
+ content: workerClientFile("tasks", {
913
+ host: "node",
914
+ memoryImport: "./tasks.memory",
915
+ memoryName: "tasksMemory",
916
+ distUrl: "../../dist/tasks.worker.js"
917
+ })
918
+ },
919
+ {
920
+ path: "src/main.ts",
921
+ content: `import { tasks, tasksPool } from './atoll/tasks';
922
+ import { tasksMemory } from './atoll/tasks.memory';
923
+
924
+ /**
925
+ * ${appName} \u2014 a worker pool behind a typed client. The echo task runs off
926
+ * the main thread; shared memory is readable here with zero dispatch.
927
+ */
928
+ const heard = await tasks.echo('hello from main');
929
+ console.log(heard);
930
+ console.log('summary (shared memory):', tasksMemory.state.summary.read());
931
+
932
+ await tasksPool.close();
933
+ `
934
+ }
935
+ ];
936
+ }
937
+ function workerBindingsFile(name, fw, dir, mem) {
938
+ const c = camel(name);
939
+ const P = pascal(name);
940
+ const memImport = mem ? `import { ${mem.export_} } from '${mem.import_}';
941
+ ` : "";
942
+ const watchLine = (hook, decl = "const") => mem ? `
943
+ // ${decl} progress = ${hook}(${mem.export_}, 'signals.progress');` : "";
944
+ switch (fw) {
945
+ case "react":
946
+ return {
947
+ path: `${dir}/use${P}.ts`,
948
+ content: `import { useTask${mem ? ", useSharedValue" : ""} } from '@atolljs/react';
949
+ import { ${c} } from './${name}';
950
+ ${memImport}
951
+ /**
952
+ * React bindings for the ${name} pool \u2014 call inside components. useTask
953
+ * wraps a call in { data, pending, settled, elapsedMs, error } with
954
+ * latest-wins run()/runOnce(); useSharedValue parks on Atomics.
955
+ */
956
+ export function use${P}() {
957
+ const echo = useTask(${c}.echo);${watchLine("useSharedValue")}
958
+ return { echo };
959
+ }
960
+ `
961
+ };
962
+ case "nextjs":
963
+ return {
964
+ path: `${dir}/${name}.tsx`,
965
+ content: `'use client';
966
+ import { useTask${mem ? ", useSharedValue" : ""} } from '@atolljs/nextjs';
967
+ import { ${c} } from './${name}';
968
+ ${memImport}
969
+ /**
970
+ * Client component over the ${name} pool \u2014 'use client' keeps the hooks
971
+ * browser-side; SSR-safe because the pool spawns lazily on first call.
972
+ */
973
+ export function ${P}() {
974
+ const echo = useTask(${c}.echo);${watchLine("useSharedValue")}
975
+ return (
976
+ <button onClick={() => echo.run('ping')}>
977
+ {echo.data ?? (echo.pending ? 'working\\u2026' : '${name}')}
978
+ </button>
979
+ );
980
+ }
981
+ `
982
+ };
983
+ case "vue":
984
+ return {
985
+ path: `${dir}/use${P}.ts`,
986
+ content: `import { useTask${mem ? ", useSharedValue" : ""} } from '@atolljs/vue';
987
+ import { ${c} } from './${name}';
988
+ ${memImport}
989
+ /** Vue composable over the ${name} pool \u2014 refs, ready for <script setup>. */
990
+ export function use${P}() {
991
+ const echo = useTask(${c}.echo);${watchLine("useSharedValue")}
992
+ return { echo };
993
+ }
994
+ `
995
+ };
996
+ case "solid":
997
+ return {
998
+ path: `${dir}/use${P}.ts`,
999
+ content: `import { createTask${mem ? ", createSharedValue" : ""} } from '@atolljs/solidjs';
1000
+ import { ${c} } from './${name}';
1001
+ ${memImport}
1002
+ /** Solid bindings over the ${name} pool \u2014 accessors, call inside setup. */
1003
+ export function create${P}() {
1004
+ const echo = createTask(${c}.echo);${watchLine("createSharedValue")}
1005
+ return { echo };
1006
+ }
1007
+ `
1008
+ };
1009
+ case "svelte":
1010
+ return {
1011
+ path: `${dir}/${name}.svelte.ts`,
1012
+ content: `import { taskState${mem ? ", sharedValue" : ""} } from '@atolljs/svelte';
1013
+ import { ${c} } from './${name}';
1014
+ ${memImport}
1015
+ /** Svelte bindings over the ${name} pool \u2014 call inside components. */
1016
+ export function ${c}State() {
1017
+ const echo = taskState(${c}.echo);${watchLine("sharedValue")}
1018
+ return { echo };
1019
+ }
1020
+ `
1021
+ };
1022
+ case "angular":
1023
+ return {
1024
+ path: `${dir}/${name}.facade.ts`,
1025
+ content: `import { Injectable } from '@angular/core';
1026
+ import { taskState${mem ? ", sharedValue" : ""} } from '@atolljs/angular';
1027
+ import { ${c} } from './${name}';
1028
+ ${memImport}
1029
+ /**
1030
+ * Angular facade over the ${name} pool \u2014 field initializers run in the
1031
+ * injection context, so the signal subscription unbinds on destroy.
1032
+ */
1033
+ @Injectable({ providedIn: 'root' })
1034
+ export class ${P}Facade {
1035
+ readonly echo = taskState(${c}.echo);${watchLine("sharedValue", "readonly")}
1036
+ readonly run = (message: string) => this.echo.run(message);
1037
+ }
1038
+ `
1039
+ };
1040
+ default:
1041
+ return null;
1042
+ }
1043
+ }
1044
+ function islandFacadeFiles(name, fw, dir = "src/islands") {
1045
+ const P = pascal(name);
1046
+ const ext = ISLAND_EXT[fw] ?? "ts";
1047
+ const contract = (appLine) => `/**
1048
+ * ${name} island contract \u2014 shell-safe: the app reference + the worker
1049
+ * factory. Never import ./${name}.worker.${ext} here \u2014 definePolyWorker
1050
+ * installs worker-side globals on evaluation.
1051
+ */
1052
+ ${appLine}
1053
+ export const worker = () =>
1054
+ new Worker(new URL('./${name}.worker.${ext}', import.meta.url), { type: 'module' });
1055
+ `;
1056
+ switch (fw) {
1057
+ case "react":
1058
+ case "solid": {
1059
+ const isReact = fw === "react";
1060
+ const sig = isReact ? "useState" : "createSignal";
1061
+ const poly = isReact ? "defineReactPolyWorker" : "defineSolidPolyWorker";
1062
+ const pkg = `@atolljs/${fw}-island/worker`;
1063
+ return [
1064
+ {
1065
+ path: `${dir}/${name}.app.tsx`,
1066
+ content: `import { ${sig} } from '${isReact ? "react" : "solid-js"}';
1067
+ import { islandApp } from '@atolljs/islands/worker';
1068
+ import { emit } from '${pkg}';
1069
+
1070
+ /**
1071
+ * ${P}App \u2014 a real ${isReact ? "React" : "Solid"} component rendered INSIDE
1072
+ * the worker. islandApp stamps the registry key (minification-proof); state
1073
+ * never leaves the worker, DOM ops stream to the shell driver.
1074
+ */
1075
+ export const ${P}App = islandApp('${name}', function ${P}(${isReact ? `{ label = '${name}' }: { label?: string }` : `props: { label?: string }`}) {
1076
+ ${isReact ? "const [count, setCount] = useState(0);" : "const [count, setCount] = createSignal(0);"}
1077
+ return (
1078
+ <button
1079
+ onClick={() => {
1080
+ const n = ${isReact ? "count" : "count()"} + 1;
1081
+ setCount(n);
1082
+ emit('incremented', { count: n });
1083
+ }}
1084
+ >
1085
+ ${isReact ? `{label}: {count}` : `{props.label ?? '${name}'}: {count()}`}
1086
+ </button>
1087
+ );
1088
+ });
1089
+ `
1090
+ },
1091
+ {
1092
+ path: `${dir}/${name}.worker.${ext}`,
1093
+ content: `import { ${poly} } from '${pkg}';
1094
+ import { ${P}App } from './${name}.app';
1095
+
1096
+ /** One worker, many islands \u2014 add apps to the registry. */
1097
+ export const worker = ${poly}({ apps: { ${name}: ${P}App } });
1098
+ `
1099
+ },
1100
+ {
1101
+ path: `${dir}/${name}.island.ts`,
1102
+ content: contract(`export { ${P}App as app } from './${name}.app';`)
1103
+ }
1104
+ ];
1105
+ }
1106
+ case "vue": {
1107
+ const [component] = islandFiles(name, "vue", dir);
1108
+ return [
1109
+ component,
1110
+ {
1111
+ path: `${dir}/${name}.worker.ts`,
1112
+ content: `import { defineVuePolyWorker } from '@atolljs/vue-island/worker';
1113
+ import ${P} from './${P}.vue';
1114
+
1115
+ /** One worker, many islands \u2014 add apps to the registry. */
1116
+ export const worker = defineVuePolyWorker({ apps: { ${name}: ${P} } });
1117
+ `
1118
+ },
1119
+ {
1120
+ path: `${dir}/${name}.island.ts`,
1121
+ content: contract(`export const app = '${name}';`)
1122
+ }
1123
+ ];
1124
+ }
1125
+ case "svelte": {
1126
+ const [component] = islandFiles(name, "svelte", dir);
1127
+ return [
1128
+ component,
1129
+ {
1130
+ path: `${dir}/${name}.worker.ts`,
1131
+ content: `import { defineSveltePolyWorker } from '@atolljs/svelte-island/worker';
1132
+ import ${P} from './${P}.svelte';
1133
+
1134
+ /** One worker, many islands \u2014 add apps to the registry. */
1135
+ export const worker = defineSveltePolyWorker({ apps: { ${name}: ${P} } });
1136
+ `
1137
+ },
1138
+ {
1139
+ path: `${dir}/${name}.island.ts`,
1140
+ content: contract(`export const app = '${name}';`)
1141
+ },
1142
+ {
1143
+ path: `${dir}/${P}Island.svelte`,
1144
+ content: `<script lang="ts">
1145
+ import { island } from '@atolljs/svelte-island';
1146
+ import { app, worker } from './${name}.island';
1147
+
1148
+ // The facade component \u2014 attrs serialize to worker props on mount.
1149
+ let { label = '${name}' }: { label?: string } = $props();
1150
+ </script>
1151
+
1152
+ <div use:island={{ app, worker, props: { label } }} />
1153
+ `
1154
+ }
1155
+ ];
1156
+ }
1157
+ case "angular":
1158
+ return [
1159
+ {
1160
+ path: `${dir}/${name}.component.ts`,
1161
+ content: `import { Component, input, output } from '@angular/core';
1162
+ import { AngularIsland } from '@atolljs/angular-island/worker';
1163
+
1164
+ /**
1165
+ * ${P}Component \u2014 rendered inside the worker by Angular's own engine.
1166
+ * @AngularIsland stamps the registry key and registers the component, so
1167
+ * defineAngularPolyWorker() collects it with no hand-kept apps map.
1168
+ */
1169
+ @AngularIsland('${name}')
1170
+ @Component({
1171
+ standalone: true,
1172
+ template: \`
1173
+ <button (click)="increment()">{{ label() }}: {{ count }}</button>
1174
+ \`,
1175
+ })
1176
+ export class ${P}Component {
1177
+ label = input('${name}');
1178
+ /** Root output() fields bridge to the shell's island event channel. */
1179
+ incremented = output<{ count: number }>();
1180
+ count = 0;
1181
+ increment() {
1182
+ this.count += 1;
1183
+ this.incremented.emit({ count: this.count });
1184
+ }
1185
+ }
1186
+ `
1187
+ },
1188
+ {
1189
+ path: `${dir}/${name}.worker.ts`,
1190
+ content: `import { defineAngularPolyWorker } from '@atolljs/angular-island/worker';
1191
+ import './${name}.component'; // registers via @AngularIsland
1192
+
1193
+ /** One worker, many islands \u2014 decorated components self-register. */
1194
+ export const worker = defineAngularPolyWorker();
1195
+ `
1196
+ },
1197
+ {
1198
+ path: `${dir}/${name}.island.ts`,
1199
+ content: `import { islandComponent } from '@atolljs/angular-island';
1200
+ import type { ${P}Component } from './${name}.component';
1201
+
1202
+ /**
1203
+ * ${name} island facade \u2014 a standalone component typed off the worker
1204
+ * component's input()/output() contract. Use <${name}-island> in templates.
1205
+ */
1206
+ export const ${P}Island = islandComponent<${P}Component>({
1207
+ app: '${name}',
1208
+ selector: '${name}-island',
1209
+ worker: () =>
1210
+ new Worker(new URL('./${name}.worker.ts', import.meta.url), { type: 'module' }),
1211
+ });
1212
+ `
1213
+ }
1214
+ ];
1215
+ default:
1216
+ return [];
1217
+ }
1218
+ }
1219
+ function islandFacadeUsage(name, fw) {
1220
+ const P = pascal(name);
1221
+ switch (fw) {
1222
+ case "react":
1223
+ return `import { Suspense } from 'react';
1224
+ import { lazyIsland } from '@atolljs/react-island';
1225
+
1226
+ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1227
+
1228
+ <Suspense fallback={null}>
1229
+ <${P}Island label="${name}" onEvent={(n, p) => console.log(n, p)} />
1230
+ </Suspense>`;
1231
+ case "vue":
1232
+ return `<script setup>
1233
+ import { lazyIsland } from '@atolljs/vue-island';
1234
+ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1235
+ </script>
1236
+
1237
+ <template>
1238
+ <${P}Island label="${name}" />
1239
+ </template>`;
1240
+ case "solid":
1241
+ return `import { lazyIsland } from '@atolljs/solid-island';
1242
+
1243
+ const ${P}Island = lazyIsland(() => import('./islands/${name}.island'));
1244
+
1245
+ <${P}Island label="${name}" />`;
1246
+ case "svelte":
1247
+ return `<script lang="ts">
1248
+ import ${P}Island from './islands/${P}Island.svelte';
1249
+ </script>
1250
+
1251
+ <${P}Island label="${name}" />`;
1252
+ case "angular":
1253
+ return `import { ${P}Island } from './islands/${name}.island';
1254
+ // add ${P}Island to the host component's imports, then:
1255
+ // <${name}-island [props]="{ label: '${name}' }" (incremented)="onIncremented($event)" />`;
1256
+ default:
1257
+ return "";
1258
+ }
1259
+ }
1260
+ function nestjsModuleFiles(name, dir, opts) {
1261
+ const P = pascal(name);
1262
+ const memImport = opts.mem ? `import { ${opts.mem.export_} } from '${opts.mem.import_}';
1263
+ ` : "";
1264
+ const serviceImport = opts.service ? `import { ${opts.service} } from './${name}.service';
1265
+ ` : "";
1266
+ const memLine = opts.mem ? ` sharedMemory: ${opts.mem.export_},
1267
+ ` : ` // message-only \u2014 add sharedMemory: <contract> to give the pool a buffer
1268
+ `;
1269
+ const providers = opts.service ? ` providers: [${opts.service}],
1270
+ exports: [${opts.service}, AtollModule],
1271
+ ` : "";
1272
+ return [
1273
+ {
1274
+ path: `${dir}/${name}.module.ts`,
1275
+ content: `import { Module } from '@nestjs/common';
1276
+ import { Worker } from 'node:worker_threads';
1277
+ import { AtollModule } from '@atolljs/nestjs';
1278
+ ${serviceImport}${memImport}
1279
+ /**
1280
+ * ${name} pool boundary \u2014 the feature module owns its worker domain.
1281
+ * Imported by AppModule for the API thread (registerPool spawns workers)
1282
+ * and by the worker entry (runAtollWorker boots the same module in-thread).
1283
+ */
1284
+ @Module({
1285
+ imports: [
1286
+ AtollModule.registerPool({
1287
+ name: '${name}',
1288
+ // webpack emits the worker entry as its own chunk \u2014 keep the URL inline
1289
+ worker: () => new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1290
+ ${memLine} poolSize: 'auto',
1291
+ }),
1292
+ ],
1293
+ ${providers}})
1294
+ export class ${P}Module {}
1295
+ `
1296
+ },
1297
+ {
1298
+ path: `${dir}/${name}.worker.ts`,
1299
+ content: `// Worker entry \u2014 boots a Nest application context inside the worker and
1300
+ // binds every @AtollService/@AtollTask provider in ${P}Module.
1301
+ import { runAtollWorker } from '@atolljs/nestjs/worker';
1302
+ import { ${P}Module } from './${name}.module';
1303
+
1304
+ void runAtollWorker(${P}Module);
1305
+ `
1306
+ }
1307
+ ];
1308
+ }
1309
+ function nestjsServiceFiles(name, dir, mem) {
1310
+ const P = pascal(name);
1311
+ const memImport = mem ? `import { ${mem.export_} } from '${mem.import_}';
1312
+ ` : "";
1313
+ const memWrite = mem ? `
1314
+ // shared memory crosses without a message \u2014 pick a real field:
1315
+ // ${mem.export_}.signals.progress.write(100);` : "";
1316
+ return [
1317
+ {
1318
+ path: `${dir}/${name}.service.ts`,
1319
+ content: `import { Injectable } from '@nestjs/common';
1320
+ import { threadId } from 'node:worker_threads';
1321
+ import { AtollService } from '@atolljs/nestjs/decorators';
1322
+ ${memImport}
1323
+ /**
1324
+ * Service-level facade \u2014 the class IS the interop surface. Consumers inject
1325
+ * it like any provider; every call dispatches to the '${name}' pool as a
1326
+ * ${P}Service.method task and the body runs on the worker's DI'd instance.
1327
+ */
1328
+ @Injectable()
1329
+ @AtollService({ pool: '${name}' })
1330
+ export class ${P}Service {
1331
+ /** Runs inside the worker \u2014 the return value crosses postMessage. */
1332
+ async echo(input: string) {${memWrite}
1333
+ return { heard: input, worker: threadId };
1334
+ }
1335
+ }
1336
+ `
1337
+ },
1338
+ ...nestjsModuleFiles(name, dir, { service: `${P}Service`, mem })
1339
+ ];
1340
+ }
1341
+ function nestjsMethodFiles(name, dir) {
1342
+ const P = pascal(name);
1343
+ return [
1344
+ {
1345
+ path: `${dir}/${name}.service.ts`,
1346
+ content: `import { Injectable } from '@nestjs/common';
1347
+ import { threadId } from 'node:worker_threads';
1348
+ import { AtollTask } from '@atolljs/nestjs/decorators';
1349
+
1350
+ /**
1351
+ * Method-level offload \u2014 only decorated methods dispatch to the pool; the
1352
+ * rest of the class stays on the API thread. The body executes inside the
1353
+ * worker's own Nest context on the DI-resolved instance.
1354
+ */
1355
+ @Injectable()
1356
+ export class ${P}Service {
1357
+ @AtollTask({ pool: '${name}' })
1358
+ async run(input: string) {
1359
+ return { heard: input, worker: threadId };
1360
+ }
1361
+
1362
+ /** Not decorated \u2014 plain main-thread method. */
1363
+ describe() {
1364
+ return '${name} service, partial offload';
1365
+ }
1366
+ }
1367
+ `
1368
+ }
1369
+ ];
1370
+ }
1371
+ function nestjsHousedFiles(name, dir) {
1372
+ const P = pascal(name);
1373
+ return [
1374
+ {
1375
+ path: `${dir}/${name}-atoll.module.ts`,
1376
+ content: `// Main-side boundary \u2014 registers the '${name}' pool the housed API
1377
+ // runs in. Message-only by default; to share another pool's buffer, wrap
1378
+ // the factory:
1379
+ // worker: withSharedBuffer(
1380
+ // () => new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1381
+ // () => getAtollPool('<other-pool>')?.sharedBuffer,
1382
+ // )
1383
+ import { Module } from '@nestjs/common';
1384
+ import { Worker } from 'node:worker_threads';
1385
+ import { AtollModule } from '@atolljs/nestjs';
1386
+
1387
+ @Module({
1388
+ imports: [
1389
+ AtollModule.registerPool({
1390
+ name: '${name}',
1391
+ worker: () => new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1392
+ poolSize: 2,
1393
+ }),
1394
+ ],
1395
+ })
1396
+ export class ${P}AtollModule {}
1397
+ `
1398
+ },
1399
+ {
1400
+ path: `${dir}/${name}.worker.ts`,
1401
+ content: `// Dedicated HTTP worker \u2014 NOT a task worker: it serves requests, so no
1402
+ // runAtollWorker. serveHttp binds an internal 127.0.0.1 port and announces
1403
+ // it via the HTTP_PORT handshake; a respawned worker re-announces.
1404
+ import 'reflect-metadata';
1405
+ import { NestFactory } from '@nestjs/core';
1406
+ import { serveHttp } from '@atolljs/node/http';
1407
+ import { ${P}ApiModule } from './${name}-api.module';
1408
+
1409
+ void (async () => {
1410
+ const app = await NestFactory.create(${P}ApiModule, { logger: ['warn', 'error'] });
1411
+ await app.init();
1412
+ serveHttp(app.getHttpServer(), { listen: 0 });
1413
+ })();
1414
+ `
1415
+ },
1416
+ {
1417
+ path: `${dir}/${name}-api.module.ts`,
1418
+ content: `// Worker-side module \u2014 pure HTTP surface (controllers/providers only).
1419
+ // No registerPool import: pools only exist on the main thread.
1420
+ import { Module } from '@nestjs/common';
1421
+ import { ${P}Controller } from './${name}.controller';
1422
+
1423
+ @Module({ controllers: [${P}Controller] })
1424
+ export class ${P}ApiModule {}
1425
+ `
1426
+ },
1427
+ {
1428
+ path: `${dir}/${name}.controller.ts`,
1429
+ content: `import { Controller, Get } from '@nestjs/common';
1430
+ import { threadId } from 'node:worker_threads';
1431
+
1432
+ /**
1433
+ * Housed controller \u2014 exists ONLY inside '${name}' pool workers. The main
1434
+ * app never sees these routes: it proxies /api/${name}/* into the workers'
1435
+ * internal listeners, so decorators, DI, and guards execute off-thread.
1436
+ */
1437
+ @Controller('api/${name}')
1438
+ export class ${P}Controller {
1439
+ /** Which pool worker owns this request. */
1440
+ @Get('whoami')
1441
+ whoami() {
1442
+ return { worker: threadId };
1443
+ }
1444
+ }
1445
+ `
1446
+ }
1447
+ ];
1448
+ }
1449
+ function nestjsHousedWiring(name) {
1450
+ return `main.ts \u2014 proxy the prefix into the pool:
1451
+
1452
+ import { getAtollPool } from '@atolljs/nestjs';
1453
+ import { proxyToWorker, workerHttpPorts } from '@atolljs/node/http';
1454
+
1455
+ const pool = getAtollPool('${name}');
1456
+ if (pool) {
1457
+ const tracker = workerHttpPorts(pool);
1458
+ app.use('/api/${name}', proxyToWorker({
1459
+ pool,
1460
+ tracker,
1461
+ to: '/api/${name}', // express strips the mount \u2014 restore it in-worker
1462
+ }));
1463
+ }`;
1464
+ }
1465
+ function nextjsRouteFiles(name, dir, variant) {
1466
+ const c = camel(name);
1467
+ const P = pascal(name);
1468
+ const workerFactory = `createNodeWorker(
1469
+ new Worker(new URL('./${name}.worker.ts', import.meta.url)),
1470
+ )`;
1471
+ if (variant === "task") {
1472
+ return [
1473
+ {
1474
+ path: `${dir}/${name}.contract.ts`,
1475
+ content: `import { z } from 'zod';
1476
+ import { defineSharedMemory, field, type TaskContract } from '@atolljs/core';
1477
+
1478
+ /** Pool-owned shared state \u2014 the route handler reads it with zero dispatch. */
1479
+ export const ${c}Memory = defineSharedMemory({
1480
+ jobsDone: field.number(),
1481
+ });
1482
+
1483
+ const result = z.object({
1484
+ echo: z.string(),
1485
+ ms: z.number(),
1486
+ jobsDone: z.number(),
1487
+ });
1488
+
1489
+ /** Executed inside a worker via TaskRegistry. */
1490
+ export const ${P}Job: TaskContract<[input?: string], z.infer<typeof result>> = {
1491
+ taskId: '${name}.run',
1492
+ resultSchema: result,
1493
+ };
1494
+ `
1495
+ },
1496
+ {
1497
+ path: `${dir}/${name}.worker.ts`,
1498
+ content: `// Worker entry \u2014 the shim MUST be first: it binds self = parentPort
1499
+ // before workerBootstrap wires INIT_MEMORY / EXECUTE_TASK.
1500
+ import '@atolljs/node/shim';
1501
+ import '@atolljs/core/worker/workerBootstrap';
1502
+ import { TaskRegistry } from '@atolljs/core';
1503
+ import { ${c}Memory, ${P}Job } from './${name}.contract';
1504
+
1505
+ TaskRegistry.register(${P}Job, (input = '${name}') => {
1506
+ const t0 = performance.now();
1507
+ const jobs = ${c}Memory.jobsDone.read() + 1;
1508
+ ${c}Memory.jobsDone.write(jobs);
1509
+ return { echo: \`worker heard: \${input}\`, ms: performance.now() - t0, jobsDone: jobs };
1510
+ });
1511
+ `
1512
+ },
1513
+ {
1514
+ path: `${dir}/pool.ts`,
1515
+ content: `import { Worker } from 'node:worker_threads';
1516
+ import { createNodePool, createNodeWorker } from '@atolljs/node';
1517
+ import { ${c}Memory, ${P}Job } from './${name}.contract';
1518
+
1519
+ const create = () => {
1520
+ const pool = createNodePool({
1521
+ sharedMemory: ${c}Memory,
1522
+ poolSize: 'auto',
1523
+ tasks: { run: ${P}Job },
1524
+ createWorker: () =>
1525
+ ${workerFactory},
1526
+ });
1527
+ // Read memory via this getter \u2014 Next compiles module graphs per route
1528
+ // layer, so a fresh contract import can be a second, unbound instance.
1529
+ return { pool, memory: ${c}Memory };
1530
+ };
1531
+
1532
+ // Module scope = process scope; globalThis survives dev-mode HMR reloads.
1533
+ export const get${P} = (): ReturnType<typeof create> => {
1534
+ const g = globalThis as { __atoll${P}?: ReturnType<typeof create> };
1535
+ return (g.__atoll${P} ??= create());
1536
+ };
1537
+ `
1538
+ },
1539
+ {
1540
+ path: `${dir}/route.ts`,
1541
+ content: `import { NextResponse } from 'next/server';
1542
+ import { get${P} } from './pool';
1543
+
1544
+ // worker_threads need the Node.js runtime \u2014 Edge cannot spawn them.
1545
+ export const runtime = 'nodejs';
1546
+ export const dynamic = 'force-dynamic';
1547
+
1548
+ /** POST /api/${name} {"input": "..."} \u2014 dispatch the task into the pool. */
1549
+ export async function POST(req: Request) {
1550
+ const body = await req.json().catch(() => ({}));
1551
+ return NextResponse.json(await get${P}().pool.run(body?.input));
1552
+ }
1553
+
1554
+ /** GET /api/${name} \u2014 read shared state on this thread, no dispatch. */
1555
+ export async function GET() {
1556
+ return NextResponse.json({ jobsDone: get${P}().memory.jobsDone.read() });
1557
+ }
1558
+ `
1559
+ }
1560
+ ];
1561
+ }
1562
+ return [
1563
+ {
1564
+ path: `${dir}/${name}.contract.ts`,
1565
+ content: `import { defineSharedMemory, field } from '@atolljs/core';
1566
+
1567
+ /** Pool-owned shared state \u2014 the route handler reads it with zero dispatch. */
1568
+ export const ${c}Memory = defineSharedMemory({
1569
+ jobsDone: field.number(),
1570
+ });
1571
+ `
1572
+ },
1573
+ {
1574
+ path: `${dir}/${name}.worker.ts`,
1575
+ content: `// Worker entry \u2014 shim first, then defineWorker installs the message loop.
1576
+ import '@atolljs/node/shim';
1577
+ import { defineWorker, serviceMethod } from '@atolljs/core';
1578
+ import { z } from 'zod';
1579
+ import { ${c}Memory } from './${name}.contract';
1580
+
1581
+ const echo = serviceMethod({
1582
+ def: { argsSchema: z.tuple([z.string()]), resultSchema: z.string() },
1583
+ run: (message) => {
1584
+ ${c}Memory.jobsDone.write(${c}Memory.jobsDone.read() + 1);
1585
+ return \`worker heard: \${message}\`;
1586
+ },
1587
+ });
1588
+
1589
+ export const ${c}Worker = defineWorker({
1590
+ sharedMemory: ${c}Memory,
1591
+ methods: { echo },
1592
+ });
1593
+ export type ${P}Worker = typeof ${c}Worker;
1594
+ `
1595
+ },
1596
+ {
1597
+ path: `${dir}/pool.ts`,
1598
+ content: `import { Worker } from 'node:worker_threads';
1599
+ import { workerClient } from '@atolljs/core';
1600
+ import { createNodePool, createNodeWorker } from '@atolljs/node';
1601
+ import { ${c}Memory } from './${name}.contract';
1602
+ // Type-only \u2014 worker code must not leak into the server bundle.
1603
+ import type { ${P}Worker } from './${name}.worker';
1604
+
1605
+ const create = () => {
1606
+ const pool = createNodePool({
1607
+ sharedMemory: ${c}Memory,
1608
+ poolSize: 'auto',
1609
+ createWorker: () =>
1610
+ ${workerFactory},
1611
+ });
1612
+ // Read memory via this getter \u2014 Next compiles module graphs per route
1613
+ // layer, so a fresh contract import can be a second, unbound instance.
1614
+ return { pool, client: workerClient<${P}Worker>(pool), memory: ${c}Memory };
1615
+ };
1616
+
1617
+ // Module scope = process scope; globalThis survives dev-mode HMR reloads.
1618
+ export const get${P} = (): ReturnType<typeof create> => {
1619
+ const g = globalThis as { __atoll${P}?: ReturnType<typeof create> };
1620
+ return (g.__atoll${P} ??= create());
1621
+ };
1622
+ `
1623
+ },
1624
+ {
1625
+ path: `${dir}/route.ts`,
1626
+ content: `import { NextResponse } from 'next/server';
1627
+ import { get${P} } from './pool';
1628
+
1629
+ // worker_threads need the Node.js runtime \u2014 Edge cannot spawn them.
1630
+ export const runtime = 'nodejs';
1631
+ export const dynamic = 'force-dynamic';
1632
+
1633
+ /** POST /api/${name} {"input": "..."} \u2014 a typed client call into the pool. */
1634
+ export async function POST(req: Request) {
1635
+ const body = await req.json().catch(() => ({}));
1636
+ return NextResponse.json({
1637
+ echo: await get${P}().client.echo(String(body?.input ?? '${name}')),
1638
+ });
1639
+ }
1640
+
1641
+ /** GET /api/${name} \u2014 read shared state on this thread, no dispatch. */
1642
+ export async function GET() {
1643
+ return NextResponse.json({ jobsDone: get${P}().memory.jobsDone.read() });
1644
+ }
1645
+ `
1646
+ }
1647
+ ];
1648
+ }
1649
+ function nextjsComponentFiles(name, dir, mem) {
1650
+ const c = camel(name);
1651
+ const P = pascal(name);
1652
+ return [
1653
+ {
1654
+ path: `${dir}/${name}.tsx`,
1655
+ content: `'use client';
1656
+ import { useTask${mem ? ", useSharedValue" : ""} } from '@atolljs/nextjs';
1657
+ import { ${c} } from './${name}';
1658
+ ${mem ? `import { ${mem.export_} } from '${mem.import_}';
1659
+ ` : ""}
1660
+ /**
1661
+ * ${P} \u2014 a client component over the ${name} pool. Field reads return
1662
+ * undefined until the contract binds on the client (SSR-safe).
1663
+ */
1664
+ export function ${P}() {
1665
+ const echo = useTask(${c}.echo);${mem ? `
1666
+ const progress = useSharedValue(${mem.export_}, 'signals.progress');` : ""}
1667
+ return (
1668
+ <button onClick={() => echo.run('ping')}>
1669
+ {echo.data ?? (echo.pending ? 'working\\u2026' : '${name}')}
1670
+ </button>
1671
+ );
1672
+ }
1673
+ `
1674
+ }
1675
+ ];
1676
+ }
1677
+ function nextjsInstrumentationFile(name, dir = "src") {
1678
+ const P = pascal(name);
1679
+ return {
1680
+ path: `${dir}/instrumentation.ts`,
1681
+ content: `/**
1682
+ * Next.js server bootstrap \u2014 runs once when the Node.js runtime boots.
1683
+ * Dynamic imports keep node:worker_threads out of the edge/browser bundles.
1684
+ */
1685
+ export async function register() {
1686
+ if (process.env.NEXT_RUNTIME !== 'nodejs') return;
1687
+ const { get${P} } = await import('./app/api/${name}/pool');
1688
+ get${P}(); // spawn workers + bind the buffer during startup, not per request
1689
+ }
1690
+ `
1691
+ };
1692
+ }
1693
+
1694
+ // src/commands.ts
1695
+ var NODE_HOSTS = ["node", "nestjs"];
1696
+ var hostOf = (fw) => NODE_HOSTS.includes(fw) ? "node" : "browser";
1697
+ async function offerInstall(ctx, project, pkgs, installAll = false) {
1698
+ if (hasFlag(ctx.args, "no-install")) {
1699
+ ctx.io.warn("--no-install \u2014 skipping dependency install");
1700
+ return;
1701
+ }
1702
+ const missing = missingDeps(project.pkg, pkgs);
1703
+ if (missing.length === 0 && !installAll) return;
1704
+ const cmd = pmInstallCmd(project.pm, installAll ? [] : missing);
1705
+ const yes = await ctx.io.confirm(`install dependencies? (${ctx.io.fmt.accent(cmd)})`, true);
1706
+ if (!yes) {
1707
+ ctx.io.warn(`skipped \u2014 run ${ctx.io.fmt.accent(cmd)} when ready`);
1708
+ return;
1709
+ }
1710
+ ctx.io.print(
1711
+ ctx.io.fmt.dim(
1712
+ "note: bootstrap install runs with --min-release-age=0 \u2014 the new .npmrc policy applies from the next install on"
1713
+ )
1714
+ );
1715
+ const [bin, ...rest] = cmd.split(" ").filter(Boolean);
1716
+ const r = spawnSync(bin, [...rest, "--min-release-age=0"], {
1717
+ cwd: project.root ?? ctx.cwd,
1718
+ stdio: "inherit"
1719
+ });
1720
+ if (r.status !== 0) throw new Error(`install failed \u2014 run \`${cmd}\` manually`);
1721
+ }
1722
+ async function runInit(ctx) {
1723
+ const project = detectProject(ctx.cwd);
1724
+ if (!project.root || !project.pkg) {
1725
+ ctx.io.error("no package.json found \u2014 run `atoll new <dir>` to scaffold, or cd into a project");
1726
+ return 1;
1727
+ }
1728
+ const rel = (abs) => relative(ctx.cwd, abs).replaceAll("\\", "/");
1729
+ ctx.io.print(
1730
+ ctx.io.fmt.dim(
1731
+ `detected: ${project.framework} \xB7 ${project.pm} \xB7 root ${project.root}`
1732
+ )
1733
+ );
1734
+ await writeTree(ctx.io, ctx.cwd, [{ path: rel(join3(project.root, ".npmrc")), content: NPMRC }], {
1735
+ force: hasFlag(ctx.args, "force")
1736
+ });
1737
+ await offerInstall(ctx, project, ATOLL_DEPS[project.framework]);
1738
+ const atollDir = join3(project.srcDir, "atoll");
1739
+ const host = hostOf(project.framework);
1740
+ const name = flag(ctx.args, "name") ?? "app";
1741
+ const files = [
1742
+ { path: `${rel(atollDir)}/${name}.memory.ts`, content: memoryContract(name) },
1743
+ {
1744
+ path: `${rel(atollDir)}/${name}.worker.ts`,
1745
+ content: workerEntry(name, {
1746
+ host,
1747
+ memoryImport: `./${name}.memory`,
1748
+ memoryName: `${camel(name)}Memory`,
1749
+ demoMemoryWrite: true
1750
+ })
1751
+ },
1752
+ {
1753
+ path: `${rel(atollDir)}/${name}.ts`,
1754
+ content: workerClientFile(name, {
1755
+ host,
1756
+ memoryImport: `./${name}.memory`,
1757
+ memoryName: `${camel(name)}Memory`,
1758
+ // src/atoll/x.ts → <root>/dist/x.worker.js
1759
+ distUrl: `${relative(atollDir, join3(project.root, "dist")).replaceAll("\\", "/")}/${name}.worker.js`
1760
+ })
1761
+ }
1762
+ ];
1763
+ await writeTree(ctx.io, ctx.cwd, files, { force: hasFlag(ctx.args, "force") });
1764
+ ctx.io.print("");
1765
+ ctx.io.print(ctx.io.fmt.strong("next steps:"));
1766
+ if (host === "node") {
1767
+ ctx.io.print(
1768
+ ` bundle the worker entry (see the comment in ${name}.ts), then call ${ctx.io.fmt.accent(`${camel(name)}.echo('hi')`)}`
1769
+ );
1770
+ } else {
1771
+ ctx.io.print(
1772
+ ` call ${ctx.io.fmt.accent(`${camel(name)}.echo('hi')`)} \u2014 the pool spawns lazily on first call`
1773
+ );
1774
+ }
1775
+ ctx.io.print(
1776
+ ` ${ctx.io.fmt.accent("atoll add worker|memory|island <name>")} grows it \xB7 ${ctx.io.fmt.accent("atoll doctor")} checks the setup`
1777
+ );
1778
+ return 0;
1779
+ }
1780
+ var ADD_KINDS = [
1781
+ "worker",
1782
+ "memory",
1783
+ "island",
1784
+ "service",
1785
+ "method",
1786
+ "module",
1787
+ "housed",
1788
+ "route",
1789
+ "component",
1790
+ "instrumentation"
1791
+ ];
1792
+ var KIND_SCOPE = {
1793
+ worker: null,
1794
+ memory: null,
1795
+ island: ISLAND_FRAMEWORKS,
1796
+ service: ["nestjs"],
1797
+ method: ["nestjs"],
1798
+ module: ["nestjs"],
1799
+ housed: ["nestjs"],
1800
+ route: ["nextjs"],
1801
+ component: ["nextjs"],
1802
+ instrumentation: ["nextjs"]
1803
+ };
1804
+ var KIND_VARIANTS = {
1805
+ island: ["facade", "mono"],
1806
+ route: ["task", "client"]
1807
+ };
1808
+ var FRAMEWORK_WORDS = [
1809
+ "angular",
1810
+ "nestjs",
1811
+ "nextjs",
1812
+ "node",
1813
+ "react",
1814
+ "solid",
1815
+ "svelte",
1816
+ "vanilla",
1817
+ "vue"
1818
+ ];
1819
+ async function pickName(ctx, kind, given) {
1820
+ const name = given ?? flag(ctx.args, "name") ?? await ctx.io.ask(`${kind} name (kebab-case):`, "");
1821
+ if (!name || !validName(name)) {
1822
+ ctx.io.error(
1823
+ `invalid ${kind} name ${ctx.io.fmt.accent(name)} \u2014 lowercase kebab-case, e.g. incidents-report`
1824
+ );
1825
+ return null;
1826
+ }
1827
+ return name;
1828
+ }
1829
+ function resolveMemory(ctx, dir, name) {
1830
+ if (hasFlag(ctx.args, "no-memory")) return null;
1831
+ const want = flag(ctx.args, "memory") ?? name;
1832
+ const direct = join3(dir, `${want}.memory.ts`);
1833
+ if (existsSync3(direct)) {
1834
+ return { import_: `./${want}.memory`, export_: `${camel(want)}Memory` };
1835
+ }
1836
+ const candidates = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && e.name.endsWith(".memory.ts")).map((e) => e.name.slice(0, -".memory.ts".length));
1837
+ const only = candidates.length === 1 ? candidates[0] : null;
1838
+ return only ? { import_: `./${only}.memory`, export_: `${camel(only)}Memory` } : null;
1839
+ }
1840
+ async function runAdd(ctx) {
1841
+ const project = detectProject(ctx.cwd);
1842
+ if (!project.root) {
1843
+ ctx.io.error("no package.json found \u2014 cd into a project or run `atoll new`");
1844
+ return 1;
1845
+ }
1846
+ const rel = (abs) => relative(ctx.cwd, abs).replaceAll("\\", "/");
1847
+ let rest = ctx.args._.slice(1);
1848
+ let framework = flag(ctx.args, "framework");
1849
+ if (rest[0] && FRAMEWORK_WORDS.includes(rest[0])) {
1850
+ framework ??= rest[0];
1851
+ rest = rest.slice(1);
1852
+ }
1853
+ framework ??= project.framework;
1854
+ let kind = rest[0];
1855
+ if (!kind) {
1856
+ const available = ADD_KINDS.filter(
1857
+ (k) => !KIND_SCOPE[k] || KIND_SCOPE[k].includes(framework)
1858
+ );
1859
+ kind = ctx.io.interactive ? await ctx.io.select("add what?", available, available[0]) : void 0;
1860
+ if (!kind) {
1861
+ ctx.io.error(`expected a kind \u2014 ${ADD_KINDS.join(" | ")}`);
1862
+ return 1;
1863
+ }
1864
+ rest = [kind, ...rest.slice(1)];
1865
+ }
1866
+ if (!ADD_KINDS.includes(kind)) {
1867
+ ctx.io.error(`unknown add target ${ctx.io.fmt.accent(kind)} \u2014 expected ${ADD_KINDS.join(" | ")}`);
1868
+ return 1;
1869
+ }
1870
+ const scope = KIND_SCOPE[kind];
1871
+ if (scope && !scope.includes(framework)) {
1872
+ ctx.io.error(
1873
+ `${kind} is for ${scope.join("/")} projects \u2014 detected "${framework}". Pass a framework word (e.g. \`atoll add nestjs ${kind} \u2026\`) to override.`
1874
+ );
1875
+ return 1;
1876
+ }
1877
+ const variants = KIND_VARIANTS[kind];
1878
+ let variant;
1879
+ let nameArg;
1880
+ if (variants) {
1881
+ if (rest[1] && variants.includes(rest[1])) {
1882
+ variant = rest[1];
1883
+ nameArg = rest[2];
1884
+ } else {
1885
+ nameArg = rest[1];
1886
+ }
1887
+ variant = flag(ctx.args, "variant") ?? variant;
1888
+ if (variant && !variants.includes(variant)) {
1889
+ ctx.io.error(`unknown ${kind} variant ${ctx.io.fmt.accent(variant)} \u2014 pick ${variants.join(" | ")}`);
1890
+ return 1;
1891
+ }
1892
+ variant ??= ctx.io.interactive ? await ctx.io.select(`${kind} variant:`, variants, variants[0]) : variants[0];
1893
+ } else {
1894
+ nameArg = rest[1];
1895
+ }
1896
+ const name = await pickName(ctx, kind, nameArg);
1897
+ if (!name) return 1;
1898
+ const force = { force: hasFlag(ctx.args, "force") };
1899
+ const atollDir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "atoll"));
1900
+ const c = camel(name);
1901
+ const P = pascal(name);
1902
+ switch (kind) {
1903
+ case "memory": {
1904
+ await writeTree(
1905
+ ctx.io,
1906
+ ctx.cwd,
1907
+ [{ path: `${atollDir}/${name}.memory.ts`, content: memoryContract(name) }],
1908
+ force
1909
+ );
1910
+ ctx.io.print(
1911
+ `
1912
+ bind it: pass ${ctx.io.fmt.accent(`${c}Memory`)} as ${ctx.io.fmt.accent("sharedMemory")} to defineWorker/connectWorker,`
1913
+ );
1914
+ ctx.io.print(
1915
+ `or ${ctx.io.fmt.accent(`atoll add worker ${name}`)} in the same directory wires it automatically.`
1916
+ );
1917
+ return 0;
1918
+ }
1919
+ case "worker": {
1920
+ const dir = atollDir;
1921
+ mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
1922
+ const host = hostOf(framework);
1923
+ const mem = resolveMemory(ctx, join3(ctx.cwd, dir), name);
1924
+ const opts = {
1925
+ host,
1926
+ memoryImport: mem?.import_ ?? null,
1927
+ memoryName: mem?.export_,
1928
+ distUrl: `${relative(join3(ctx.cwd, dir), join3(project.root, "dist")).replaceAll("\\", "/")}/${name}.worker.js`
1929
+ };
1930
+ const bindings = workerBindingsFile(name, framework, dir, mem);
1931
+ await writeTree(
1932
+ ctx.io,
1933
+ ctx.cwd,
1934
+ [
1935
+ { path: `${dir}/${name}.worker.ts`, content: workerEntry(name, opts) },
1936
+ { path: `${dir}/${name}.ts`, content: workerClientFile(name, opts) },
1937
+ ...bindings ? [bindings] : []
1938
+ ],
1939
+ force
1940
+ );
1941
+ ctx.io.print("");
1942
+ if (host === "node") {
1943
+ ctx.io.print(`bundle before spawning \u2014 add to package.json scripts:`);
1944
+ ctx.io.print(
1945
+ ctx.io.fmt.accent(
1946
+ ` "bundle:${name}": "esbuild ${dir}/${name}.worker.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/${name}.worker.js"`
1947
+ )
1948
+ );
1949
+ } else {
1950
+ ctx.io.print(
1951
+ `call ${ctx.io.fmt.accent(`${c}.echo('hi')`)} \u2014 the pool spawns lazily on first call`
1952
+ );
1953
+ if (bindings) ctx.io.print(`framework bindings: ${ctx.io.fmt.accent(bindings.path)}`);
1954
+ }
1955
+ return 0;
1956
+ }
1957
+ case "island": {
1958
+ const dir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "islands"));
1959
+ const files = variant === "facade" ? islandFacadeFiles(name, framework, dir) : islandFiles(name, framework, dir);
1960
+ await writeTree(ctx.io, ctx.cwd, files, force);
1961
+ const usage = variant === "facade" ? islandFacadeUsage(name, framework) : islandUsage(name, framework);
1962
+ if (usage) {
1963
+ ctx.io.print(ctx.io.fmt.strong("\nshell side:"));
1964
+ ctx.io.print(usage);
1965
+ }
1966
+ return 0;
1967
+ }
1968
+ case "service": {
1969
+ const dir = atollDir;
1970
+ mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
1971
+ const mem = resolveMemory(ctx, join3(ctx.cwd, dir), name);
1972
+ await writeTree(ctx.io, ctx.cwd, nestjsServiceFiles(name, dir, mem), force);
1973
+ ctx.io.print("");
1974
+ ctx.io.print(
1975
+ `inject ${ctx.io.fmt.accent(`${P}Service`)} anywhere \u2014 every method dispatches to the ${ctx.io.fmt.accent(`'${name}'`)} pool.`
1976
+ );
1977
+ return 0;
1978
+ }
1979
+ case "method": {
1980
+ const dir = atollDir;
1981
+ mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
1982
+ await writeTree(ctx.io, ctx.cwd, nestjsMethodFiles(name, dir), force);
1983
+ ctx.io.print("");
1984
+ ctx.io.print(
1985
+ `needs a '${name}' pool \u2014 ${ctx.io.fmt.accent(`atoll add nestjs module ${name}`)} registers one,`
1986
+ );
1987
+ ctx.io.print(`and the worker entry must runAtollWorker a module providing ${P}Service.`);
1988
+ return 0;
1989
+ }
1990
+ case "module": {
1991
+ const dir = atollDir;
1992
+ mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
1993
+ const mem = resolveMemory(ctx, join3(ctx.cwd, dir), name);
1994
+ await writeTree(ctx.io, ctx.cwd, nestjsModuleFiles(name, dir, { mem }), force);
1995
+ ctx.io.print("");
1996
+ ctx.io.print(
1997
+ `import ${ctx.io.fmt.accent(`${P}Module`)} into AppModule \u2014 then ${ctx.io.fmt.accent(`atoll add nestjs service ${name}`)} adds the facade.`
1998
+ );
1999
+ return 0;
2000
+ }
2001
+ case "housed": {
2002
+ const dir = atollDir;
2003
+ mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
2004
+ await writeTree(ctx.io, ctx.cwd, nestjsHousedFiles(name, dir), force);
2005
+ ctx.io.print("");
2006
+ ctx.io.print(ctx.io.fmt.strong("wire the gateway:"));
2007
+ ctx.io.print(nestjsHousedWiring(name));
2008
+ ctx.io.print(ctx.io.fmt.dim(`routes live only in workers \u2014 GET /api/${name}/whoami`));
2009
+ return 0;
2010
+ }
2011
+ case "route": {
2012
+ const dir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "app/api", name));
2013
+ await writeTree(
2014
+ ctx.io,
2015
+ ctx.cwd,
2016
+ nextjsRouteFiles(name, dir, variant),
2017
+ force
2018
+ );
2019
+ ctx.io.print("");
2020
+ ctx.io.print(
2021
+ `${ctx.io.fmt.accent(`POST /api/${name}`)} dispatches into the pool \xB7 ${ctx.io.fmt.accent(`GET /api/${name}`)} reads shared memory.`
2022
+ );
2023
+ ctx.io.print(
2024
+ ctx.io.fmt.dim(`warm it at boot: atoll add nextjs instrumentation ${name}`)
2025
+ );
2026
+ return 0;
2027
+ }
2028
+ case "component": {
2029
+ const dir = atollDir;
2030
+ mkdirSync2(join3(ctx.cwd, dir), { recursive: true });
2031
+ const mem = resolveMemory(ctx, join3(ctx.cwd, dir), name);
2032
+ await writeTree(ctx.io, ctx.cwd, nextjsComponentFiles(name, dir, mem), force);
2033
+ ctx.io.print("");
2034
+ ctx.io.print(
2035
+ `expects the ${ctx.io.fmt.accent(`./${name}`)} client + contract beside it \u2014 ${ctx.io.fmt.accent(`atoll add nextjs worker ${name}`)} first.`
2036
+ );
2037
+ return 0;
2038
+ }
2039
+ case "instrumentation": {
2040
+ await writeTree(
2041
+ ctx.io,
2042
+ ctx.cwd,
2043
+ [nextjsInstrumentationFile(name, rel(project.srcDir))],
2044
+ force
2045
+ );
2046
+ ctx.io.print(
2047
+ `
2048
+ pools warm at boot \u2014 extend register() as more routes appear.`
2049
+ );
2050
+ return 0;
2051
+ }
2052
+ default:
2053
+ ctx.io.error(`unhandled add kind ${kind}`);
2054
+ return 1;
2055
+ }
2056
+ }
2057
+ var NEW_FRAMEWORKS = ["react", "vue", "solid", "svelte", "node"];
2058
+ async function runCreate(ctx) {
2059
+ let dirName = ctx.args._[1] ?? flag(ctx.args, "dir");
2060
+ if (!dirName) dirName = await ctx.io.ask("project directory:", "my-atoll-app");
2061
+ if (!validName(dirName)) {
2062
+ ctx.io.error(`invalid directory name ${ctx.io.fmt.accent(dirName)}`);
2063
+ return 1;
2064
+ }
2065
+ const fwFlag = flag(ctx.args, "framework");
2066
+ const fw = fwFlag ?? await ctx.io.select("framework:", NEW_FRAMEWORKS, "react");
2067
+ if (fw === "angular") {
2068
+ ctx.io.error("angular apps scaffold through the CLI: `ng new <name>`, then `atoll init` + `atoll add island`");
2069
+ return 1;
2070
+ }
2071
+ if (!NEW_FRAMEWORKS.includes(fw)) {
2072
+ ctx.io.error(`unknown framework "${fw}" \u2014 pick from ${NEW_FRAMEWORKS.join(", ")}`);
2073
+ return 1;
2074
+ }
2075
+ const target = join3(ctx.cwd, dirName);
2076
+ if (existsSync3(target) && readdirSync(target).length > 0) {
2077
+ ctx.io.error(`${dirName}/ exists and is not empty`);
2078
+ return 1;
2079
+ }
2080
+ mkdirSync2(target, { recursive: true });
2081
+ const files = fw === "node" ? nodeScaffoldFiles(dirName) : scaffoldFiles(dirName, fw);
2082
+ await writeTree(ctx.io, target, files, { force: true });
2083
+ const written = JSON.parse(readFileSync2(join3(target, "package.json"), "utf8"));
2084
+ const allDeps = [
2085
+ ...Object.keys(written.dependencies ?? {}),
2086
+ ...Object.keys(written.devDependencies ?? {})
2087
+ ];
2088
+ const pm = flag(ctx.args, "pm") ?? "npm";
2089
+ await offerInstall(
2090
+ { ...ctx, cwd: target },
2091
+ { root: target, pkg: {}, framework: "vanilla", pm, srcDir: target },
2092
+ allDeps,
2093
+ true
2094
+ // fresh scaffold — the deps are declared, nothing is installed yet
2095
+ );
2096
+ ctx.io.print("");
2097
+ ctx.io.print(`done \u2014 ${ctx.io.fmt.accent(`cd ${dirName} && ${pm} run dev`)}`);
2098
+ return 0;
2099
+ }
2100
+ function runDoctor(ctx) {
2101
+ const project = detectProject(ctx.cwd);
2102
+ const fix = hasFlag(ctx.args, "fix");
2103
+ const checks = [
2104
+ {
2105
+ label: "node >= 22.18 (type-stripped CLI) / >= 20 (runtime)",
2106
+ run: () => {
2107
+ const [maj, min] = process.versions.node.split(".").map(Number);
2108
+ return maj > 22 || maj === 22 && min >= 18 ? { level: "pass", detail: `v${process.versions.node}` } : maj >= 20 ? { level: "warn", detail: `v${process.versions.node} \u2014 runtime OK; the CLI bin needs >=22.18` } : { level: "fail", detail: `v${process.versions.node}` };
2109
+ }
2110
+ },
2111
+ {
2112
+ label: "package.json",
2113
+ run: () => project.pkg ? { level: "pass", detail: `${project.root}/package.json` } : { level: "fail", detail: "not found \u2014 run `atoll new` or cd into a project" }
2114
+ },
2115
+ {
2116
+ label: "@atolljs/* dependencies",
2117
+ run: () => {
2118
+ const deps = { ...project.pkg?.dependencies, ...project.pkg?.devDependencies };
2119
+ const found = Object.keys(deps).filter((d) => d.startsWith("@atolljs/"));
2120
+ return found.length ? { level: "pass", detail: found.join(", ") } : { level: "warn", detail: "none \u2014 `atoll init` installs what the framework needs" };
2121
+ }
2122
+ },
2123
+ {
2124
+ label: ".npmrc supply-chain policy (min-release-age)",
2125
+ run: () => {
2126
+ const p = join3(project.root ?? ctx.cwd, ".npmrc");
2127
+ const has = existsSync3(p) && readFileSync2(p, "utf8").includes("min-release-age");
2128
+ if (has) return { level: "pass" };
2129
+ if (fix) {
2130
+ writeFileSync2(p, NPMRC);
2131
+ return { level: "pass", detail: "written (--fix)" };
2132
+ }
2133
+ return { level: "warn", detail: "missing \u2014 `atoll doctor --fix` writes the hardened .npmrc" };
2134
+ }
2135
+ },
2136
+ {
2137
+ label: "lockfile present",
2138
+ run: () => existsSync3(join3(project.root ?? ctx.cwd, "package-lock.json")) || existsSync3(join3(project.root ?? ctx.cwd, "pnpm-lock.yaml")) || existsSync3(join3(project.root ?? ctx.cwd, "yarn.lock")) ? { level: "pass" } : { level: "warn", detail: "no lockfile \u2014 installs are not reproducible" }
2139
+ },
2140
+ {
2141
+ label: "worker entries bundler-detectable",
2142
+ run: () => {
2143
+ if (!project.srcDir || !existsSync3(project.srcDir)) return { level: "pass" };
2144
+ let scanned = 0;
2145
+ let bad = 0;
2146
+ const walk = (d) => {
2147
+ for (const e of readdirSync(d, { withFileTypes: true })) {
2148
+ const p = join3(d, e.name);
2149
+ if (e.isDirectory() && e.name !== "node_modules") walk(p);
2150
+ else if (/\.(ts|tsx|js|mjs)$/.test(e.name)) {
2151
+ scanned++;
2152
+ const src = readFileSync2(p, "utf8");
2153
+ const entries = src.match(/new Worker\s*\(/g) ?? [];
2154
+ const detectable = src.match(/new Worker\s*\(\s*new URL\s*\(/g) ?? [];
2155
+ bad += entries.length - detectable.length;
2156
+ }
2157
+ }
2158
+ };
2159
+ walk(project.srcDir);
2160
+ return bad ? { level: "fail", detail: `${bad} new Worker() call(s) without \`new URL(..., import.meta.url)\` \u2014 bundlers can't see them` } : { level: "pass", detail: `${scanned} files scanned` };
2161
+ }
2162
+ },
2163
+ {
2164
+ label: "COOP/COEP headers (SharedArrayBuffer gate)",
2165
+ run: () => {
2166
+ if (NODE_HOSTS.includes(project.framework)) {
2167
+ return { level: "pass", detail: "n/a \u2014 node runtime" };
2168
+ }
2169
+ const vite = ["vite.config.ts", "vite.config.js", "vite.config.mts"].map((f) => join3(project.root ?? ctx.cwd, f)).find(existsSync3);
2170
+ if (!vite) return { level: "warn", detail: "no vite config found \u2014 verify headers on your server" };
2171
+ const src = readFileSync2(vite, "utf8");
2172
+ return src.includes("Cross-Origin-Opener-Policy") && src.includes("Cross-Origin-Embedder-Policy") ? { level: "pass" } : { level: "warn", detail: `${vite.split(/[\\/]/).pop()} lacks COOP/COEP \u2014 shared memory will be gated in dev` };
2173
+ }
2174
+ }
2175
+ ];
2176
+ let fails = 0;
2177
+ for (const c of checks) {
2178
+ const r = c.run();
2179
+ const mark = r.level === "pass" ? "ok" : r.level === "warn" ? "warn" : "error";
2180
+ ctx.io[mark](`${c.label}${r.detail ? ctx.io.fmt.dim(` \u2014 ${r.detail}`) : ""}`);
2181
+ if (r.level === "fail") fails++;
2182
+ }
2183
+ return fails ? 1 : 0;
2184
+ }
2185
+
2186
+ // src/cli.ts
2187
+ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fmt.dim("(experimental)")}
2188
+
2189
+ ${fmt.accent("atoll new <dir>")} [--framework react|vue|solid|svelte|node] [--pm npm]
2190
+ Scaffold a fresh app \u2014 vite shell + a worker-rendered island,
2191
+ or a tsx Node service with a pooled worker.
2192
+
2193
+ ${fmt.accent("atoll init")} [--name app] [--force]
2194
+ Wire Atoll into the current project: hardened .npmrc, package
2195
+ install, and a contract + worker + client spine under src/atoll/.
2196
+
2197
+ ${fmt.accent("atoll add [framework] <kind> [variant] [name]")}
2198
+ worker pool + typed client (+ framework bindings on UI projects)
2199
+ memory shared-memory contract
2200
+ island worker-rendered UI \u2014 facade (contract + lazy import) | mono
2201
+ service nestjs: @AtollService class facade + pool module + worker
2202
+ method nestjs: @AtollTask method-level offload
2203
+ module nestjs: registerPool boundary module + worker entry
2204
+ housed nestjs: route subtree served entirely inside workers
2205
+ route nextjs: api route pool \u2014 task | client variants
2206
+ component nextjs: 'use client' hooks component over a pool
2207
+ instrumentation nextjs: server bootstrap that warms route pools
2208
+
2209
+ Frameworks: react vue solid svelte angular nestjs nextjs node
2210
+ [--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
2211
+
2212
+ ${fmt.accent("atoll doctor")} [--fix]
2213
+ Check the setup \u2014 deps, lockfile, .npmrc policy, bundler-detectable
2214
+ worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
2215
+
2216
+ ${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
2217
+ --no-install skips dependency installation`)}
2218
+ `;
2219
+ var DISCLAIMER = "atoll is experimental \u2014 report bugs or feedback: https://github.com/jwhenry3/atolljs/issues";
2220
+ async function runCli(argv, io, cwd) {
2221
+ const args = parseArgs(argv);
2222
+ const ctx = { io, cwd, args };
2223
+ const [cmd] = args._;
2224
+ io.warn(io.fmt.dim(DISCLAIMER));
2225
+ try {
2226
+ switch (cmd) {
2227
+ case "new":
2228
+ case "create":
2229
+ return await runCreate(ctx);
2230
+ case "init":
2231
+ return await runInit(ctx);
2232
+ case "add":
2233
+ return await runAdd(ctx);
2234
+ case "doctor":
2235
+ return runDoctor(ctx);
2236
+ case "help":
2237
+ case void 0:
2238
+ io.print(HELP(io.fmt));
2239
+ return cmd === void 0 ? 0 : 0;
2240
+ default:
2241
+ io.error(`unknown command ${io.fmt.accent(cmd)}`);
2242
+ io.print(HELP(io.fmt));
2243
+ return 1;
2244
+ }
2245
+ } catch (e) {
2246
+ io.error(e instanceof Error ? e.message : String(e));
2247
+ return 1;
2248
+ } finally {
2249
+ io.close();
2250
+ }
2251
+ }
2252
+ var invokedAs = process.argv[1] && import.meta.url.endsWith(process.argv[1].replaceAll("\\", "/"));
2253
+ if (invokedAs) {
2254
+ const args = parseArgs(process.argv.slice(2));
2255
+ const interactive = process.stdin.isTTY === true && !hasFlag(args, "yes", "y");
2256
+ const code = await runCli(process.argv.slice(2), consoleIo(interactive), process.cwd());
2257
+ process.exit(code);
2258
+ }
2259
+ export {
2260
+ runCli
2261
+ };