@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.
Files changed (131) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.js +1 -1
  3. package/dist/commands/cache.js +1 -1
  4. package/dist/commands/dev.d.ts +20 -7
  5. package/dist/commands/dev.js +142 -45
  6. package/dist/commands/ls.js +5 -3
  7. package/dist/commands/please.js +4 -2
  8. package/dist/commands/run.js +6 -2
  9. package/dist/commands/studio.js +74 -7
  10. package/dist/lib/dev-entry.d.ts +8 -9
  11. package/dist/lib/dev-entry.js +24 -25
  12. package/dist/lib/dev-host.d.ts +88 -0
  13. package/dist/lib/dev-host.js +426 -0
  14. package/dist/lib/ensure-built.d.ts +32 -0
  15. package/dist/lib/ensure-built.js +62 -0
  16. package/dist/lib/ensure-scan.d.ts +9 -4
  17. package/dist/lib/ensure-scan.js +46 -19
  18. package/dist/lib/studio-host-api.d.ts +97 -0
  19. package/dist/lib/studio-host-api.js +336 -0
  20. package/dist/lib/studio-probe.d.ts +63 -0
  21. package/dist/lib/studio-probe.js +132 -0
  22. package/dist/lib/vite-node.d.ts +7 -6
  23. package/dist/lib/vite-node.js +8 -8
  24. package/dist/lib/vite-run.d.ts +15 -0
  25. package/dist/lib/vite-run.js +33 -0
  26. package/dist/load-config.d.ts +10 -10
  27. package/dist/load-config.js +3 -3
  28. package/dist/studio/assets/abap-DVwoIrM0.js +1 -0
  29. package/dist/studio/assets/apex-B9XtvxSu.js +1 -0
  30. package/dist/studio/assets/azcli-D7JTNGKs.js +1 -0
  31. package/dist/studio/assets/bat-BNHAuPwR.js +1 -0
  32. package/dist/studio/assets/bicep-C3w6oSfK.js +2 -0
  33. package/dist/studio/assets/cameligo-DM9kSiq7.js +1 -0
  34. package/dist/studio/assets/clojure-FWLBUPxU.js +1 -0
  35. package/dist/studio/assets/codicon-ngg6Pgfi.ttf +0 -0
  36. package/dist/studio/assets/coffee-DCoMPIwW.js +1 -0
  37. package/dist/studio/assets/cpp-BNbIvdcw.js +1 -0
  38. package/dist/studio/assets/csharp-Dj4ULDZr.js +1 -0
  39. package/dist/studio/assets/csp-C-n5jZKF.js +1 -0
  40. package/dist/studio/assets/css-COIa8ZTR.js +3 -0
  41. package/dist/studio/assets/css.worker-CpJJqcA4.js +89 -0
  42. package/dist/studio/assets/cssMode-CFR5_xwk.js +1 -0
  43. package/dist/studio/assets/cypher-CW08XVUh.js +1 -0
  44. package/dist/studio/assets/dart-Bz550Pyv.js +1 -0
  45. package/dist/studio/assets/dockerfile-DW5REF8E.js +1 -0
  46. package/dist/studio/assets/ecl-BqdYhwmw.js +1 -0
  47. package/dist/studio/assets/editor-Br_kD0ds.css +1 -0
  48. package/dist/studio/assets/editor.api2-CTGEM8gT.js +872 -0
  49. package/dist/studio/assets/editor.main-sW1qgHFj.js +6 -0
  50. package/dist/studio/assets/elixir-Oi_9aIAu.js +1 -0
  51. package/dist/studio/assets/flow9-CIb9youF.js +1 -0
  52. package/dist/studio/assets/freemarker2-CPrni8hw.js +3 -0
  53. package/dist/studio/assets/fsharp-64tUaD-0.js +1 -0
  54. package/dist/studio/assets/go-DLKGL0rd.js +1 -0
  55. package/dist/studio/assets/graphql-Bz88xn3Q.js +1 -0
  56. package/dist/studio/assets/handlebars-DkvSNpQB.js +1 -0
  57. package/dist/studio/assets/hcl-Cq76tSVN.js +1 -0
  58. package/dist/studio/assets/html-ceN7ITxG.js +1 -0
  59. package/dist/studio/assets/html.worker-wsVgX3gp.js +502 -0
  60. package/dist/studio/assets/htmlMode-wduanCXn.js +1 -0
  61. package/dist/studio/assets/index-4tH0-1cA.js +41 -0
  62. package/dist/studio/assets/index-Fy3xDmV2.css +1 -0
  63. package/dist/studio/assets/ini-BTNe9zdh.js +1 -0
  64. package/dist/studio/assets/java-DzRJKRF3.js +1 -0
  65. package/dist/studio/assets/javascript-WF3LGLje.js +1 -0
  66. package/dist/studio/assets/json.worker-CcNiYOcv.js +58 -0
  67. package/dist/studio/assets/jsonMode-DSujY8tB.js +7 -0
  68. package/dist/studio/assets/julia-Bgv08lKa.js +1 -0
  69. package/dist/studio/assets/kotlin-Dzz8TWAt.js +1 -0
  70. package/dist/studio/assets/less-ak6GUtsl.js +2 -0
  71. package/dist/studio/assets/lexon-zuaObGAE.js +1 -0
  72. package/dist/studio/assets/liquid-C5Z7zFr3.js +1 -0
  73. package/dist/studio/assets/lspLanguageFeatures-B55yfFgf.js +4 -0
  74. package/dist/studio/assets/lua-ClKCZMmP.js +1 -0
  75. package/dist/studio/assets/m3-C7XHeDz_.js +1 -0
  76. package/dist/studio/assets/markdown-LT3qFBoR.js +1 -0
  77. package/dist/studio/assets/mdx-Bu5jRl19.js +1 -0
  78. package/dist/studio/assets/mips-B8clQ9KB.js +1 -0
  79. package/dist/studio/assets/monaco.contribution-KjQ4yOxj.js +2 -0
  80. package/dist/studio/assets/msdax-DBxc5qPJ.js +1 -0
  81. package/dist/studio/assets/mysql-qocW_xba.js +1 -0
  82. package/dist/studio/assets/objective-c-DhkpBlGX.js +1 -0
  83. package/dist/studio/assets/pascal-C_PJR40u.js +1 -0
  84. package/dist/studio/assets/pascaligo-BI_Gz9Bp.js +1 -0
  85. package/dist/studio/assets/perl-CIqGOHTo.js +1 -0
  86. package/dist/studio/assets/pgsql-DI_z9qfW.js +1 -0
  87. package/dist/studio/assets/php-Dkwn_yn0.js +1 -0
  88. package/dist/studio/assets/pla-DvzjACL6.js +1 -0
  89. package/dist/studio/assets/postiats-Cc9-hkUx.js +1 -0
  90. package/dist/studio/assets/powerquery-IGzsITFg.js +1 -0
  91. package/dist/studio/assets/powershell-BHlZlUN6.js +1 -0
  92. package/dist/studio/assets/protobuf-pGrmMUz5.js +2 -0
  93. package/dist/studio/assets/pug-B4eH693Y.js +1 -0
  94. package/dist/studio/assets/python-DpEFuk0I.js +1 -0
  95. package/dist/studio/assets/qsharp-CwO3kTIU.js +1 -0
  96. package/dist/studio/assets/r-CiZUpdIa.js +1 -0
  97. package/dist/studio/assets/razor-BF1svRn9.js +1 -0
  98. package/dist/studio/assets/redis-DjdIzLdf.js +1 -0
  99. package/dist/studio/assets/redshift-vCL5QMyw.js +1 -0
  100. package/dist/studio/assets/restructuredtext-D5hoMHB1.js +1 -0
  101. package/dist/studio/assets/ruby-ByPQrqP4.js +1 -0
  102. package/dist/studio/assets/rust-Nz5wukP7.js +1 -0
  103. package/dist/studio/assets/sb-DgR1RbMJ.js +1 -0
  104. package/dist/studio/assets/scala-BCgNuXrV.js +1 -0
  105. package/dist/studio/assets/scheme-TgKpKGpb.js +1 -0
  106. package/dist/studio/assets/scss-BKxAkvnT.js +3 -0
  107. package/dist/studio/assets/shell-COgstXIQ.js +1 -0
  108. package/dist/studio/assets/solidity-DaqmtBSV.js +1 -0
  109. package/dist/studio/assets/sophia-Da67i9pL.js +1 -0
  110. package/dist/studio/assets/sparql-CtN8jEDV.js +1 -0
  111. package/dist/studio/assets/sql-BnJfQHXt.js +1 -0
  112. package/dist/studio/assets/st-cGKU4FoP.js +1 -0
  113. package/dist/studio/assets/swift-DyyME8zA.js +1 -0
  114. package/dist/studio/assets/systemverilog-BYZY5TEG.js +1 -0
  115. package/dist/studio/assets/tcl-CHv1_zaR.js +1 -0
  116. package/dist/studio/assets/ts.worker-DfMAw22J.js +67719 -0
  117. package/dist/studio/assets/tsMode-BiiF1JOM.js +11 -0
  118. package/dist/studio/assets/twig-CNwULq4h.js +1 -0
  119. package/dist/studio/assets/typescript-BefpzegH.js +1 -0
  120. package/dist/studio/assets/typespec-B3KUNs_P.js +1 -0
  121. package/dist/studio/assets/vb-phZZ2pCs.js +1 -0
  122. package/dist/studio/assets/wgsl-Df-y4I4e.js +298 -0
  123. package/dist/studio/assets/workers-DqAl3RFu.js +1 -0
  124. package/dist/studio/assets/xml-CWw7bbeo.js +1 -0
  125. package/dist/studio/assets/yaml-CZ0k9DUm.js +1 -0
  126. package/dist/studio/index.html +13 -0
  127. package/dist/wire-runner.d.ts +12 -0
  128. package/dist/wire-runner.js +19 -0
  129. package/package.json +8 -6
  130. package/dist/cache-runner.d.ts +0 -1
  131. package/dist/cache-runner.js +0 -206
package/dist/cli.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * `nwire` — the umbrella CLI. Built on citty: one root command, one
4
4
  * subcommand per file under `commands/`. The CLI is intentionally thin —
5
5
  * every command delegates to a small set of execution helpers
6
- * (`viteNode`, `spawnInteractive`) and, where needed, an in-tree
6
+ * (`viteRun`, `spawnInteractive`) and, where needed, an in-tree
7
7
  * RunnerSupervisor for long-lived processes.
8
8
  *
9
9
  * Adding a new command:
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * `nwire` — the umbrella CLI. Built on citty: one root command, one
4
4
  * subcommand per file under `commands/`. The CLI is intentionally thin —
5
5
  * every command delegates to a small set of execution helpers
6
- * (`viteNode`, `spawnInteractive`) and, where needed, an in-tree
6
+ * (`viteRun`, `spawnInteractive`) and, where needed, an in-tree
7
7
  * RunnerSupervisor for long-lived processes.
8
8
  *
9
9
  * Adding a new command:
@@ -30,7 +30,7 @@ export const cacheCommand = defineCommand({
30
30
  },
31
31
  async run({ args }) {
32
32
  const force = !args["if-stale"];
33
- const result = ensureScanFresh(process.cwd(), { force, quiet: args.quiet });
33
+ const result = await ensureScanFresh(process.cwd(), { force, quiet: args.quiet });
34
34
  // exit 0 when the cache is current — whether we just rebuilt it or
35
35
  // the cheap-path skipped. exit 1 only when we tried to rebuild and
36
36
  // the builder failed.
@@ -1,10 +1,23 @@
1
1
  /**
2
- * `nwire dev` — boots the project's wire under vite-node --watch with
3
- * stdio passthrough. Prints a branded banner before spawning so the
4
- * user sees what's about to run + the project context; the child's
5
- * stdout/stderr flow through unmodified afterwards.
2
+ * `nwire dev` — boots the project's wire + Studio on one port via the
3
+ * in-process Vite dev host. Prints a branded banner, then hands off to
4
+ * `startDevHost` which serves both surfaces without a child process.
6
5
  *
7
- * Also writes a state file under `.nwire/processes/` so `nwire ps`
8
- * sees the dev session from any other shell.
6
+ * For projects that declare a `scripts.dev` fallback instead of a recognized
7
+ * wire entry, the old child-spawn path is preserved so nothing breaks.
8
+ *
9
+ * Also writes a state file under `.nwire/processes/` so `nwire ps` sees
10
+ * the dev session from any other shell.
9
11
  */
10
- export declare const devCommand: import("citty").CommandDef<import("citty").ArgsDef>;
12
+ export declare const devCommand: import("citty").CommandDef<{
13
+ endpoint: {
14
+ type: "string";
15
+ description: string;
16
+ required: false;
17
+ };
18
+ port: {
19
+ type: "string";
20
+ description: string;
21
+ required: false;
22
+ };
23
+ }>;
@@ -1,36 +1,68 @@
1
1
  /**
2
- * `nwire dev` — boots the project's wire under vite-node --watch with
3
- * stdio passthrough. Prints a branded banner before spawning so the
4
- * user sees what's about to run + the project context; the child's
5
- * stdout/stderr flow through unmodified afterwards.
2
+ * `nwire dev` — boots the project's wire + Studio on one port via the
3
+ * in-process Vite dev host. Prints a branded banner, then hands off to
4
+ * `startDevHost` which serves both surfaces without a child process.
6
5
  *
7
- * Also writes a state file under `.nwire/processes/` so `nwire ps`
8
- * sees the dev session from any other shell.
6
+ * For projects that declare a `scripts.dev` fallback instead of a recognized
7
+ * wire entry, the old child-spawn path is preserved so nothing breaks.
8
+ *
9
+ * Also writes a state file under `.nwire/processes/` so `nwire ps` sees
10
+ * the dev session from any other shell.
9
11
  */
10
12
  import { defineCommand } from "citty";
11
- import { appendFileSync, closeSync, openSync } from "node:fs";
12
- import { spawn } from "node:child_process";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { palette } from "../lib/colors.js";
15
- /** Windows toggle — `shell:true` so `pnpm.cmd` resolves on spawn. */
16
- const IS_WIN = process.platform === "win32";
17
15
  import { detectProject } from "../lib/project.js";
16
+ import { ensureWorkspaceBuilt } from "../lib/ensure-built.js";
18
17
  import { resolveDevEntry, devEntryCandidates } from "../lib/dev-entry.js";
18
+ import { startDevHost } from "../lib/dev-host.js";
19
19
  import { ensureDir as ensureProcDir, logPath as procLogPath, removeRecord, writeRecord, } from "../lib/process-state.js";
20
+ // ── npm-script fallback (child spawn) ───────────────────────────────────────
21
+ // Only used when resolved.kind === "npm-script". Kept self-contained so the
22
+ // host path above has no child-process machinery at all.
23
+ import { appendFileSync, closeSync, openSync } from "node:fs";
24
+ import { spawn } from "node:child_process";
25
+ /** Windows toggle — `shell:true` so `pnpm.cmd` resolves on spawn. */
26
+ const IS_WIN = process.platform === "win32";
27
+ const DEFAULT_PORT = 4000;
28
+ /**
29
+ * Resolve the host port: explicit `--port` wins, then `$PORT`, then the
30
+ * default. A non-numeric or out-of-range value is ignored (falls through to
31
+ * the next source) so a typo can't silently bind an ephemeral port.
32
+ */
33
+ function resolvePort(flag) {
34
+ for (const candidate of [flag, process.env.PORT]) {
35
+ if (candidate === undefined || candidate === "")
36
+ continue;
37
+ const n = Number(candidate);
38
+ if (Number.isInteger(n) && n >= 0 && n <= 65535)
39
+ return n;
40
+ }
41
+ return DEFAULT_PORT;
42
+ }
20
43
  export const devCommand = defineCommand({
21
44
  meta: {
22
45
  name: "dev",
23
- description: "Boot the dev-all topology with watch mode",
46
+ description: "Boot the project wire with watch mode",
24
47
  },
25
- async run() {
48
+ args: {
49
+ endpoint: {
50
+ type: "string",
51
+ description: "Name of the endpoint to front on the HTTP surface (multi-endpoint projects).",
52
+ required: false,
53
+ },
54
+ port: {
55
+ type: "string",
56
+ description: `Port for the wire + Studio host. Falls back to $PORT, then ${DEFAULT_PORT}.`,
57
+ required: false,
58
+ },
59
+ },
60
+ async run({ args }) {
26
61
  const cwd = process.cwd();
62
+ // Monorepo only: keep workspace `dist/` fresh so a stale `@nwire/*`
63
+ // doesn't crash the example wire. No-op in consumer apps.
64
+ ensureWorkspaceBuilt(cwd);
27
65
  const project = detectProject(cwd);
28
- // Resolution covers every recognized project shape:
29
- // 1. apps/dev-all/run.ts (multi-wire)
30
- // 2. apps/<single>/{run,main}.ts
31
- // 3. app/{main,run,index}.ts (singular small-example shape)
32
- // 4. package.json scripts.dev (last resort — honors whatever the
33
- // consumer scripts already encode, e.g. vite-node app/main.ts)
34
66
  const resolved = resolveDevEntry(cwd);
35
67
  if (!resolved) {
36
68
  // eslint-disable-next-line no-console
@@ -39,12 +71,95 @@ export const devCommand = defineCommand({
39
71
  ` looked for: ${devEntryCandidates().join(", ")}`);
40
72
  process.exit(1);
41
73
  }
42
- const subtitle = resolved.kind === "vite-node" ? resolved.label : `pnpm run ${resolved.script}`;
43
- const entryRel = resolved.kind === "vite-node" ? resolved.rel : `package.json scripts.${resolved.script}`;
44
- // ── Banner ──────────────────────────────────────────────────────
45
- // Single block before stdio passthrough takes over. The child's
46
- // boot logs (runApp's "ready" banner, ports, etc.) flow through
47
- // immediately after.
74
+ if (resolved.kind === "wire") {
75
+ // ── In-process one-Vite host ─────────────────────────────────────
76
+ // Port precedence: --port flag → $PORT env → default. Invalid values
77
+ // fall through to the next source so a typo never silently binds 0.
78
+ const port = resolvePort(args.port);
79
+ const id = randomUUID();
80
+ const startedAt = new Date().toISOString();
81
+ // eslint-disable-next-line no-console
82
+ console.log([
83
+ "",
84
+ ` ${palette.brand(palette.bold("Nwire dev"))} ${palette.dim(project.name)}`,
85
+ ` ${palette.dim("entry ")} ${resolved.rel}`,
86
+ ` ${palette.dim("wire ")} ${resolved.label}`,
87
+ ` ${palette.dim("host ")} http://127.0.0.1:${port}`,
88
+ ` ${palette.dim("mode ")} ${palette.accent("dev-host")}`,
89
+ ` ${palette.dim("Ctrl+C ")} stop`,
90
+ "",
91
+ ].join("\n"));
92
+ ensureProcDir(cwd);
93
+ let currentStatus = "starting";
94
+ const persistRecord = (p) => {
95
+ const record = {
96
+ id,
97
+ name: `dev:${resolved.label}`,
98
+ pid: process.pid,
99
+ status: currentStatus,
100
+ port: p ?? port,
101
+ startedAt,
102
+ cwd,
103
+ command: `nwire dev-host ${resolved.rel}`,
104
+ logPath: procLogPath(cwd, id),
105
+ lastUpdated: new Date().toISOString(),
106
+ };
107
+ writeRecord(cwd, record);
108
+ };
109
+ persistRecord();
110
+ let host;
111
+ let stopping = false;
112
+ const stop = async () => {
113
+ if (stopping)
114
+ return;
115
+ stopping = true;
116
+ currentStatus = "stopping";
117
+ persistRecord();
118
+ if (host) {
119
+ try {
120
+ await host.close();
121
+ }
122
+ catch {
123
+ // best-effort
124
+ }
125
+ }
126
+ removeRecord(cwd, id);
127
+ // eslint-disable-next-line no-console
128
+ console.log(palette.dim("\nDev stopped."));
129
+ process.exit(0);
130
+ };
131
+ process.on("SIGINT", () => void stop());
132
+ process.on("SIGTERM", () => void stop());
133
+ try {
134
+ host = await startDevHost({
135
+ cwd,
136
+ entry: resolved.entry,
137
+ port,
138
+ activeEndpoint: args.endpoint || undefined,
139
+ });
140
+ currentStatus = "running";
141
+ persistRecord(port);
142
+ // eslint-disable-next-line no-console
143
+ console.log(` ${palette.ok("ready")} wire + Studio on http://127.0.0.1:${port}\n`);
144
+ // Park the process — the http.Server keeps the event loop alive.
145
+ await new Promise(() => { });
146
+ }
147
+ catch (err) {
148
+ // eslint-disable-next-line no-console
149
+ console.error(palette.err("nwire dev: host failed to start —"), err);
150
+ currentStatus = "crashed";
151
+ persistRecord();
152
+ removeRecord(cwd, id);
153
+ process.exit(1);
154
+ }
155
+ return;
156
+ }
157
+ // ── npm-script fallback (child spawn) ────────────────────────────────
158
+ // `resolved.kind === "npm-script"`: the project has no recognized wire
159
+ // entry but declares a `scripts.dev`. Honor it via spawn so `nwire dev`
160
+ // still works on those projects without any behavior change.
161
+ const subtitle = `pnpm run ${resolved.script}`;
162
+ const entryRel = `package.json scripts.${resolved.script}`;
48
163
  // eslint-disable-next-line no-console
49
164
  console.log([
50
165
  "",
@@ -55,22 +170,11 @@ export const devCommand = defineCommand({
55
170
  ` ${palette.dim("Ctrl+C ")} stop`,
56
171
  "",
57
172
  ].join("\n"));
58
- // ── Spawn child with stdio inherited ────────────────────────────
59
- // Inherit means logs go straight to the user's terminal — no ink
60
- // capture, no log buffering. The trade-off: `nwire logs <id>`
61
- // can't tail the live stream (only what we tee to disk). For dev
62
- // sessions that's fine — the user is watching the terminal.
63
- const spawnCmd = resolved.kind === "vite-node"
64
- ? ["pnpm", ["exec", "vite-node", "--watch", resolved.entry]]
65
- : ["pnpm", ["run", resolved.script]];
66
- const child = spawn(spawnCmd[0], spawnCmd[1], {
173
+ const child = spawn("pnpm", ["run", resolved.script], {
67
174
  stdio: ["inherit", "pipe", "pipe"],
68
175
  cwd,
69
176
  shell: IS_WIN,
70
177
  });
71
- // Tee child stdio through to BOTH the user's terminal AND the
72
- // per-process log file so `nwire logs <id>` works from another
73
- // shell. Direct passthrough to stdout to keep ordering intact.
74
178
  const id = randomUUID();
75
179
  const startedAt = new Date().toISOString();
76
180
  ensureProcDir(cwd);
@@ -81,15 +185,13 @@ export const devCommand = defineCommand({
81
185
  const persistRecord = () => {
82
186
  const record = {
83
187
  id,
84
- name: `dev:${subtitle}`,
188
+ name: `dev:${resolved.script}`,
85
189
  pid: child.pid ?? -1,
86
190
  status: currentStatus,
87
191
  port,
88
192
  startedAt,
89
193
  cwd,
90
- command: resolved.kind === "vite-node"
91
- ? `vite-node --watch ${resolved.rel}`
92
- : `pnpm run ${resolved.script} (${resolved.command})`,
194
+ command: `pnpm run ${resolved.script} (${resolved.command})`,
93
195
  logPath: logFile,
94
196
  lastUpdated: new Date().toISOString(),
95
197
  };
@@ -107,7 +209,6 @@ export const devCommand = defineCommand({
107
209
  }
108
210
  out.write(text);
109
211
  if (currentStatus !== "running" && READY_RE.test(text)) {
110
- // Try to capture the port from "Local: http://...:<port>"
111
212
  const match = /:(\d+)\b/.exec(text);
112
213
  if (match)
113
214
  port = Number(match[1]);
@@ -117,7 +218,6 @@ export const devCommand = defineCommand({
117
218
  };
118
219
  child.stdout?.on("data", handleStream(process.stdout));
119
220
  child.stderr?.on("data", handleStream(process.stderr));
120
- // ── Shutdown ────────────────────────────────────────────────────
121
221
  let stopping = false;
122
222
  const stop = (signal) => {
123
223
  if (stopping)
@@ -134,9 +234,6 @@ export const devCommand = defineCommand({
134
234
  setTimeout(() => {
135
235
  if (child.exitCode === null && child.signalCode === null) {
136
236
  try {
137
- // Windows has no signal mechanism — SIGTERM and SIGKILL
138
- // collapse to the same TerminateProcess call. Skip the
139
- // explicit SIGKILL and let Node use the platform default.
140
237
  if (IS_WIN)
141
238
  child.kill();
142
239
  else
@@ -8,7 +8,7 @@ import { existsSync } from "node:fs";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { palette } from "../lib/colors.js";
10
10
  import { ensureScanFresh } from "../lib/ensure-scan.js";
11
- import { viteNode } from "../lib/vite-node.js";
11
+ import { execSync } from "../lib/exec.js";
12
12
  const here = dirname(fileURLToPath(import.meta.url));
13
13
  export const lsCommand = defineCommand({
14
14
  meta: {
@@ -16,13 +16,15 @@ export const lsCommand = defineCommand({
16
16
  description: "List discovered wires + actions",
17
17
  },
18
18
  async run({ rawArgs }) {
19
- ensureScanFresh(process.cwd(), { quiet: true });
19
+ await ensureScanFresh(process.cwd(), { quiet: true });
20
20
  const builderPath = resolve(here, "..", "ls-runner.js");
21
21
  if (!existsSync(builderPath)) {
22
22
  // eslint-disable-next-line no-console
23
23
  console.error(palette.err("nwire ls:") + ` ls runner missing at ${builderPath}`);
24
24
  process.exit(1);
25
25
  }
26
- process.exit(viteNode(builderPath, rawArgs));
26
+ // ls-runner is plain built JS that reads `.nwire/manifest.json` — plain node,
27
+ // no Vite needed.
28
+ process.exit(execSync("node", [builderPath, ...rawArgs]));
27
29
  },
28
30
  });
@@ -7,7 +7,7 @@ import { defineCommand } from "citty";
7
7
  import { resolve } from "node:path";
8
8
  import { existsSync, readdirSync } from "node:fs";
9
9
  import { palette } from "../lib/colors.js";
10
- import { viteNode } from "../lib/vite-node.js";
10
+ import { viteRun } from "../lib/vite-run.js";
11
11
  function findPleaseEntry(cwd) {
12
12
  const appsDir = resolve(cwd, "apps");
13
13
  if (!existsSync(appsDir))
@@ -40,6 +40,8 @@ export const pleaseCommand = defineCommand({
40
40
  console.error(palette.err("nwire please:") + " no apps/*/run.please.ts found in this project");
41
41
  process.exit(1);
42
42
  }
43
- process.exit(viteNode(pleasePath, rawArgs));
43
+ // The please wire runs in-process; it exits with its own code, or we exit 0.
44
+ await viteRun(pleasePath, rawArgs);
45
+ process.exit(0);
44
46
  },
45
47
  });
@@ -10,10 +10,12 @@
10
10
  * difference is `dev` adds `--watch` and reloads on change.
11
11
  */
12
12
  import { defineCommand } from "citty";
13
- import { basename, relative, resolve } from "node:path";
13
+ import { basename, relative, resolve, dirname } from "node:path";
14
+ import { fileURLToPath } from "node:url";
14
15
  import { existsSync } from "node:fs";
15
16
  import { spawn } from "node:child_process";
16
17
  import { palette } from "../lib/colors.js";
18
+ const here = dirname(fileURLToPath(import.meta.url));
17
19
  /** Windows toggle — `shell:true` so `.cmd` shims like `pnpm` resolve. */
18
20
  const IS_WIN = process.platform === "win32";
19
21
  export const runCommand = defineCommand({
@@ -68,7 +70,9 @@ export const runCommand = defineCommand({
68
70
  // The child writes directly to the user's terminal. runApp() emits
69
71
  // its own banner (ports, docs, openapi paths) so the user gets
70
72
  // immediate feedback once the wire is listening.
71
- const child = spawn("pnpm", ["exec", "vite-node", entry, ...passthrough], {
73
+ // Load the wire via the Vite-backed wire-runner (no vite-node).
74
+ const wireRunner = resolve(here, "..", "wire-runner.js");
75
+ const child = spawn("node", [wireRunner, entry, ...passthrough], {
72
76
  stdio: "inherit",
73
77
  cwd,
74
78
  shell: IS_WIN,
@@ -7,19 +7,66 @@
7
7
  */
8
8
  import { defineCommand } from "citty";
9
9
  import { createRequire } from "node:module";
10
- import { resolve, dirname } from "node:path";
11
- import { existsSync } from "node:fs";
10
+ import { resolve, dirname, basename } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { existsSync, readFileSync } from "node:fs";
12
13
  import { palette } from "../lib/colors.js";
13
14
  import { spawnInteractive } from "../lib/exec.js";
15
+ import { ensureWorkspaceBuilt } from "../lib/ensure-built.js";
16
+ import { decideStudioProbe, hasExplicitPort, openBrowser, probeStudio, registerProject, resolveStudioPort, studioBaseUrl, } from "../lib/studio-probe.js";
17
+ /** A project's display name — its `package.json` `name`, else the dir name. */
18
+ function projectName(cwd) {
19
+ try {
20
+ const pj = resolve(cwd, "package.json");
21
+ if (existsSync(pj)) {
22
+ const parsed = JSON.parse(readFileSync(pj, "utf8"));
23
+ if (parsed.name)
24
+ return parsed.name;
25
+ }
26
+ }
27
+ catch {
28
+ // fall through to the directory name
29
+ }
30
+ return basename(cwd);
31
+ }
14
32
  export const studioCommand = defineCommand({
15
33
  meta: {
16
34
  name: "studio",
17
35
  description: "Boot Nwire Studio (Vue + Vite UI)",
18
36
  },
19
37
  async run({ rawArgs }) {
20
- // Try resolving @nwire/studio from the consumer's cwd first (so a
21
- // dev-deps install in their project works), then from the CLI's own
22
- // location (covers the framework workspace + `pnpm dlx` flows).
38
+ // ── Singleton: attach to an already-running studio ───────────────
39
+ // Probe the studio port. If a studio is live there, register this
40
+ // project with it, open the browser, and exit 0 never bind a second
41
+ // server. Only on the spawn path do we ensure the workspace is built.
42
+ const port = resolveStudioPort(rawArgs);
43
+ const cwd = process.cwd();
44
+ const decision = decideStudioProbe(await probeStudio(port));
45
+ if (decision.action === "attach") {
46
+ await registerProject(port, cwd);
47
+ // Name the project we just registered (this cwd), not the host studio.
48
+ const label = projectName(cwd);
49
+ // eslint-disable-next-line no-console
50
+ console.log(palette.brand("nwire studio:") +
51
+ ` already running on :${port} — registered ${palette.bold(label)}, opening…`);
52
+ openBrowser(`${studioBaseUrl(port)}/projects`);
53
+ process.exit(0);
54
+ }
55
+ if (decision.action === "spawn-foreign") {
56
+ // eslint-disable-next-line no-console
57
+ console.log(palette.warn("nwire studio:") +
58
+ ` port ${port} is taken by another service — letting vite pick a free port.`);
59
+ }
60
+ // Monorepo only: keep workspace `dist/` fresh before booting so
61
+ // examples don't crash on stale `@nwire/*`. No-op in consumer apps.
62
+ ensureWorkspaceBuilt(cwd);
63
+ // Resolve the Studio source dir so we can find its vite binary.
64
+ //
65
+ // Resolution order:
66
+ // 1. Bundled inside the CLI itself (dist/studio ships the prebuilt SPA but
67
+ // we need the *source* dir for vite — fall through to node_modules paths).
68
+ // 2. Consumer's node_modules (explicit devDep install, dev-hacking workflow).
69
+ // 3. CLI's own node_modules (framework workspace + `pnpm dlx` flows).
23
70
  let studioDir;
24
71
  const tryResolveFrom = (basePath) => {
25
72
  try {
@@ -33,6 +80,18 @@ export const studioCommand = defineCommand({
33
80
  studioDir =
34
81
  tryResolveFrom(resolve(process.cwd(), "package.json")) ?? tryResolveFrom(import.meta.url);
35
82
  if (!studioDir) {
83
+ // @nwire/studio not reachable as a package — check whether the bundled
84
+ // prebuilt SPA is present (the normal consumer path). When it is, the
85
+ // `nwire dev` host already serves Studio at `/`; `nwire studio` (the
86
+ // standalone Vite dev server) is only needed in the framework workspace.
87
+ const bundledDist = resolve(dirname(fileURLToPath(import.meta.url)), "..", "studio");
88
+ if (existsSync(bundledDist)) {
89
+ // eslint-disable-next-line no-console
90
+ console.log(palette.brand("nwire studio:") +
91
+ " Studio is served by `nwire dev` at http://localhost:<port>/. " +
92
+ "Run `nwire dev` to see the live Studio UI.");
93
+ process.exit(0);
94
+ }
36
95
  // eslint-disable-next-line no-console
37
96
  console.error(palette.err("nwire studio:") +
38
97
  " @nwire/studio not installed. `pnpm add -D @nwire/studio` first.");
@@ -51,9 +110,17 @@ export const studioCommand = defineCommand({
51
110
  ` vite binary missing at ${viteBin} — run \`pnpm install\` in ${studioDir}`);
52
111
  process.exit(1);
53
112
  }
54
- const { done } = spawnInteractive(viteBin, rawArgs, {
113
+ // Deterministic port: on the normal (free-port) spawn path, pin the
114
+ // studio to `port` with `--strictPort` so it OWNS that port and the
115
+ // next `nwire studio` invocation finds + attaches to it rather than
116
+ // scattering onto 7778, 7779, … . Skip when the user already pinned a
117
+ // `--port`, or on the foreign-port fallback (let vite pick a free one).
118
+ const spawnArgs = decision.action === "spawn" && !hasExplicitPort(rawArgs)
119
+ ? [...rawArgs, "--port", String(port), "--strictPort"]
120
+ : rawArgs;
121
+ const { done } = spawnInteractive(viteBin, spawnArgs, {
55
122
  cwd: studioDir,
56
- env: { NWIRE_CWD: process.cwd() },
123
+ env: { NWIRE_CWD: cwd },
57
124
  stoppedMessage: "Studio stopped.",
58
125
  });
59
126
  process.exit(await done);
@@ -1,25 +1,24 @@
1
1
  /**
2
2
  * `resolveDevEntry` — figure out how to boot the project's dev server.
3
3
  *
4
- * Nwire projects come in three shapes, and `nwire dev` needs to work on all
5
- * of them out of the box (no `nwire.config.ts` required):
4
+ * Nwire projects come in two shapes, and `nwire dev` works on both out
5
+ * of the box (no `nwire.config.ts` required):
6
6
  *
7
- * 1. Multi-wire monorepo style — `apps/dev-all/run.ts` runs every app.
8
- * 2. Single wire under `apps/` — `apps/<name>/{run,main}.ts`.
9
- * 3. Single-app style `app/{main,run,index}.ts` (the small-example shape
10
- * used by hello-world / todo-app / moderation-queue).
11
- * 4. Fallback — `package.json` exposes a `scripts.dev` entry; we honor it.
7
+ * 1. Single wire under `apps/` — `apps/<name>/{run,main}.ts`.
8
+ * 2. Single-app style — `app/{main,run,index}.ts` (the small-example
9
+ * shape used by hello-world / todo-app / moderation-queue).
10
+ * 3. Fallback `package.json` exposes a `scripts.dev` entry; we honor it.
12
11
  *
13
12
  * Returns a discriminated value the caller can spawn directly. The CLI prints
14
13
  * a banner before handing off, so the kind + label flow through to the user.
15
14
  */
16
15
  export type DevEntry = {
17
- readonly kind: "vite-node";
16
+ readonly kind: "wire";
18
17
  /** Absolute path to the .ts entry file. */
19
18
  readonly entry: string;
20
19
  /** Project-relative path of `entry`, for the banner. */
21
20
  readonly rel: string;
22
- /** Subtitle shown in the banner: dev-all | <wire-name> | app. */
21
+ /** Subtitle shown in the banner: <wire-name> | app. */
23
22
  readonly label: string;
24
23
  } | {
25
24
  readonly kind: "npm-script";
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * `resolveDevEntry` — figure out how to boot the project's dev server.
3
3
  *
4
- * Nwire projects come in three shapes, and `nwire dev` needs to work on all
5
- * of them out of the box (no `nwire.config.ts` required):
4
+ * Nwire projects come in two shapes, and `nwire dev` works on both out
5
+ * of the box (no `nwire.config.ts` required):
6
6
  *
7
- * 1. Multi-wire monorepo style — `apps/dev-all/run.ts` runs every app.
8
- * 2. Single wire under `apps/` — `apps/<name>/{run,main}.ts`.
9
- * 3. Single-app style `app/{main,run,index}.ts` (the small-example shape
10
- * used by hello-world / todo-app / moderation-queue).
11
- * 4. Fallback — `package.json` exposes a `scripts.dev` entry; we honor it.
7
+ * 1. Single wire under `apps/` — `apps/<name>/{run,main}.ts`.
8
+ * 2. Single-app style — `app/{main,run,index}.ts` (the small-example
9
+ * shape used by hello-world / todo-app / moderation-queue).
10
+ * 3. Fallback `package.json` exposes a `scripts.dev` entry; we honor it.
12
11
  *
13
12
  * Returns a discriminated value the caller can spawn directly. The CLI prints
14
13
  * a banner before handing off, so the kind + label flow through to the user.
@@ -21,17 +20,9 @@ import { relative, resolve } from "node:path";
21
20
  * with a clear message listing what was tried.
22
21
  */
23
22
  export function resolveDevEntry(cwd) {
24
- // 1. dev-all (multi-wire dev topology)
25
- const devAll = resolve(cwd, "apps/dev-all/run.ts");
26
- if (existsSync(devAll)) {
27
- return { kind: "vite-node", entry: devAll, rel: relPath(cwd, devAll), label: "dev-all" };
28
- }
29
- // 2. Single wire under apps/ — accept run.ts OR main.ts.
23
+ // 1. Single wire under apps/ — accept run.ts OR main.ts.
30
24
  const appsDir = resolve(cwd, "apps");
31
25
  if (existsSync(appsDir)) {
32
- // Cheap enumeration — projects rarely have more than a few entries here.
33
- // We don't need detectProject (avoids the import cycle and keeps this
34
- // helper focused).
35
26
  try {
36
27
  const wires = [];
37
28
  for (const ent of readdirSync(appsDir, { withFileTypes: true })) {
@@ -47,7 +38,7 @@ export function resolveDevEntry(cwd) {
47
38
  for (const f of ["run.ts", "main.ts"]) {
48
39
  const p = resolve(appsDir, only, f);
49
40
  if (existsSync(p)) {
50
- return { kind: "vite-node", entry: p, rel: relPath(cwd, p), label: only };
41
+ return { kind: "wire", entry: p, rel: relPath(cwd, p), label: only };
51
42
  }
52
43
  }
53
44
  }
@@ -56,14 +47,25 @@ export function resolveDevEntry(cwd) {
56
47
  // ignore — fall through to app/ and scripts
57
48
  }
58
49
  }
59
- // 3. Single-app folderapp/main.ts, app/run.ts, app/index.ts.
50
+ // 2. Root entry — main.ts / run.ts at the project root (the template shape:
51
+ // the file that calls `endpoint().run()` lives at the root, with an
52
+ // `app/index.ts` barrel beside it). Checked BEFORE the app/ folder so the
53
+ // real entry wins over a barrel that mounts nothing.
54
+ for (const f of ["main.ts", "run.ts"]) {
55
+ const p = resolve(cwd, f);
56
+ if (existsSync(p)) {
57
+ return { kind: "wire", entry: p, rel: relPath(cwd, p), label: "app" };
58
+ }
59
+ }
60
+ // 3. Single-app folder — app/main.ts, app/run.ts, then the app/index.ts
61
+ // barrel as a last resort.
60
62
  for (const f of ["app/main.ts", "app/run.ts", "app/index.ts"]) {
61
63
  const p = resolve(cwd, f);
62
64
  if (existsSync(p)) {
63
- return { kind: "vite-node", entry: p, rel: relPath(cwd, p), label: "app" };
65
+ return { kind: "wire", entry: p, rel: relPath(cwd, p), label: "app" };
64
66
  }
65
67
  }
66
- // 4. package.json scripts.dev fallback.
68
+ // 3. package.json scripts.dev fallback.
67
69
  const pj = resolve(cwd, "package.json");
68
70
  if (existsSync(pj)) {
69
71
  try {
@@ -82,9 +84,10 @@ export function resolveDevEntry(cwd) {
82
84
  /** Same fallback list rendered for the "no dev entry found" error message. */
83
85
  export function devEntryCandidates() {
84
86
  return [
85
- "apps/dev-all/run.ts",
86
87
  "apps/<wire>/run.ts",
87
88
  "apps/<wire>/main.ts",
89
+ "main.ts",
90
+ "run.ts",
88
91
  "app/main.ts",
89
92
  "app/run.ts",
90
93
  "app/index.ts",
@@ -92,10 +95,6 @@ export function devEntryCandidates() {
92
95
  ];
93
96
  }
94
97
  function relPath(cwd, abs) {
95
- // `path.relative` already handles the cross-platform `path.sep` story
96
- // (Windows backslashes vs POSIX forward slashes) and the
97
- // "abs is outside cwd" case (returns a `..` chain) — no manual
98
- // `startsWith(cwd + "/")` math needed.
99
98
  const rel = relative(cwd, abs);
100
99
  return rel === "" ? abs : rel;
101
100
  }