@atolljs/cli 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "atoll — scaffold worker pools, shared-memory contracts, and framework islands (experimental)",
5
5
  "homepage": "https://jwhenry3.github.io/atolljs/consumer/quickstart/",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -12,13 +12,14 @@
12
12
  */
13
13
  import { consoleIo, type Fmt, type Io } from './io.ts';
14
14
  import { hasFlag, parseArgs } from './parse.ts';
15
- import { runAdd, runCreate, runDoctor, runInit, type Ctx } from './commands.ts';
15
+ import { runAdd, runCreate, runDevtools, runDoctor, runInit, type Ctx } from './commands.ts';
16
16
 
17
17
  const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading ${fmt.dim('(experimental)')}
18
18
 
19
- ${fmt.accent('atoll new <dir>')} [--framework react|vue|solid|svelte|node] [--pm npm]
19
+ ${fmt.accent('atoll new <dir>')} [--framework react|vue|solid|svelte|node] [--mfe] [--pm npm]
20
20
  Scaffold a fresh app — vite shell + a worker-rendered island,
21
- or a tsx Node service with a pooled worker.
21
+ or a tsx Node service with a pooled worker. --mfe scaffolds a
22
+ publishable MFE package (contract + worker + bundle build).
22
23
 
23
24
  ${fmt.accent('atoll init')} [--name app] [--force]
24
25
  Wire Atoll into the current project: hardened .npmrc, package
@@ -28,6 +29,8 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
28
29
  worker pool + typed client (+ framework bindings on UI projects)
29
30
  memory shared-memory contract
30
31
  island worker-rendered UI — facade (contract + lazy import) | mono
32
+ mfe publishable micro-frontend — island contract + worker +
33
+ vite.mfe.config.ts (build a self-contained remote bundle)
31
34
  service nestjs: @AtollService class facade + pool module + worker
32
35
  method nestjs: @AtollTask method-level offload
33
36
  module nestjs: registerPool boundary module + worker entry
@@ -35,6 +38,8 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
35
38
  route nextjs: api route pool — task | client variants
36
39
  component nextjs: 'use client' hooks component over a pool
37
40
  instrumentation nextjs: server bootstrap that warms route pools
41
+ devtools observability — init module + @atolljs/devtools install
42
+ (browser: ?__atoll_devtools + /__atoll/ · node: ATOLL_DEVTOOLS=1)
38
43
 
39
44
  Frameworks: react vue solid svelte angular nestjs nextjs node
40
45
  [--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
@@ -43,6 +48,10 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
43
48
  Check the setup — deps, lockfile, .npmrc policy, bundler-detectable
44
49
  worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
45
50
 
51
+ ${fmt.accent('atoll devtools')} [--port 4780]
52
+ Serve the local devtools dashboard — pools, task timings, shared-memory
53
+ writes, island traffic. Apps connect via connectDevtools().
54
+
46
55
  ${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
47
56
  --no-install skips dependency installation`)}
48
57
  `;
@@ -69,6 +78,8 @@ export async function runCli(argv: readonly string[], io: Io, cwd: string): Prom
69
78
  return await runAdd(ctx);
70
79
  case 'doctor':
71
80
  return runDoctor(ctx);
81
+ case 'devtools':
82
+ return await runDevtools(ctx);
72
83
  case 'help':
73
84
  case undefined:
74
85
  io.print(HELP(io.fmt));
package/src/commands.ts CHANGED
@@ -137,6 +137,7 @@ const ADD_KINDS = [
137
137
  'worker',
138
138
  'memory',
139
139
  'island',
140
+ 'mfe',
140
141
  'service',
141
142
  'method',
142
143
  'module',
@@ -144,6 +145,7 @@ const ADD_KINDS = [
144
145
  'route',
145
146
  'component',
146
147
  'instrumentation',
148
+ 'devtools',
147
149
  ] as const;
148
150
  type AddKind = (typeof ADD_KINDS)[number];
149
151
 
@@ -152,6 +154,7 @@ const KIND_SCOPE: Record<AddKind, readonly Framework[] | null> = {
152
154
  worker: null,
153
155
  memory: null,
154
156
  island: ISLAND_FRAMEWORKS,
157
+ mfe: ISLAND_FRAMEWORKS,
155
158
  service: ['nestjs'],
156
159
  method: ['nestjs'],
157
160
  module: ['nestjs'],
@@ -159,6 +162,7 @@ const KIND_SCOPE: Record<AddKind, readonly Framework[] | null> = {
159
162
  route: ['nextjs'],
160
163
  component: ['nextjs'],
161
164
  instrumentation: ['nextjs'],
165
+ devtools: null,
162
166
  };
163
167
 
164
168
  /**
@@ -281,7 +285,12 @@ export async function runAdd(ctx: Ctx): Promise<number> {
281
285
  nameArg = rest[1];
282
286
  }
283
287
 
284
- const name = await pickName(ctx, kind, nameArg);
288
+ // devtools writes a fixed devtools.ts — a positional/--name argument
289
+ // becomes the dashboard session label, not a file name.
290
+ const name =
291
+ kind === 'devtools'
292
+ ? (nameArg ?? flag(ctx.args, 'name') ?? project.pkg?.name?.split('/').pop() ?? 'app')
293
+ : await pickName(ctx, kind, nameArg);
285
294
  if (!name) return 1;
286
295
  const force = { force: hasFlag(ctx.args, 'force') };
287
296
  const atollDir = flag(ctx.args, 'dir') ?? rel(join(project.srcDir, 'atoll'));
@@ -360,6 +369,34 @@ export async function runAdd(ctx: Ctx): Promise<number> {
360
369
  return 0;
361
370
  }
362
371
 
372
+ case 'mfe': {
373
+ const dir = flag(ctx.args, 'dir') ?? rel(join(project.srcDir, 'mfe'));
374
+ const files = T.mfeFiles(name, framework, dir);
375
+ // The publish build lives at the project root beside vite.config.ts —
376
+ // it exists only for `vite build --config`, never the app dev server.
377
+ const publishCfg = rel(join(project.root!, 'vite.mfe.config.ts'));
378
+ // The config lives at the project root — the lib entry must be
379
+ // root-relative even when `add` ran from a subdirectory or --dir moved.
380
+ const dirFromRoot = relative(project.root!, join(ctx.cwd, dir)).replaceAll('\\', '/');
381
+ await writeTree(
382
+ ctx.io,
383
+ ctx.cwd,
384
+ [
385
+ ...files,
386
+ { path: publishCfg, content: T.mfePublishConfig(name, framework, dirFromRoot) },
387
+ ],
388
+ force,
389
+ );
390
+ if (missingDeps(project.pkg, ['vite']).length) {
391
+ ctx.io.warn(
392
+ `vite isn't installed — the publish build needs it: ${ctx.io.fmt.accent(pmInstallCmd(project.pm, ['-D', 'vite']))}`,
393
+ );
394
+ }
395
+ ctx.io.print(ctx.io.fmt.strong('\nwire it:'));
396
+ ctx.io.print(T.mfeUsage(name, framework, dir));
397
+ return 0;
398
+ }
399
+
363
400
  case 'service': {
364
401
  const dir = atollDir;
365
402
  mkdirSync(join(ctx.cwd, dir), { recursive: true });
@@ -450,6 +487,54 @@ export async function runAdd(ctx: Ctx): Promise<number> {
450
487
  return 0;
451
488
  }
452
489
 
490
+ case 'devtools': {
491
+ const host = hostOf(framework);
492
+ // Angular's CLI dev server mounts no /__atoll/ — stream to the
493
+ // aggregate server instead of the BroadcastChannel + flyout path.
494
+ const wsUrl = host === 'browser' && framework === 'angular'
495
+ ? 'ws://127.0.0.1:4780/events'
496
+ : undefined;
497
+ const file = `${atollDir}/devtools.ts`;
498
+ await writeTree(
499
+ ctx.io,
500
+ ctx.cwd,
501
+ [{ path: file, content: T.devtoolsSetup(name, host, wsUrl) }],
502
+ force,
503
+ );
504
+ if (missingDeps(project.pkg, ['@atolljs/devtools']).length) {
505
+ await offerInstall(ctx, project, ['-D', '@atolljs/devtools']);
506
+ }
507
+ // Print the specifier relative to srcDir — the import lands in the
508
+ // app entry (src/main.ts style), not in cwd.
509
+ const srcRel = rel(project.srcDir);
510
+ const spec = file.startsWith(`${srcRel}/`)
511
+ ? `./${file.slice(srcRel.length + 1).replace(/\.ts$/, '')}`
512
+ : `./${file.replace(/\.ts$/, '')}`;
513
+ ctx.io.print('');
514
+ ctx.io.print(ctx.io.fmt.strong('wire it:'));
515
+ ctx.io.print(` import the module first in your entry — before any pool spawns:`);
516
+ ctx.io.print(ctx.io.fmt.accent(` import '${spec}';`));
517
+ if (host === 'node') {
518
+ ctx.io.print(
519
+ ` ${ctx.io.fmt.accent('ATOLL_DEVTOOLS=1')} starts streaming · ${ctx.io.fmt.accent('atoll devtools')} serves the dashboard`,
520
+ );
521
+ } else if (wsUrl) {
522
+ ctx.io.print(
523
+ ` ${ctx.io.fmt.accent('atoll devtools')} serves the aggregate dashboard · open the app with ${ctx.io.fmt.accent('?__atoll_devtools')}`,
524
+ );
525
+ } else {
526
+ ctx.io.print(
527
+ ` open the app with ${ctx.io.fmt.accent('?__atoll_devtools')} — the flyout mounts and /__atoll/ serves the dashboard`,
528
+ );
529
+ ctx.io.print(
530
+ ctx.io.fmt.dim(
531
+ ` /__atoll/ needs the @atolljs/vite plugin (plugins: [atoll()]) — no plugin? run \`atoll devtools\` + pass transport: 'websocket'`,
532
+ ),
533
+ );
534
+ }
535
+ return 0;
536
+ }
537
+
453
538
  default:
454
539
  ctx.io.error(`unhandled add kind ${kind}`);
455
540
  return 1;
@@ -488,8 +573,17 @@ export async function runCreate(ctx: Ctx): Promise<number> {
488
573
  }
489
574
  mkdirSync(target, { recursive: true });
490
575
 
576
+ const mfe = hasFlag(ctx.args, 'mfe');
577
+ if (mfe && fw === 'node') {
578
+ ctx.io.error('--mfe is a browser-island shape — pick react|vue|solid|svelte');
579
+ return 1;
580
+ }
491
581
  const files =
492
- fw === 'node' ? T.nodeScaffoldFiles(dirName) : T.scaffoldFiles(dirName, fw);
582
+ fw === 'node'
583
+ ? T.nodeScaffoldFiles(dirName)
584
+ : mfe
585
+ ? T.mfeScaffoldFiles(dirName, fw)
586
+ : T.scaffoldFiles(dirName, fw);
493
587
  await writeTree(ctx.io, target, files, { force: true });
494
588
 
495
589
  const written = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
@@ -507,9 +601,50 @@ export async function runCreate(ctx: Ctx): Promise<number> {
507
601
 
508
602
  ctx.io.print('');
509
603
  ctx.io.print(`done — ${ctx.io.fmt.accent(`cd ${dirName} && ${pm} run dev`)}`);
604
+ if (mfe) {
605
+ ctx.io.print(
606
+ ` ${ctx.io.fmt.accent('npm run build:mfe')} → dist-mfe/${dirName}.worker.js — publish behind CORS + a versioned URL`,
607
+ );
608
+ ctx.io.print(
609
+ ` ${ctx.io.fmt.accent('npm run preview:mfe')} serves it cross-origin; shells mount the contract`,
610
+ );
611
+ }
510
612
  return 0;
511
613
  }
512
614
 
615
+ /* ── atoll devtools ─────────────────────────────────────────────────────── */
616
+
617
+ /**
618
+ * `atoll devtools [--port N]` — boot the local devtools server + dashboard.
619
+ * The server lives in @atolljs/devtools: resolved as an installed package in
620
+ * consumer projects, with a relative fallback so `npm run atoll devtools`
621
+ * works inside the atoll monorepo where the package isn't installed.
622
+ *
623
+ * Never resolves — the process serves until SIGINT.
624
+ */
625
+ export async function runDevtools(ctx: Ctx): Promise<number> {
626
+ const { io } = ctx;
627
+ const spec = '@atolljs/devtools/server';
628
+ const mod: { createDevtoolsServer?: (o: { port: number }) => Promise<{ url: string }> } | null =
629
+ await import(spec).catch(async () => {
630
+ // In-repo fallback: the workspace package's built dist (extensionless
631
+ // sources can't run under type stripping — `npm run build` there first).
632
+ const local = new URL('../../devtools/dist/server.js', import.meta.url).href;
633
+ return import(local).catch(() => null);
634
+ });
635
+ if (!mod?.createDevtoolsServer) {
636
+ io.error(`@atolljs/devtools isn't installed — ${io.fmt.accent('npm i -D @atolljs/devtools')}`);
637
+ return 1;
638
+ }
639
+ const port = Number(flag(ctx.args, 'port') ?? 4780);
640
+ const server = await mod.createDevtoolsServer({ port: Number.isFinite(port) ? port : 4780 });
641
+ io.print(`atoll devtools → ${io.fmt.accent(server.url)}`);
642
+ io.print(io.fmt.dim(' ingest endpoint: ' + server.url.replace('http://', 'ws://') + '/events'));
643
+ io.print(io.fmt.dim(" in the app: connectDevtools() from '@atolljs/devtools'"));
644
+ io.warn('serving — Ctrl+C to stop');
645
+ return new Promise<number>(() => {});
646
+ }
647
+
513
648
  /* ── atoll doctor ───────────────────────────────────────────────────────── */
514
649
 
515
650
  interface Check {