@atolljs/cli 0.1.6 → 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/README.md CHANGED
@@ -42,6 +42,10 @@ atoll add mfe <name> publishable micro-frontend — framework-free
42
42
  contract (defineIslandContract) + worker entry
43
43
  attaching it + vite.mfe.config.ts publish build
44
44
  → dist-mfe/<name>.worker.js behind CORS
45
+ atoll add devtools [name] observability — src/atoll/devtools.ts init module
46
+ (import first in the app entry) + @atolljs/devtools
47
+ dev dep. Browser apps get the ?__atoll_devtools gate;
48
+ node hosts get the /node entry + ATOLL_DEVTOOLS=1
45
49
 
46
50
  atoll doctor [--fix] verify deps, lockfile, .npmrc policy,
47
51
  worker-entry detectability, COOP/COEP headers
package/dist/cli.js CHANGED
@@ -2194,6 +2194,57 @@ export async function register() {
2194
2194
  `
2195
2195
  };
2196
2196
  }
2197
+ function devtoolsSetup(name, host, wsUrl) {
2198
+ if (host === "node") {
2199
+ return `import { initDevtools } from '@atolljs/devtools/node';
2200
+
2201
+ /**
2202
+ * Devtools sink \u2014 imported first from the entry so it's installed before any
2203
+ * pool spawns (worker INIT carries the flag at spawn time; late installs
2204
+ * leave existing workers silent).
2205
+ *
2206
+ * No-op unless the process was started with ATOLL_DEVTOOLS=1 \u2014 then events
2207
+ * stream over WebSocket to the standalone dashboard (\`atoll devtools\`,
2208
+ * http://127.0.0.1:4780). Node has no BroadcastChannel page context, so the
2209
+ * aggregate server is the transport.
2210
+ */
2211
+ initDevtools({ session: { name: '${name}' } });
2212
+ `;
2213
+ }
2214
+ if (wsUrl) {
2215
+ return `import { initDevtools } from '@atolljs/devtools';
2216
+
2217
+ /**
2218
+ * Devtools sink \u2014 imported first from the app entry so it's installed before
2219
+ * any pool spawns (worker INIT carries the flag at spawn time; late installs
2220
+ * leave existing workers silent).
2221
+ *
2222
+ * No-op unless the page URL carries ?__atoll_devtools \u2014 then events stream
2223
+ * over WebSocket to the standalone aggregate dashboard (\`atoll devtools\`,
2224
+ * http://127.0.0.1:4780). This app's dev server doesn't mount /__atoll/, so
2225
+ * the overlay is off and the aggregate server is the viewer.
2226
+ */
2227
+ initDevtools({
2228
+ session: { name: '${name}' },
2229
+ overlay: false,
2230
+ url: '${wsUrl}',
2231
+ });
2232
+ `;
2233
+ }
2234
+ return `import { initDevtools } from '@atolljs/devtools';
2235
+
2236
+ /**
2237
+ * Devtools sink \u2014 imported first from the app entry so it's installed before
2238
+ * any pool spawns (worker INIT carries the flag at spawn time; late installs
2239
+ * leave existing workers silent).
2240
+ *
2241
+ * No-op unless the page URL carries ?__atoll_devtools \u2014 then events stream
2242
+ * over BroadcastChannel on the app's own origin and a flyout overlay mounts.
2243
+ * /__atoll/ itself is served by the @atolljs/vite plugin (plugins: [atoll()]).
2244
+ */
2245
+ initDevtools({ session: { name: '${name}' } });
2246
+ `;
2247
+ }
2197
2248
 
2198
2249
  // src/commands.ts
2199
2250
  var NODE_HOSTS = ["node", "nestjs"];
@@ -2292,7 +2343,8 @@ var ADD_KINDS = [
2292
2343
  "housed",
2293
2344
  "route",
2294
2345
  "component",
2295
- "instrumentation"
2346
+ "instrumentation",
2347
+ "devtools"
2296
2348
  ];
2297
2349
  var KIND_SCOPE = {
2298
2350
  worker: null,
@@ -2305,7 +2357,8 @@ var KIND_SCOPE = {
2305
2357
  housed: ["nestjs"],
2306
2358
  route: ["nextjs"],
2307
2359
  component: ["nextjs"],
2308
- instrumentation: ["nextjs"]
2360
+ instrumentation: ["nextjs"],
2361
+ devtools: null
2309
2362
  };
2310
2363
  var KIND_VARIANTS = {
2311
2364
  island: ["facade", "mono"],
@@ -2399,7 +2452,7 @@ async function runAdd(ctx) {
2399
2452
  } else {
2400
2453
  nameArg = rest[1];
2401
2454
  }
2402
- const name = await pickName(ctx, kind, nameArg);
2455
+ const name = kind === "devtools" ? nameArg ?? flag(ctx.args, "name") ?? project.pkg?.name?.split("/").pop() ?? "app" : await pickName(ctx, kind, nameArg);
2403
2456
  if (!name) return 1;
2404
2457
  const force = { force: hasFlag(ctx.args, "force") };
2405
2458
  const atollDir = flag(ctx.args, "dir") ?? rel(join3(project.srcDir, "atoll"));
@@ -2578,6 +2631,45 @@ pools warm at boot \u2014 extend register() as more routes appear.`
2578
2631
  );
2579
2632
  return 0;
2580
2633
  }
2634
+ case "devtools": {
2635
+ const host = hostOf(framework);
2636
+ const wsUrl = host === "browser" && framework === "angular" ? "ws://127.0.0.1:4780/events" : void 0;
2637
+ const file = `${atollDir}/devtools.ts`;
2638
+ await writeTree(
2639
+ ctx.io,
2640
+ ctx.cwd,
2641
+ [{ path: file, content: devtoolsSetup(name, host, wsUrl) }],
2642
+ force
2643
+ );
2644
+ if (missingDeps(project.pkg, ["@atolljs/devtools"]).length) {
2645
+ await offerInstall(ctx, project, ["-D", "@atolljs/devtools"]);
2646
+ }
2647
+ const srcRel = rel(project.srcDir);
2648
+ const spec = file.startsWith(`${srcRel}/`) ? `./${file.slice(srcRel.length + 1).replace(/\.ts$/, "")}` : `./${file.replace(/\.ts$/, "")}`;
2649
+ ctx.io.print("");
2650
+ ctx.io.print(ctx.io.fmt.strong("wire it:"));
2651
+ ctx.io.print(` import the module first in your entry \u2014 before any pool spawns:`);
2652
+ ctx.io.print(ctx.io.fmt.accent(` import '${spec}';`));
2653
+ if (host === "node") {
2654
+ ctx.io.print(
2655
+ ` ${ctx.io.fmt.accent("ATOLL_DEVTOOLS=1")} starts streaming \xB7 ${ctx.io.fmt.accent("atoll devtools")} serves the dashboard`
2656
+ );
2657
+ } else if (wsUrl) {
2658
+ ctx.io.print(
2659
+ ` ${ctx.io.fmt.accent("atoll devtools")} serves the aggregate dashboard \xB7 open the app with ${ctx.io.fmt.accent("?__atoll_devtools")}`
2660
+ );
2661
+ } else {
2662
+ ctx.io.print(
2663
+ ` open the app with ${ctx.io.fmt.accent("?__atoll_devtools")} \u2014 the flyout mounts and /__atoll/ serves the dashboard`
2664
+ );
2665
+ ctx.io.print(
2666
+ ctx.io.fmt.dim(
2667
+ ` /__atoll/ needs the @atolljs/vite plugin (plugins: [atoll()]) \u2014 no plugin? run \`atoll devtools\` + pass transport: 'websocket'`
2668
+ )
2669
+ );
2670
+ }
2671
+ return 0;
2672
+ }
2581
2673
  default:
2582
2674
  ctx.io.error(`unhandled add kind ${kind}`);
2583
2675
  return 1;
@@ -2639,6 +2731,26 @@ async function runCreate(ctx) {
2639
2731
  }
2640
2732
  return 0;
2641
2733
  }
2734
+ async function runDevtools(ctx) {
2735
+ const { io } = ctx;
2736
+ const spec = "@atolljs/devtools/server";
2737
+ const mod = await import(spec).catch(async () => {
2738
+ const local = new URL("../../devtools/dist/server.js", import.meta.url).href;
2739
+ return import(local).catch(() => null);
2740
+ });
2741
+ if (!mod?.createDevtoolsServer) {
2742
+ io.error(`@atolljs/devtools isn't installed \u2014 ${io.fmt.accent("npm i -D @atolljs/devtools")}`);
2743
+ return 1;
2744
+ }
2745
+ const port = Number(flag(ctx.args, "port") ?? 4780);
2746
+ const server = await mod.createDevtoolsServer({ port: Number.isFinite(port) ? port : 4780 });
2747
+ io.print(`atoll devtools \u2192 ${io.fmt.accent(server.url)}`);
2748
+ io.print(io.fmt.dim(" ingest endpoint: " + server.url.replace("http://", "ws://") + "/events"));
2749
+ io.print(io.fmt.dim(" in the app: connectDevtools() from '@atolljs/devtools'"));
2750
+ io.warn("serving \u2014 Ctrl+C to stop");
2751
+ return new Promise(() => {
2752
+ });
2753
+ }
2642
2754
  function runDoctor(ctx) {
2643
2755
  const project = detectProject(ctx.cwd);
2644
2756
  const fix = hasFlag(ctx.args, "fix");
@@ -2750,6 +2862,8 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
2750
2862
  route nextjs: api route pool \u2014 task | client variants
2751
2863
  component nextjs: 'use client' hooks component over a pool
2752
2864
  instrumentation nextjs: server bootstrap that warms route pools
2865
+ devtools observability \u2014 init module + @atolljs/devtools install
2866
+ (browser: ?__atoll_devtools + /__atoll/ \xB7 node: ATOLL_DEVTOOLS=1)
2753
2867
 
2754
2868
  Frameworks: react vue solid svelte angular nestjs nextjs node
2755
2869
  [--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
@@ -2758,6 +2872,10 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
2758
2872
  Check the setup \u2014 deps, lockfile, .npmrc policy, bundler-detectable
2759
2873
  worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
2760
2874
 
2875
+ ${fmt.accent("atoll devtools")} [--port 4780]
2876
+ Serve the local devtools dashboard \u2014 pools, task timings, shared-memory
2877
+ writes, island traffic. Apps connect via connectDevtools().
2878
+
2761
2879
  ${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
2762
2880
  --no-install skips dependency installation`)}
2763
2881
  `;
@@ -2778,6 +2896,8 @@ async function runCli(argv, io, cwd) {
2778
2896
  return await runAdd(ctx);
2779
2897
  case "doctor":
2780
2898
  return runDoctor(ctx);
2899
+ case "devtools":
2900
+ return await runDevtools(ctx);
2781
2901
  case "help":
2782
2902
  case void 0:
2783
2903
  io.print(HELP(io.fmt));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolljs/cli",
3
- "version": "0.1.6",
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,7 +12,7 @@
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
 
@@ -38,6 +38,8 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
38
38
  route nextjs: api route pool — task | client variants
39
39
  component nextjs: 'use client' hooks component over a pool
40
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)
41
43
 
42
44
  Frameworks: react vue solid svelte angular nestjs nextjs node
43
45
  [--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
@@ -46,6 +48,10 @@ const HELP = (fmt: Fmt) => `${fmt.strong('atoll')} — bootstrap multithreading
46
48
  Check the setup — deps, lockfile, .npmrc policy, bundler-detectable
47
49
  worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
48
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
+
49
55
  ${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
50
56
  --no-install skips dependency installation`)}
51
57
  `;
@@ -72,6 +78,8 @@ export async function runCli(argv: readonly string[], io: Io, cwd: string): Prom
72
78
  return await runAdd(ctx);
73
79
  case 'doctor':
74
80
  return runDoctor(ctx);
81
+ case 'devtools':
82
+ return await runDevtools(ctx);
75
83
  case 'help':
76
84
  case undefined:
77
85
  io.print(HELP(io.fmt));
package/src/commands.ts CHANGED
@@ -145,6 +145,7 @@ const ADD_KINDS = [
145
145
  'route',
146
146
  'component',
147
147
  'instrumentation',
148
+ 'devtools',
148
149
  ] as const;
149
150
  type AddKind = (typeof ADD_KINDS)[number];
150
151
 
@@ -161,6 +162,7 @@ const KIND_SCOPE: Record<AddKind, readonly Framework[] | null> = {
161
162
  route: ['nextjs'],
162
163
  component: ['nextjs'],
163
164
  instrumentation: ['nextjs'],
165
+ devtools: null,
164
166
  };
165
167
 
166
168
  /**
@@ -283,7 +285,12 @@ export async function runAdd(ctx: Ctx): Promise<number> {
283
285
  nameArg = rest[1];
284
286
  }
285
287
 
286
- 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);
287
294
  if (!name) return 1;
288
295
  const force = { force: hasFlag(ctx.args, 'force') };
289
296
  const atollDir = flag(ctx.args, 'dir') ?? rel(join(project.srcDir, 'atoll'));
@@ -480,6 +487,54 @@ export async function runAdd(ctx: Ctx): Promise<number> {
480
487
  return 0;
481
488
  }
482
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
+
483
538
  default:
484
539
  ctx.io.error(`unhandled add kind ${kind}`);
485
540
  return 1;
@@ -557,6 +612,39 @@ export async function runCreate(ctx: Ctx): Promise<number> {
557
612
  return 0;
558
613
  }
559
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
+
560
648
  /* ── atoll doctor ───────────────────────────────────────────────────────── */
561
649
 
562
650
  interface Check {
package/src/templates.ts CHANGED
@@ -2163,3 +2163,66 @@ export async function register() {
2163
2163
  };
2164
2164
  }
2165
2165
 
2166
+
2167
+ /**
2168
+ * `add devtools` — the init module, imported first from the app entry so the
2169
+ * sink is installed before any pool spawns (worker INIT carries the flag at
2170
+ * spawn time; late installs leave existing workers silent).
2171
+ *
2172
+ * Three shapes:
2173
+ * - node host → '@atolljs/devtools/node' entry, ATOLL_DEVTOOLS env gate
2174
+ * - browser → '@atolljs/devtools', ?__atoll_devtools URL gate + overlay
2175
+ * - browser + wsUrl → same browser gate but streams to the aggregate server
2176
+ * (angular: nothing mounts /__atoll/)
2177
+ */
2178
+ export function devtoolsSetup(name: string, host: 'browser' | 'node', wsUrl?: string): string {
2179
+ if (host === 'node') {
2180
+ return `import { initDevtools } from '@atolljs/devtools/node';
2181
+
2182
+ /**
2183
+ * Devtools sink — imported first from the entry so it's installed before any
2184
+ * pool spawns (worker INIT carries the flag at spawn time; late installs
2185
+ * leave existing workers silent).
2186
+ *
2187
+ * No-op unless the process was started with ATOLL_DEVTOOLS=1 — then events
2188
+ * stream over WebSocket to the standalone dashboard (\`atoll devtools\`,
2189
+ * http://127.0.0.1:4780). Node has no BroadcastChannel page context, so the
2190
+ * aggregate server is the transport.
2191
+ */
2192
+ initDevtools({ session: { name: '${name}' } });
2193
+ `;
2194
+ }
2195
+ if (wsUrl) {
2196
+ return `import { initDevtools } from '@atolljs/devtools';
2197
+
2198
+ /**
2199
+ * Devtools sink — imported first from the app entry so it's installed before
2200
+ * any pool spawns (worker INIT carries the flag at spawn time; late installs
2201
+ * leave existing workers silent).
2202
+ *
2203
+ * No-op unless the page URL carries ?__atoll_devtools — then events stream
2204
+ * over WebSocket to the standalone aggregate dashboard (\`atoll devtools\`,
2205
+ * http://127.0.0.1:4780). This app's dev server doesn't mount /__atoll/, so
2206
+ * the overlay is off and the aggregate server is the viewer.
2207
+ */
2208
+ initDevtools({
2209
+ session: { name: '${name}' },
2210
+ overlay: false,
2211
+ url: '${wsUrl}',
2212
+ });
2213
+ `;
2214
+ }
2215
+ return `import { initDevtools } from '@atolljs/devtools';
2216
+
2217
+ /**
2218
+ * Devtools sink — imported first from the app entry so it's installed before
2219
+ * any pool spawns (worker INIT carries the flag at spawn time; late installs
2220
+ * leave existing workers silent).
2221
+ *
2222
+ * No-op unless the page URL carries ?__atoll_devtools — then events stream
2223
+ * over BroadcastChannel on the app's own origin and a flyout overlay mounts.
2224
+ * /__atoll/ itself is served by the @atolljs/vite plugin (plugins: [atoll()]).
2225
+ */
2226
+ initDevtools({ session: { name: '${name}' } });
2227
+ `;
2228
+ }