@nwire/cli 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/commands/cache.js +1 -1
- package/dist/commands/dev.d.ts +20 -7
- package/dist/commands/dev.js +142 -45
- package/dist/commands/ls.js +5 -3
- package/dist/commands/please.js +4 -2
- package/dist/commands/run.js +6 -2
- package/dist/commands/studio.js +74 -7
- package/dist/lib/dev-entry.d.ts +8 -9
- package/dist/lib/dev-entry.js +24 -25
- package/dist/lib/dev-host.d.ts +88 -0
- package/dist/lib/dev-host.js +426 -0
- package/dist/lib/ensure-built.d.ts +32 -0
- package/dist/lib/ensure-built.js +62 -0
- package/dist/lib/ensure-scan.d.ts +9 -4
- package/dist/lib/ensure-scan.js +46 -19
- package/dist/lib/studio-host-api.d.ts +97 -0
- package/dist/lib/studio-host-api.js +336 -0
- package/dist/lib/studio-probe.d.ts +63 -0
- package/dist/lib/studio-probe.js +132 -0
- package/dist/lib/vite-node.d.ts +7 -6
- package/dist/lib/vite-node.js +8 -8
- package/dist/lib/vite-run.d.ts +15 -0
- package/dist/lib/vite-run.js +33 -0
- package/dist/load-config.d.ts +10 -10
- package/dist/load-config.js +3 -3
- package/dist/studio/assets/abap-DVwoIrM0.js +1 -0
- package/dist/studio/assets/apex-B9XtvxSu.js +1 -0
- package/dist/studio/assets/azcli-D7JTNGKs.js +1 -0
- package/dist/studio/assets/bat-BNHAuPwR.js +1 -0
- package/dist/studio/assets/bicep-C3w6oSfK.js +2 -0
- package/dist/studio/assets/cameligo-DM9kSiq7.js +1 -0
- package/dist/studio/assets/clojure-FWLBUPxU.js +1 -0
- package/dist/studio/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/dist/studio/assets/coffee-DCoMPIwW.js +1 -0
- package/dist/studio/assets/cpp-BNbIvdcw.js +1 -0
- package/dist/studio/assets/csharp-Dj4ULDZr.js +1 -0
- package/dist/studio/assets/csp-C-n5jZKF.js +1 -0
- package/dist/studio/assets/css-COIa8ZTR.js +3 -0
- package/dist/studio/assets/css.worker-CpJJqcA4.js +89 -0
- package/dist/studio/assets/cssMode-CFR5_xwk.js +1 -0
- package/dist/studio/assets/cypher-CW08XVUh.js +1 -0
- package/dist/studio/assets/dart-Bz550Pyv.js +1 -0
- package/dist/studio/assets/dockerfile-DW5REF8E.js +1 -0
- package/dist/studio/assets/ecl-BqdYhwmw.js +1 -0
- package/dist/studio/assets/editor-Br_kD0ds.css +1 -0
- package/dist/studio/assets/editor.api2-CTGEM8gT.js +872 -0
- package/dist/studio/assets/editor.main-sW1qgHFj.js +6 -0
- package/dist/studio/assets/elixir-Oi_9aIAu.js +1 -0
- package/dist/studio/assets/flow9-CIb9youF.js +1 -0
- package/dist/studio/assets/freemarker2-CPrni8hw.js +3 -0
- package/dist/studio/assets/fsharp-64tUaD-0.js +1 -0
- package/dist/studio/assets/go-DLKGL0rd.js +1 -0
- package/dist/studio/assets/graphql-Bz88xn3Q.js +1 -0
- package/dist/studio/assets/handlebars-DkvSNpQB.js +1 -0
- package/dist/studio/assets/hcl-Cq76tSVN.js +1 -0
- package/dist/studio/assets/html-ceN7ITxG.js +1 -0
- package/dist/studio/assets/html.worker-wsVgX3gp.js +502 -0
- package/dist/studio/assets/htmlMode-wduanCXn.js +1 -0
- package/dist/studio/assets/index-4tH0-1cA.js +41 -0
- package/dist/studio/assets/index-Fy3xDmV2.css +1 -0
- package/dist/studio/assets/ini-BTNe9zdh.js +1 -0
- package/dist/studio/assets/java-DzRJKRF3.js +1 -0
- package/dist/studio/assets/javascript-WF3LGLje.js +1 -0
- package/dist/studio/assets/json.worker-CcNiYOcv.js +58 -0
- package/dist/studio/assets/jsonMode-DSujY8tB.js +7 -0
- package/dist/studio/assets/julia-Bgv08lKa.js +1 -0
- package/dist/studio/assets/kotlin-Dzz8TWAt.js +1 -0
- package/dist/studio/assets/less-ak6GUtsl.js +2 -0
- package/dist/studio/assets/lexon-zuaObGAE.js +1 -0
- package/dist/studio/assets/liquid-C5Z7zFr3.js +1 -0
- package/dist/studio/assets/lspLanguageFeatures-B55yfFgf.js +4 -0
- package/dist/studio/assets/lua-ClKCZMmP.js +1 -0
- package/dist/studio/assets/m3-C7XHeDz_.js +1 -0
- package/dist/studio/assets/markdown-LT3qFBoR.js +1 -0
- package/dist/studio/assets/mdx-Bu5jRl19.js +1 -0
- package/dist/studio/assets/mips-B8clQ9KB.js +1 -0
- package/dist/studio/assets/monaco.contribution-KjQ4yOxj.js +2 -0
- package/dist/studio/assets/msdax-DBxc5qPJ.js +1 -0
- package/dist/studio/assets/mysql-qocW_xba.js +1 -0
- package/dist/studio/assets/objective-c-DhkpBlGX.js +1 -0
- package/dist/studio/assets/pascal-C_PJR40u.js +1 -0
- package/dist/studio/assets/pascaligo-BI_Gz9Bp.js +1 -0
- package/dist/studio/assets/perl-CIqGOHTo.js +1 -0
- package/dist/studio/assets/pgsql-DI_z9qfW.js +1 -0
- package/dist/studio/assets/php-Dkwn_yn0.js +1 -0
- package/dist/studio/assets/pla-DvzjACL6.js +1 -0
- package/dist/studio/assets/postiats-Cc9-hkUx.js +1 -0
- package/dist/studio/assets/powerquery-IGzsITFg.js +1 -0
- package/dist/studio/assets/powershell-BHlZlUN6.js +1 -0
- package/dist/studio/assets/protobuf-pGrmMUz5.js +2 -0
- package/dist/studio/assets/pug-B4eH693Y.js +1 -0
- package/dist/studio/assets/python-DpEFuk0I.js +1 -0
- package/dist/studio/assets/qsharp-CwO3kTIU.js +1 -0
- package/dist/studio/assets/r-CiZUpdIa.js +1 -0
- package/dist/studio/assets/razor-BF1svRn9.js +1 -0
- package/dist/studio/assets/redis-DjdIzLdf.js +1 -0
- package/dist/studio/assets/redshift-vCL5QMyw.js +1 -0
- package/dist/studio/assets/restructuredtext-D5hoMHB1.js +1 -0
- package/dist/studio/assets/ruby-ByPQrqP4.js +1 -0
- package/dist/studio/assets/rust-Nz5wukP7.js +1 -0
- package/dist/studio/assets/sb-DgR1RbMJ.js +1 -0
- package/dist/studio/assets/scala-BCgNuXrV.js +1 -0
- package/dist/studio/assets/scheme-TgKpKGpb.js +1 -0
- package/dist/studio/assets/scss-BKxAkvnT.js +3 -0
- package/dist/studio/assets/shell-COgstXIQ.js +1 -0
- package/dist/studio/assets/solidity-DaqmtBSV.js +1 -0
- package/dist/studio/assets/sophia-Da67i9pL.js +1 -0
- package/dist/studio/assets/sparql-CtN8jEDV.js +1 -0
- package/dist/studio/assets/sql-BnJfQHXt.js +1 -0
- package/dist/studio/assets/st-cGKU4FoP.js +1 -0
- package/dist/studio/assets/swift-DyyME8zA.js +1 -0
- package/dist/studio/assets/systemverilog-BYZY5TEG.js +1 -0
- package/dist/studio/assets/tcl-CHv1_zaR.js +1 -0
- package/dist/studio/assets/ts.worker-DfMAw22J.js +67719 -0
- package/dist/studio/assets/tsMode-BiiF1JOM.js +11 -0
- package/dist/studio/assets/twig-CNwULq4h.js +1 -0
- package/dist/studio/assets/typescript-BefpzegH.js +1 -0
- package/dist/studio/assets/typespec-B3KUNs_P.js +1 -0
- package/dist/studio/assets/vb-phZZ2pCs.js +1 -0
- package/dist/studio/assets/wgsl-Df-y4I4e.js +298 -0
- package/dist/studio/assets/workers-DqAl3RFu.js +1 -0
- package/dist/studio/assets/xml-CWw7bbeo.js +1 -0
- package/dist/studio/assets/yaml-CZ0k9DUm.js +1 -0
- package/dist/studio/index.html +13 -0
- package/dist/wire-runner.d.ts +12 -0
- package/dist/wire-runner.js +19 -0
- package/package.json +8 -6
- package/dist/cache-runner.d.ts +0 -1
- package/dist/cache-runner.js +0 -206
package/dist/lib/ensure-scan.js
CHANGED
|
@@ -4,36 +4,63 @@
|
|
|
4
4
|
* every CLI command that reads the scan cache.
|
|
5
5
|
*
|
|
6
6
|
* Cheap path: compute fingerprint, compare to saved; if match, return.
|
|
7
|
-
* Expensive path:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Expensive path: rebuild the manifest IN-PROCESS via `@nwire/scan`'s
|
|
8
|
+
* `buildManifest` + `writeManifest` — a pure static scan of the source
|
|
9
|
+
* tree plus the folded `.nwire/topology.json` a running app self-emits.
|
|
10
|
+
* No app boot, no loader, no subprocess.
|
|
11
|
+
*
|
|
12
|
+
* If `.nwire/topology.json` is absent (the app has never run), the
|
|
13
|
+
* manifest still writes — static-only; the topology layer fills in the
|
|
14
|
+
* next time the app runs and re-emits.
|
|
10
15
|
*/
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
16
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { resolve } from "node:path";
|
|
18
|
+
import { buildManifest, writeManifest } from "@nwire/scan";
|
|
14
19
|
import { palette } from "./colors.js";
|
|
15
|
-
import {
|
|
20
|
+
import { loadConfig } from "../load-config.js";
|
|
16
21
|
import { fingerprint, isFresh, writeFingerprint } from "./scan-cache.js";
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Read the app name from the emitted `<cwd>/.nwire/topology.json` so the
|
|
24
|
+
* static manifest tags records with the same name a running app reports.
|
|
25
|
+
* The static scan treats the name as a label only, so an empty string is
|
|
26
|
+
* harmless when no topology has been emitted yet.
|
|
27
|
+
*/
|
|
28
|
+
function appNameFromTopology(cwd) {
|
|
29
|
+
const p = resolve(cwd, ".nwire", "topology.json");
|
|
30
|
+
if (!existsSync(p))
|
|
31
|
+
return "";
|
|
32
|
+
try {
|
|
33
|
+
const file = JSON.parse(readFileSync(p, "utf8"));
|
|
34
|
+
return file.topology?.apps?.[0]?.name ?? "";
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function ensureScanFresh(cwd = process.cwd(), opts = {}) {
|
|
19
41
|
const fp = fingerprint(cwd);
|
|
20
42
|
if (!opts.force && isFresh(cwd, fp)) {
|
|
21
43
|
return { rebuilt: false, fingerprint: fp };
|
|
22
44
|
}
|
|
23
|
-
const builderPath = resolve(here, "..", "cache-runner.js");
|
|
24
|
-
if (!existsSync(builderPath)) {
|
|
25
|
-
// Builder is missing from dist (dev-mode oddity). Skip — commands
|
|
26
|
-
// that need the cache will surface their own "no manifest" error.
|
|
27
|
-
return { rebuilt: false, fingerprint: fp };
|
|
28
|
-
}
|
|
29
45
|
if (!opts.quiet) {
|
|
30
46
|
// eslint-disable-next-line no-console
|
|
31
47
|
console.error(palette.dim("scanning project…"));
|
|
32
48
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
49
|
+
try {
|
|
50
|
+
const config = await loadConfig(cwd);
|
|
51
|
+
const appName = appNameFromTopology(cwd);
|
|
52
|
+
// Pure static scan + folded topology.json — no app boot, no loader.
|
|
53
|
+
const manifest = buildManifest(cwd, appName);
|
|
54
|
+
const outDir = resolve(cwd, config.cacheDir ?? ".nwire");
|
|
55
|
+
await writeManifest(manifest, outDir);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (!opts.quiet) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.error(palette.err("nwire cache:") + ` scan failed — ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
// Keep whatever's on disk; don't update the fingerprint so the next
|
|
63
|
+
// call retries.
|
|
37
64
|
return { rebuilt: false, fingerprint: fp };
|
|
38
65
|
}
|
|
39
66
|
writeFingerprint(cwd, fp);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio data API for the one-Vite dev host.
|
|
3
|
+
*
|
|
4
|
+
* The standalone Studio process serves `/__nwire/*` by reading `.nwire/`
|
|
5
|
+
* from disk and proxying `/_nwire/*` to a separately-started wire process.
|
|
6
|
+
* The one-Vite host has the wire running IN-PROCESS, which is strictly better:
|
|
7
|
+
*
|
|
8
|
+
* - telemetry/live: subscribe `runtime.onTelemetry` directly — no disk writes
|
|
9
|
+
* required, records arrive at zero-latency with no file-tail polling loop.
|
|
10
|
+
* - manifest.json: serve the disk `.nwire/manifest.json` (built by `nwire cache`)
|
|
11
|
+
* and, when a live runtime is present, fold in the runtime topology so the
|
|
12
|
+
* manifest stays fresh even during HMR reloads without a full cache rebuild.
|
|
13
|
+
* - telemetry/runs: disk-based history — same as standalone Studio (these are
|
|
14
|
+
* past run files; there is no in-process equivalent).
|
|
15
|
+
*
|
|
16
|
+
* Routes owned here (all under `/__nwire/*`):
|
|
17
|
+
*
|
|
18
|
+
* GET /__nwire/manifest.json disk (.nwire/manifest.json) + runtime fold
|
|
19
|
+
* GET /__nwire/telemetry/live SSE — runtime.onTelemetry (runtime-direct)
|
|
20
|
+
* GET /__nwire/telemetry/runs disk list { runs: TelemetryRunMeta[] }
|
|
21
|
+
* GET /__nwire/telemetry/runs/:id disk single run { id, records: unknown[] }
|
|
22
|
+
* GET /__nwire/endpoints list { endpoints: EndpointInfo[] }
|
|
23
|
+
* POST /__nwire/endpoints/active { name } → switch active HTTP front
|
|
24
|
+
* * /__nwire/run/* 501 — supervisor belongs to standalone Studio
|
|
25
|
+
* * /__nwire/* 501 — not yet implemented
|
|
26
|
+
*
|
|
27
|
+
* `/__nwire/project`, `/__nwire/source`, `/__nwire/projects/*` are the
|
|
28
|
+
* multi-project shell routes. They are out of scope for the dev-host context
|
|
29
|
+
* (the host always serves one project — the one it loaded) and return 501.
|
|
30
|
+
*
|
|
31
|
+
* The wire's own `/_nwire/*` surface (inspect, events) is NOT owned here. The
|
|
32
|
+
* dev-host routes it to `wireHandlers[0]` via the hardcoded prefix check in
|
|
33
|
+
* dev-host.ts (a known tech-debt; see the review findings). Once that prefix
|
|
34
|
+
* check is removed, the wire's handler self-dispatches via Connect-style `next`.
|
|
35
|
+
*/
|
|
36
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
37
|
+
import type { DevHostMount } from "@nwire/endpoint";
|
|
38
|
+
/** Minimal runtime surface needed by this module. Duck-typed so the host never
|
|
39
|
+
* imports from `@nwire/runtime` directly — it reads whatever the app exposes. */
|
|
40
|
+
export interface RuntimeLike {
|
|
41
|
+
/** Subscribe to the telemetry stream. Returns an unsubscribe function. */
|
|
42
|
+
onTelemetry(listener: (record: unknown) => void): () => void;
|
|
43
|
+
}
|
|
44
|
+
/** Per-endpoint summary returned by `GET /__nwire/endpoints`. */
|
|
45
|
+
export interface EndpointInfo {
|
|
46
|
+
/** Endpoint name — from `endpoint("name")`. */
|
|
47
|
+
readonly name: string;
|
|
48
|
+
/** True when this is the currently active HTTP front. */
|
|
49
|
+
readonly active: boolean;
|
|
50
|
+
/** True when this endpoint registered at least one HTTP handler. */
|
|
51
|
+
readonly hasHttp: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface StudioHostApiOptions {
|
|
54
|
+
/**
|
|
55
|
+
* Live mounts from the endpoint collector. The API reads `mount.apps[0].runtime`
|
|
56
|
+
* (duck-typed as `RuntimeLike`) from whichever mount has a booted forge app.
|
|
57
|
+
* Safe to pass a live, mutable array — the handler reads it on each request.
|
|
58
|
+
*/
|
|
59
|
+
readonly mounts: readonly DevHostMount[];
|
|
60
|
+
/**
|
|
61
|
+
* Project root — where `.nwire/` lives. Defaults to `process.cwd()`.
|
|
62
|
+
*/
|
|
63
|
+
readonly cwd?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Return the name of the currently active HTTP front. Used by
|
|
66
|
+
* `GET /__nwire/endpoints` to mark the active entry. Optional — when
|
|
67
|
+
* absent, the API reports no endpoint as active.
|
|
68
|
+
*/
|
|
69
|
+
readonly getActiveEndpointName?: () => string | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Switch the active HTTP front. Called by `POST /__nwire/endpoints/active`.
|
|
72
|
+
* Should throw when the named endpoint is not found. Optional — when absent,
|
|
73
|
+
* the switch endpoint returns 501.
|
|
74
|
+
*/
|
|
75
|
+
readonly setActiveEndpoint?: (name: string) => void;
|
|
76
|
+
}
|
|
77
|
+
/** The handler signature: returns `true` when it handled the request (caller
|
|
78
|
+
* must not pass to the next handler), `false` to fall through. */
|
|
79
|
+
export type StudioHostApiHandler = (req: IncomingMessage, res: ServerResponse) => boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Create the `/__nwire/*` request handler for the one-Vite dev host.
|
|
82
|
+
*
|
|
83
|
+
* Returns `true` when it handled the request (caller stops). `false` means
|
|
84
|
+
* the request was not for `/__nwire/*` and should fall through to Vite's
|
|
85
|
+
* middlewares.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* const studioApi = createStudioHostApi({ mounts, cwd: opts.cwd });
|
|
90
|
+
*
|
|
91
|
+
* const server = createHttpServer((req, res) => {
|
|
92
|
+
* if (studioApi(req, res)) return;
|
|
93
|
+
* vite.middlewares(req, res, () => { ... });
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function createStudioHostApi(opts: StudioHostApiOptions): StudioHostApiHandler;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio data API for the one-Vite dev host.
|
|
3
|
+
*
|
|
4
|
+
* The standalone Studio process serves `/__nwire/*` by reading `.nwire/`
|
|
5
|
+
* from disk and proxying `/_nwire/*` to a separately-started wire process.
|
|
6
|
+
* The one-Vite host has the wire running IN-PROCESS, which is strictly better:
|
|
7
|
+
*
|
|
8
|
+
* - telemetry/live: subscribe `runtime.onTelemetry` directly — no disk writes
|
|
9
|
+
* required, records arrive at zero-latency with no file-tail polling loop.
|
|
10
|
+
* - manifest.json: serve the disk `.nwire/manifest.json` (built by `nwire cache`)
|
|
11
|
+
* and, when a live runtime is present, fold in the runtime topology so the
|
|
12
|
+
* manifest stays fresh even during HMR reloads without a full cache rebuild.
|
|
13
|
+
* - telemetry/runs: disk-based history — same as standalone Studio (these are
|
|
14
|
+
* past run files; there is no in-process equivalent).
|
|
15
|
+
*
|
|
16
|
+
* Routes owned here (all under `/__nwire/*`):
|
|
17
|
+
*
|
|
18
|
+
* GET /__nwire/manifest.json disk (.nwire/manifest.json) + runtime fold
|
|
19
|
+
* GET /__nwire/telemetry/live SSE — runtime.onTelemetry (runtime-direct)
|
|
20
|
+
* GET /__nwire/telemetry/runs disk list { runs: TelemetryRunMeta[] }
|
|
21
|
+
* GET /__nwire/telemetry/runs/:id disk single run { id, records: unknown[] }
|
|
22
|
+
* GET /__nwire/endpoints list { endpoints: EndpointInfo[] }
|
|
23
|
+
* POST /__nwire/endpoints/active { name } → switch active HTTP front
|
|
24
|
+
* * /__nwire/run/* 501 — supervisor belongs to standalone Studio
|
|
25
|
+
* * /__nwire/* 501 — not yet implemented
|
|
26
|
+
*
|
|
27
|
+
* `/__nwire/project`, `/__nwire/source`, `/__nwire/projects/*` are the
|
|
28
|
+
* multi-project shell routes. They are out of scope for the dev-host context
|
|
29
|
+
* (the host always serves one project — the one it loaded) and return 501.
|
|
30
|
+
*
|
|
31
|
+
* The wire's own `/_nwire/*` surface (inspect, events) is NOT owned here. The
|
|
32
|
+
* dev-host routes it to `wireHandlers[0]` via the hardcoded prefix check in
|
|
33
|
+
* dev-host.ts (a known tech-debt; see the review findings). Once that prefix
|
|
34
|
+
* check is removed, the wire's handler self-dispatches via Connect-style `next`.
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
37
|
+
import { resolve } from "node:path";
|
|
38
|
+
import { listTelemetryRuns, readTelemetryRun } from "@nwire/scan";
|
|
39
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
40
|
+
function json(res, status, body) {
|
|
41
|
+
res.statusCode = status;
|
|
42
|
+
res.setHeader("Content-Type", "application/json");
|
|
43
|
+
res.end(JSON.stringify(body));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Walk the mounts to find the first `RuntimeLike` available. Returns `undefined`
|
|
47
|
+
* when no forge runtime is mounted (transport-only endpoints without an app, or
|
|
48
|
+
* apps that haven't exposed `.runtime`).
|
|
49
|
+
*/
|
|
50
|
+
function findRuntime(mounts) {
|
|
51
|
+
for (const mount of mounts) {
|
|
52
|
+
for (const app of mount.apps) {
|
|
53
|
+
const runtime = app.runtime;
|
|
54
|
+
if (runtime !== null &&
|
|
55
|
+
runtime !== undefined &&
|
|
56
|
+
typeof runtime.onTelemetry === "function") {
|
|
57
|
+
return runtime;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
// ── SSE helpers ───────────────────────────────────────────────────────────────
|
|
64
|
+
const KEEPALIVE_MS = 25_000;
|
|
65
|
+
/** Write a raw SSE event frame. Returns false when the socket has closed. */
|
|
66
|
+
function sseWrite(res, event, data) {
|
|
67
|
+
try {
|
|
68
|
+
if (event !== null) {
|
|
69
|
+
res.write(`event: ${event}\ndata: ${data}\n\n`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
res.write(`data: ${data}\n\n`);
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ── Route handlers ───────────────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* GET /__nwire/manifest.json
|
|
83
|
+
*
|
|
84
|
+
* Serves the disk-built `.nwire/manifest.json`. When a live runtime is
|
|
85
|
+
* present the static file already reflects the last `nwire cache` run;
|
|
86
|
+
* the intent for future increments is to fold live handler registrations
|
|
87
|
+
* from the runtime so the manifest stays accurate during HMR. Today the
|
|
88
|
+
* disk file is served verbatim (the same as standalone Studio) — this is
|
|
89
|
+
* correct: `nwire dev` runs `nwire cache --if-stale` at boot.
|
|
90
|
+
*
|
|
91
|
+
* A `?force=true` query param is reserved for a future "rebuild" button
|
|
92
|
+
* trigger but is not acted on here (that requires spawning `nwire cache`).
|
|
93
|
+
*/
|
|
94
|
+
function handleManifest(req, res, cwd) {
|
|
95
|
+
const manifestPath = resolve(cwd, ".nwire", "manifest.json");
|
|
96
|
+
if (!existsSync(manifestPath)) {
|
|
97
|
+
json(res, 404, {
|
|
98
|
+
error: "manifest not built — run `nwire cache` or start with `nwire dev` which builds it automatically",
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = readFileSync(manifestPath, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
json(res, 500, { error: err.message });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
res.statusCode = 200;
|
|
111
|
+
res.setHeader("Content-Type", "application/json");
|
|
112
|
+
res.end(raw);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* GET /__nwire/telemetry/live (SSE)
|
|
116
|
+
*
|
|
117
|
+
* Runtime-direct: subscribes `runtime.onTelemetry` and streams each record
|
|
118
|
+
* as a plain SSE `data:` event. This replaces the file-tail approach used in
|
|
119
|
+
* standalone Studio — records arrive at zero-latency with no disk I/O.
|
|
120
|
+
*
|
|
121
|
+
* On open the server emits a `run` event carrying `null` (no discrete run ID
|
|
122
|
+
* in the in-process path — the runtime emits all records on one continuous
|
|
123
|
+
* stream). Studio's `useTelemetry` composable handles a null runId gracefully.
|
|
124
|
+
*
|
|
125
|
+
* When no runtime is available (transport-only endpoint), the stream stays
|
|
126
|
+
* open with keepalive comments — Studio will show an empty live view rather
|
|
127
|
+
* than an error.
|
|
128
|
+
*/
|
|
129
|
+
function handleTelemetryLive(req, res, mounts) {
|
|
130
|
+
res.statusCode = 200;
|
|
131
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
132
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
133
|
+
res.setHeader("Connection", "keep-alive");
|
|
134
|
+
// Signal which run the client is tailing. In dev-host mode there is no
|
|
135
|
+
// discrete run file — the stream is continuous — so we emit null.
|
|
136
|
+
sseWrite(res, "run", JSON.stringify(null));
|
|
137
|
+
const runtime = findRuntime(mounts);
|
|
138
|
+
let unsubscribe;
|
|
139
|
+
if (runtime) {
|
|
140
|
+
unsubscribe = runtime.onTelemetry((record) => {
|
|
141
|
+
sseWrite(res, null, JSON.stringify(record));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const keepalive = setInterval(() => {
|
|
145
|
+
try {
|
|
146
|
+
res.write(": keepalive\n\n");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
clearInterval(keepalive);
|
|
150
|
+
}
|
|
151
|
+
}, KEEPALIVE_MS);
|
|
152
|
+
// Don't let the keepalive timer hold the process open: an idle SSE
|
|
153
|
+
// connection must never be the reason Node (or a test runner) can't exit.
|
|
154
|
+
keepalive.unref();
|
|
155
|
+
req.on("close", () => {
|
|
156
|
+
unsubscribe?.();
|
|
157
|
+
clearInterval(keepalive);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* GET /__nwire/telemetry/runs
|
|
162
|
+
* GET /__nwire/telemetry/runs/:id
|
|
163
|
+
*
|
|
164
|
+
* Disk-based telemetry history — same as standalone Studio. History records
|
|
165
|
+
* come from `.nwire/telemetry/*.jsonl` files written by the telemetry reporter.
|
|
166
|
+
* The dev-host writes these too (the reporter is installed per-app at boot).
|
|
167
|
+
*
|
|
168
|
+
* The sub-path after the `/runs` prefix determines whether this is a list
|
|
169
|
+
* or a single-run fetch. In the Vite middleware model the mount prefix is
|
|
170
|
+
* stripped before `req.url` is set; in the raw Node http.Server model used
|
|
171
|
+
* by dev-host, `req.url` still carries the full path. We parse against the
|
|
172
|
+
* full path to be safe.
|
|
173
|
+
*/
|
|
174
|
+
function handleTelemetryRuns(req, res, cwd) {
|
|
175
|
+
const url = req.url ?? "/";
|
|
176
|
+
const pathname = new URL(url, "http://x").pathname;
|
|
177
|
+
// Exact list: /__nwire/telemetry/runs
|
|
178
|
+
if (pathname === "/__nwire/telemetry/runs" || pathname === "/__nwire/telemetry/runs/") {
|
|
179
|
+
json(res, 200, { runs: listTelemetryRuns(cwd) });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Single run: /__nwire/telemetry/runs/:id
|
|
183
|
+
const runIdMatch = /^\/__nwire\/telemetry\/runs\/([^/]+)$/.exec(pathname);
|
|
184
|
+
if (runIdMatch) {
|
|
185
|
+
const runId = decodeURIComponent(runIdMatch[1]);
|
|
186
|
+
const file = resolve(cwd, ".nwire", "telemetry", `${runId}.jsonl`);
|
|
187
|
+
if (!existsSync(file)) {
|
|
188
|
+
json(res, 404, { error: "run not found", id: runId });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const records = readTelemetryRun(cwd, runId);
|
|
192
|
+
json(res, 200, { id: runId, records });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
json(res, 404, { error: "not found" });
|
|
196
|
+
}
|
|
197
|
+
// ── Endpoint list + active-front switch ──────────────────────────────────────
|
|
198
|
+
/**
|
|
199
|
+
* GET /__nwire/endpoints
|
|
200
|
+
*
|
|
201
|
+
* Returns the list of collected endpoint mounts with their HTTP status and
|
|
202
|
+
* which one is currently active. The payload shape is intentionally minimal —
|
|
203
|
+
* Studio uses it only for the picker and does not need adapter-level detail.
|
|
204
|
+
*
|
|
205
|
+
* For a single-endpoint project the list has one entry that is always active;
|
|
206
|
+
* the picker renders as a no-op label rather than a selectable list.
|
|
207
|
+
*
|
|
208
|
+
* Response: `{ endpoints: EndpointInfo[] }`
|
|
209
|
+
*/
|
|
210
|
+
function handleEndpointsList(_req, res, mounts, getActive) {
|
|
211
|
+
const activeName = getActive();
|
|
212
|
+
const endpoints = mounts.map((m) => ({
|
|
213
|
+
name: m.name,
|
|
214
|
+
active: m.name === activeName,
|
|
215
|
+
hasHttp: m.handlers.length > 0,
|
|
216
|
+
}));
|
|
217
|
+
json(res, 200, { endpoints });
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* POST /__nwire/endpoints/active
|
|
221
|
+
*
|
|
222
|
+
* Body: `{ name: string }` — the endpoint to make the active HTTP front.
|
|
223
|
+
* Response: `{ name }` on success, `{ error }` on failure.
|
|
224
|
+
*
|
|
225
|
+
* Reads the JSON body (the server never gets large bodies here so we buffer
|
|
226
|
+
* synchronously via the request `data` event — no streaming needed).
|
|
227
|
+
*/
|
|
228
|
+
function handleSetActiveEndpoint(req, res, setter) {
|
|
229
|
+
let body = "";
|
|
230
|
+
req.on("data", (chunk) => {
|
|
231
|
+
body += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
232
|
+
});
|
|
233
|
+
req.on("end", () => {
|
|
234
|
+
let parsed;
|
|
235
|
+
try {
|
|
236
|
+
parsed = JSON.parse(body);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
json(res, 400, { error: "Invalid JSON body" });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const name = parsed !== null &&
|
|
243
|
+
typeof parsed === "object" &&
|
|
244
|
+
typeof parsed.name === "string"
|
|
245
|
+
? parsed.name
|
|
246
|
+
: undefined;
|
|
247
|
+
if (!name) {
|
|
248
|
+
json(res, 400, { error: 'Body must be { "name": "<endpoint-name>" }' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
setter(name);
|
|
253
|
+
json(res, 200, { name });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
json(res, 404, { error: err.message });
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
req.on("error", () => {
|
|
260
|
+
json(res, 400, { error: "Failed to read request body" });
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// ── Public factory ────────────────────────────────────────────────────────────
|
|
264
|
+
/**
|
|
265
|
+
* Create the `/__nwire/*` request handler for the one-Vite dev host.
|
|
266
|
+
*
|
|
267
|
+
* Returns `true` when it handled the request (caller stops). `false` means
|
|
268
|
+
* the request was not for `/__nwire/*` and should fall through to Vite's
|
|
269
|
+
* middlewares.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* const studioApi = createStudioHostApi({ mounts, cwd: opts.cwd });
|
|
274
|
+
*
|
|
275
|
+
* const server = createHttpServer((req, res) => {
|
|
276
|
+
* if (studioApi(req, res)) return;
|
|
277
|
+
* vite.middlewares(req, res, () => { ... });
|
|
278
|
+
* });
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function createStudioHostApi(opts) {
|
|
282
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
283
|
+
const getActive = opts.getActiveEndpointName ?? (() => undefined);
|
|
284
|
+
return function studioHostApi(req, res) {
|
|
285
|
+
const url = req.url ?? "/";
|
|
286
|
+
const pathname = new URL(url, "http://x").pathname;
|
|
287
|
+
if (!pathname.startsWith("/__nwire/"))
|
|
288
|
+
return false;
|
|
289
|
+
// ── manifest ───────────────────────────────────────────────────────────
|
|
290
|
+
if (req.method === "GET" && pathname === "/__nwire/manifest.json") {
|
|
291
|
+
handleManifest(req, res, cwd);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
// ── telemetry live (SSE) ───────────────────────────────────────────────
|
|
295
|
+
if (req.method === "GET" && pathname === "/__nwire/telemetry/live") {
|
|
296
|
+
handleTelemetryLive(req, res, opts.mounts);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
// ── telemetry runs (disk history) ──────────────────────────────────────
|
|
300
|
+
if (req.method === "GET" && pathname.startsWith("/__nwire/telemetry/runs")) {
|
|
301
|
+
handleTelemetryRuns(req, res, cwd);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
// ── endpoints list ─────────────────────────────────────────────────────
|
|
305
|
+
if (req.method === "GET" && pathname === "/__nwire/endpoints") {
|
|
306
|
+
handleEndpointsList(req, res, opts.mounts, getActive);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
// ── switch active HTTP front ───────────────────────────────────────────
|
|
310
|
+
if (req.method === "POST" && pathname === "/__nwire/endpoints/active") {
|
|
311
|
+
if (!opts.setActiveEndpoint) {
|
|
312
|
+
json(res, 501, {
|
|
313
|
+
error: "Active endpoint switching is not configured for this host. " +
|
|
314
|
+
"Pass `setActiveEndpoint` to createStudioHostApi to enable it.",
|
|
315
|
+
});
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
handleSetActiveEndpoint(req, res, opts.setActiveEndpoint);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
// ── run/* — supervisor is standalone-Studio only ───────────────────────
|
|
322
|
+
if (pathname.startsWith("/__nwire/run/")) {
|
|
323
|
+
json(res, 501, {
|
|
324
|
+
error: "/__nwire/run/* is not available in the one-Vite dev host. " +
|
|
325
|
+
"Process management runs in the standalone Studio (`nwire studio`).",
|
|
326
|
+
});
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
// ── everything else under /__nwire/* ──────────────────────────────────
|
|
330
|
+
json(res, 501, {
|
|
331
|
+
error: `Studio data API route not yet implemented in the one-Vite dev host: ${req.method} ${pathname}`,
|
|
332
|
+
hint: "Use `nwire studio` for the full Studio experience while this route is being ported.",
|
|
333
|
+
});
|
|
334
|
+
return true;
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton detection for `nwire studio`. Before spawning a second vite,
|
|
3
|
+
* probe the studio port: if a studio is already running there, register the
|
|
4
|
+
* current project with it and open the browser instead of starting a rival
|
|
5
|
+
* server. Keeps one studio owning the deterministic port (7777) so every
|
|
6
|
+
* later invocation attaches rather than scattering across 7778, 7779, …
|
|
7
|
+
*
|
|
8
|
+
* The decision logic is factored pure (`decideStudioProbe`) so it can be
|
|
9
|
+
* tested without a live server; the live HTTP is in `probeStudio` /
|
|
10
|
+
* `registerProject` and the platform opener in `openBrowser`.
|
|
11
|
+
*/
|
|
12
|
+
/** Default studio port — mirrors tool-studio's `vite.config.ts`. */
|
|
13
|
+
export declare const DEFAULT_STUDIO_PORT = 7777;
|
|
14
|
+
/** What a probe of `GET /__nwire/project` told us about the port. */
|
|
15
|
+
export type ProbeResult = {
|
|
16
|
+
kind: "free";
|
|
17
|
+
} | {
|
|
18
|
+
kind: "studio";
|
|
19
|
+
cwd: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "foreign";
|
|
23
|
+
};
|
|
24
|
+
/** What the studio command should do, given a probe result. */
|
|
25
|
+
export type StudioDecision = {
|
|
26
|
+
action: "attach";
|
|
27
|
+
cwd: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
} | {
|
|
30
|
+
action: "spawn";
|
|
31
|
+
} | {
|
|
32
|
+
action: "spawn-foreign";
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Pure mapping from a probe result to the action the command takes. Kept
|
|
36
|
+
* free of I/O so it is exhaustively unit-testable.
|
|
37
|
+
*/
|
|
38
|
+
export declare function decideStudioProbe(probe: ProbeResult): StudioDecision;
|
|
39
|
+
/**
|
|
40
|
+
* Interpret a `GET /__nwire/project` response body + status into a
|
|
41
|
+
* ProbeResult. A 200 with a JSON body carrying a `cwd` string is a studio;
|
|
42
|
+
* a 200 without that shape (or non-JSON) is a foreign service. Factored
|
|
43
|
+
* pure so the JSON-shape branch is testable without sockets.
|
|
44
|
+
*/
|
|
45
|
+
export declare function classifyProjectResponse(status: number, body: string): ProbeResult;
|
|
46
|
+
/** Base URL for a studio on `127.0.0.1:<port>`. */
|
|
47
|
+
export declare function studioBaseUrl(port: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Live probe of the studio port. Returns `free` on any connection error or
|
|
50
|
+
* timeout (port unbound or unresponsive), otherwise classifies the answer.
|
|
51
|
+
*/
|
|
52
|
+
export declare function probeStudio(port: number, timeoutMs?: number): Promise<ProbeResult>;
|
|
53
|
+
/** POST the current project's cwd to a running studio's register endpoint. */
|
|
54
|
+
export declare function registerProject(port: number, cwd: string): Promise<boolean>;
|
|
55
|
+
/** Open a URL in the platform's default browser. Best-effort, fire-and-forget. */
|
|
56
|
+
export declare function openBrowser(url: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the studio port for this invocation. Honors a `--port <n>` or
|
|
59
|
+
* `--port=<n>` flag in the raw args (vite's own flag), else the default.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveStudioPort(rawArgs: readonly string[]): number;
|
|
62
|
+
/** True when the raw args already pin a `--port` (so we shouldn't add one). */
|
|
63
|
+
export declare function hasExplicitPort(rawArgs: readonly string[]): boolean;
|