@atolljs/cli 0.1.6 → 0.1.8
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 +4 -0
- package/dist/cli.js +140 -4
- package/package.json +1 -1
- package/src/cli.ts +10 -1
- package/src/commands.ts +115 -1
- package/src/templates.ts +64 -1
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
|
@@ -333,7 +333,7 @@ ${memImport}import type { ${P}Worker } from './${name}.worker';
|
|
|
333
333
|
export const ${c} = connectWorker<${P}Worker>({
|
|
334
334
|
worker: () =>
|
|
335
335
|
new Worker(new URL('./${name}.worker.ts', import.meta.url), { type: 'module' }),
|
|
336
|
-
${memLine}
|
|
336
|
+
${memLine} workers: 'auto',
|
|
337
337
|
});
|
|
338
338
|
export type ${P}Client = typeof ${c};
|
|
339
339
|
|
|
@@ -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,41 @@ 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 otlpEndpoint = flag(ctx.args, "otlp");
|
|
2747
|
+
const headers = Object.fromEntries(
|
|
2748
|
+
(flag(ctx.args, "otlp-headers") ?? "").split(",").filter((h) => h.includes("=")).map((h) => [h.slice(0, h.indexOf("=")).trim(), h.slice(h.indexOf("=") + 1).trim()])
|
|
2749
|
+
);
|
|
2750
|
+
const server = await mod.createDevtoolsServer({
|
|
2751
|
+
port: Number.isFinite(port) ? port : 4780,
|
|
2752
|
+
...otlpEndpoint ? {
|
|
2753
|
+
otlp: {
|
|
2754
|
+
endpoint: otlpEndpoint,
|
|
2755
|
+
headers,
|
|
2756
|
+
serviceName: flag(ctx.args, "otlp-service"),
|
|
2757
|
+
onError: (e) => io.warn(`otlp: ${e.message}`)
|
|
2758
|
+
}
|
|
2759
|
+
} : {}
|
|
2760
|
+
});
|
|
2761
|
+
io.print(`atoll devtools \u2192 ${io.fmt.accent(server.url)}`);
|
|
2762
|
+
io.print(io.fmt.dim(" ingest endpoint: " + server.url.replace("http://", "ws://") + "/events"));
|
|
2763
|
+
if (otlpEndpoint) io.print(io.fmt.dim(" otlp export: " + otlpEndpoint));
|
|
2764
|
+
io.print(io.fmt.dim(" in the app: connectDevtools() from '@atolljs/devtools'"));
|
|
2765
|
+
io.warn("serving \u2014 Ctrl+C to stop");
|
|
2766
|
+
return new Promise(() => {
|
|
2767
|
+
});
|
|
2768
|
+
}
|
|
2642
2769
|
function runDoctor(ctx) {
|
|
2643
2770
|
const project = detectProject(ctx.cwd);
|
|
2644
2771
|
const fix = hasFlag(ctx.args, "fix");
|
|
@@ -2750,6 +2877,8 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2750
2877
|
route nextjs: api route pool \u2014 task | client variants
|
|
2751
2878
|
component nextjs: 'use client' hooks component over a pool
|
|
2752
2879
|
instrumentation nextjs: server bootstrap that warms route pools
|
|
2880
|
+
devtools observability \u2014 init module + @atolljs/devtools install
|
|
2881
|
+
(browser: ?__atoll_devtools + /__atoll/ \xB7 node: ATOLL_DEVTOOLS=1)
|
|
2753
2882
|
|
|
2754
2883
|
Frameworks: react vue solid svelte angular nestjs nextjs node
|
|
2755
2884
|
[--dir <path>] [--memory <name>|--no-memory] [--variant <v>] [--force]
|
|
@@ -2758,6 +2887,11 @@ var HELP = (fmt) => `${fmt.strong("atoll")} \u2014 bootstrap multithreading ${fm
|
|
|
2758
2887
|
Check the setup \u2014 deps, lockfile, .npmrc policy, bundler-detectable
|
|
2759
2888
|
worker entries, COOP/COEP headers. --fix writes a hardened .npmrc.
|
|
2760
2889
|
|
|
2890
|
+
${fmt.accent("atoll devtools")} [--port 4780] [--otlp <url>] [--otlp-headers k=v,\u2026] [--otlp-service <name>]
|
|
2891
|
+
Serve the local devtools dashboard \u2014 pools, task timings, shared-memory
|
|
2892
|
+
writes, island traffic. Apps connect via connectDevtools().
|
|
2893
|
+
--otlp also forwards every session to an OpenTelemetry OTLP/HTTP endpoint.
|
|
2894
|
+
|
|
2761
2895
|
${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
|
|
2762
2896
|
--no-install skips dependency installation`)}
|
|
2763
2897
|
`;
|
|
@@ -2778,6 +2912,8 @@ async function runCli(argv, io, cwd) {
|
|
|
2778
2912
|
return await runAdd(ctx);
|
|
2779
2913
|
case "doctor":
|
|
2780
2914
|
return runDoctor(ctx);
|
|
2915
|
+
case "devtools":
|
|
2916
|
+
return await runDevtools(ctx);
|
|
2781
2917
|
case "help":
|
|
2782
2918
|
case void 0:
|
|
2783
2919
|
io.print(HELP(io.fmt));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atolljs/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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,11 @@ 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] [--otlp <url>] [--otlp-headers k=v,…] [--otlp-service <name>]
|
|
52
|
+
Serve the local devtools dashboard — pools, task timings, shared-memory
|
|
53
|
+
writes, island traffic. Apps connect via connectDevtools().
|
|
54
|
+
--otlp also forwards every session to an OpenTelemetry OTLP/HTTP endpoint.
|
|
55
|
+
|
|
49
56
|
${fmt.dim(`flags: --yes/-y accepts defaults, --force overwrites existing files,
|
|
50
57
|
--no-install skips dependency installation`)}
|
|
51
58
|
`;
|
|
@@ -72,6 +79,8 @@ export async function runCli(argv: readonly string[], io: Io, cwd: string): Prom
|
|
|
72
79
|
return await runAdd(ctx);
|
|
73
80
|
case 'doctor':
|
|
74
81
|
return runDoctor(ctx);
|
|
82
|
+
case 'devtools':
|
|
83
|
+
return await runDevtools(ctx);
|
|
75
84
|
case 'help':
|
|
76
85
|
case undefined:
|
|
77
86
|
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
|
-
|
|
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,65 @@ 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: {
|
|
629
|
+
createDevtoolsServer?: (o: {
|
|
630
|
+
port: number;
|
|
631
|
+
otlp?: { endpoint: string; headers: Record<string, string>; serviceName?: string; onError?: (e: Error) => void };
|
|
632
|
+
}) => Promise<{ url: string }>;
|
|
633
|
+
} | null =
|
|
634
|
+
await import(spec).catch(async () => {
|
|
635
|
+
// In-repo fallback: the workspace package's built dist (extensionless
|
|
636
|
+
// sources can't run under type stripping — `npm run build` there first).
|
|
637
|
+
const local = new URL('../../devtools/dist/server.js', import.meta.url).href;
|
|
638
|
+
return import(local).catch(() => null);
|
|
639
|
+
});
|
|
640
|
+
if (!mod?.createDevtoolsServer) {
|
|
641
|
+
io.error(`@atolljs/devtools isn't installed — ${io.fmt.accent('npm i -D @atolljs/devtools')}`);
|
|
642
|
+
return 1;
|
|
643
|
+
}
|
|
644
|
+
const port = Number(flag(ctx.args, 'port') ?? 4780);
|
|
645
|
+
const otlpEndpoint = flag(ctx.args, 'otlp');
|
|
646
|
+
// `k=v,k2=v2`: the OTEL_EXPORTER_OTLP_HEADERS format.
|
|
647
|
+
const headers = Object.fromEntries(
|
|
648
|
+
(flag(ctx.args, 'otlp-headers') ?? '')
|
|
649
|
+
.split(',')
|
|
650
|
+
.filter((h) => h.includes('='))
|
|
651
|
+
.map((h) => [h.slice(0, h.indexOf('=')).trim(), h.slice(h.indexOf('=') + 1).trim()]),
|
|
652
|
+
);
|
|
653
|
+
const server = await mod.createDevtoolsServer({
|
|
654
|
+
port: Number.isFinite(port) ? port : 4780,
|
|
655
|
+
...(otlpEndpoint
|
|
656
|
+
? {
|
|
657
|
+
otlp: {
|
|
658
|
+
endpoint: otlpEndpoint,
|
|
659
|
+
headers,
|
|
660
|
+
serviceName: flag(ctx.args, 'otlp-service'),
|
|
661
|
+
onError: (e: Error) => io.warn(`otlp: ${e.message}`),
|
|
662
|
+
},
|
|
663
|
+
}
|
|
664
|
+
: {}),
|
|
665
|
+
});
|
|
666
|
+
io.print(`atoll devtools → ${io.fmt.accent(server.url)}`);
|
|
667
|
+
io.print(io.fmt.dim(' ingest endpoint: ' + server.url.replace('http://', 'ws://') + '/events'));
|
|
668
|
+
if (otlpEndpoint) io.print(io.fmt.dim(' otlp export: ' + otlpEndpoint));
|
|
669
|
+
io.print(io.fmt.dim(" in the app: connectDevtools() from '@atolljs/devtools'"));
|
|
670
|
+
io.warn('serving — Ctrl+C to stop');
|
|
671
|
+
return new Promise<number>(() => {});
|
|
672
|
+
}
|
|
673
|
+
|
|
560
674
|
/* ── atoll doctor ───────────────────────────────────────────────────────── */
|
|
561
675
|
|
|
562
676
|
interface Check {
|
package/src/templates.ts
CHANGED
|
@@ -158,7 +158,7 @@ ${memImport}import type { ${P}Worker } from './${name}.worker';
|
|
|
158
158
|
export const ${c} = connectWorker<${P}Worker>({
|
|
159
159
|
worker: () =>
|
|
160
160
|
new Worker(new URL('./${name}.worker.ts', import.meta.url), { type: 'module' }),
|
|
161
|
-
${memLine}
|
|
161
|
+
${memLine} workers: 'auto',
|
|
162
162
|
});
|
|
163
163
|
export type ${P}Client = typeof ${c};
|
|
164
164
|
|
|
@@ -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
|
+
}
|