@webstir-io/webstir 0.1.1 → 0.1.2

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 (77) hide show
  1. package/README.md +13 -0
  2. package/assets/deployment/docker/.dockerignore +7 -0
  3. package/assets/deployment/docker/Dockerfile +17 -0
  4. package/assets/deployment/docker/README.md +44 -0
  5. package/assets/deployment/docker/example.env +3 -0
  6. package/assets/features/client_nav/client_nav.ts +369 -264
  7. package/assets/features/client_nav/document_navigation.ts +344 -0
  8. package/assets/features/client_nav/form_enhancement.ts +275 -0
  9. package/assets/templates/api/src/backend/index.ts +71 -10
  10. package/assets/templates/api/src/backend/tsconfig.json +6 -1
  11. package/assets/templates/full/src/backend/index.ts +71 -10
  12. package/assets/templates/full/src/backend/module.ts +515 -0
  13. package/assets/templates/full/src/backend/tests/progressive-enhancement.test.ts +180 -0
  14. package/assets/templates/full/src/backend/tsconfig.json +6 -1
  15. package/assets/templates/full/src/frontend/app/scripts/features/client-nav.ts +574 -0
  16. package/assets/templates/full/src/frontend/app/scripts/features/document-navigation.ts +344 -0
  17. package/assets/templates/full/src/frontend/app/scripts/features/form-enhancement.ts +275 -0
  18. package/assets/templates/full/src/frontend/pages/home/index.css +8 -0
  19. package/assets/templates/full/src/frontend/pages/home/index.html +6 -1
  20. package/assets/templates/full/src/frontend/pages/home/tests/home.test.ts +12 -2
  21. package/assets/templates/spa/src/frontend/pages/home/tests/home.test.ts +10 -2
  22. package/package.json +31 -13
  23. package/scripts/check-feature-projections.mjs +87 -0
  24. package/scripts/check-full-demo-sync.mjs +89 -0
  25. package/scripts/check-package-install.mjs +537 -0
  26. package/scripts/check-standalone-install.mjs +221 -0
  27. package/scripts/pack-standalone.mjs +52 -28
  28. package/scripts/publish.sh +9 -0
  29. package/scripts/run-tests.mjs +99 -0
  30. package/scripts/sync-assets.mjs +175 -17
  31. package/src/add-backend-compat.ts +628 -0
  32. package/src/add-backend.ts +155 -27
  33. package/src/add.ts +111 -4
  34. package/src/agent.ts +393 -0
  35. package/src/api-watch.ts +7 -4
  36. package/src/backend-inspect.ts +70 -2
  37. package/src/backend-runtime.ts +22 -14
  38. package/src/build.ts +1 -3
  39. package/src/bun-generated-frontend-watch.ts +209 -0
  40. package/src/bun-globals.d.ts +23 -0
  41. package/src/bun-spa-document.ts +310 -0
  42. package/src/bun-spa-routes.ts +159 -0
  43. package/src/bun-spa-watch.ts +29 -0
  44. package/src/bun-ssg-watch.ts +304 -0
  45. package/src/cli.ts +381 -50
  46. package/src/compile-tests.ts +37 -29
  47. package/src/dev-server.ts +214 -143
  48. package/src/doctor.ts +164 -0
  49. package/src/enable-assets.ts +18 -1
  50. package/src/enable.ts +133 -41
  51. package/src/execute.ts +28 -4
  52. package/src/external-workspace.ts +178 -0
  53. package/src/format.ts +296 -17
  54. package/src/frontend-inspect.ts +32 -0
  55. package/src/frontend-watch.ts +27 -102
  56. package/src/full-watch.ts +13 -18
  57. package/src/index.ts +7 -0
  58. package/src/init-assets.ts +41 -11
  59. package/src/init.ts +85 -71
  60. package/src/inspect.ts +112 -0
  61. package/src/mcp/run-cli-json.ts +46 -0
  62. package/src/mcp/server.ts +307 -0
  63. package/src/operations.ts +176 -0
  64. package/src/providers.ts +20 -18
  65. package/src/refresh.ts +29 -3
  66. package/src/repair.ts +110 -43
  67. package/src/runtime-filter.ts +41 -0
  68. package/src/runtime.ts +1 -1
  69. package/src/smoke.ts +48 -16
  70. package/src/test.ts +54 -16
  71. package/src/testing-runtime.ts +273 -0
  72. package/src/types.ts +1 -4
  73. package/src/watch-events.ts +46 -17
  74. package/src/watch.ts +5 -1
  75. package/src/workspace-watcher.ts +10 -6
  76. package/src/workspace.ts +4 -2
  77. package/src/watch-daemon-client.ts +0 -171
package/src/cli.ts CHANGED
@@ -4,30 +4,30 @@ import path from 'node:path';
4
4
  import { realpathSync } from 'node:fs';
5
5
  import { fileURLToPath } from 'node:url';
6
6
 
7
- import { runAddPageCommand, runAddTestCommand } from './add.ts';
8
- import { runAddJobCommand, runAddRouteCommand } from './add-backend.ts';
9
- import { runBackendInspect } from './backend-inspect.ts';
10
- import { runEnable } from './enable.ts';
11
7
  import {
12
8
  formatAddSummary,
9
+ formatAgentJson,
10
+ formatAgentSummary,
11
+ formatBackendInspectJson,
13
12
  formatBackendInspectSummary,
14
13
  formatBuildSummary,
14
+ formatDoctorJson,
15
+ formatDoctorSummary,
15
16
  formatEnableSummary,
17
+ formatFrontendInspectJson,
18
+ formatFrontendInspectSummary,
16
19
  formatInitSummary,
20
+ formatInspectJson,
21
+ formatInspectSummary,
22
+ formatOperationsJson,
23
+ formatOperationsSummary,
17
24
  formatPublishSummary,
25
+ formatRepairJson,
18
26
  formatRepairSummary,
19
27
  formatRefreshSummary,
20
28
  formatSmokeSummary,
21
29
  formatTestSummary,
22
30
  } from './format.ts';
23
- import { runInit } from './init.ts';
24
- import { runRepair } from './repair.ts';
25
- import { runRefresh } from './refresh.ts';
26
- import { runBuild } from './build.ts';
27
- import { runPublish } from './publish.ts';
28
- import { runSmoke } from './smoke.ts';
29
- import { runTest } from './test.ts';
30
- import { runWatch } from './watch.ts';
31
31
 
32
32
  interface CliStream {
33
33
  write(message: string): void;
@@ -43,13 +43,19 @@ const HELP_TEXT = `Usage:
43
43
  webstir init <directory>
44
44
  webstir add-page <name> --workspace <path>
45
45
  webstir add-test <name-or-path> --workspace <path>
46
- webstir add-route <name> --workspace <path> [--method <METHOD>] [--path <path>] [--fastify]
46
+ webstir add-route <name> --workspace <path> [--method <METHOD>] [--path <path>] [--interaction <navigation|mutation>] [--session <optional|required>] [--session-write] [--form-urlencoded] [--csrf] [--fragment-target <target>] [--fragment-selector <selector>] [--fragment-mode <replace|append|prepend>]
47
47
  webstir add-job <name> --workspace <path> [--schedule <expression>]
48
+ webstir operations
49
+ webstir mcp
50
+ webstir agent <inspect|validate|repair|scaffold-page|scaffold-route|scaffold-job> [...]
51
+ webstir inspect --workspace <path>
52
+ webstir frontend-inspect --workspace <path>
48
53
  webstir backend-inspect --workspace <path>
54
+ webstir doctor --workspace <path>
49
55
  webstir test --workspace <path> [--runtime <frontend|backend|all>]
50
56
  webstir smoke [--workspace <path>]
51
57
  webstir build --workspace <path>
52
- webstir publish --workspace <path>
58
+ webstir publish --workspace <path> [--frontend-mode <bundle|ssg>]
53
59
  webstir enable <feature> [feature-args...] --workspace <path>
54
60
  webstir repair --workspace <path> [--dry-run]
55
61
  webstir refresh <mode> --workspace <path>
@@ -61,7 +67,13 @@ Commands:
61
67
  add-test Scaffold a test file in an existing workspace.
62
68
  add-route Scaffold a backend route in an existing workspace.
63
69
  add-job Scaffold a backend job in an existing workspace.
70
+ operations List the stable Webstir framework operations.
71
+ mcp Run the Webstir MCP server over stdio.
72
+ agent Orchestrate stable Webstir operations for a narrow goal.
73
+ inspect Diagnose and surface stable frontend and backend contract data.
74
+ frontend-inspect Inspect stable frontend workspace facts for an existing workspace.
64
75
  backend-inspect Inspect the backend manifest for an existing workspace.
76
+ doctor Diagnose scaffold drift and backend health for an existing workspace.
65
77
  test Build and run workspace tests with the Bun orchestrator.
66
78
  smoke Run an end-to-end Bun orchestrator verification flow.
67
79
  build Build a Webstir workspace with the Bun orchestrator.
@@ -75,7 +87,9 @@ Options:
75
87
  -w, --workspace <path> Workspace root to operate on.
76
88
  --host <host> Dev host or bind address (default: 127.0.0.1).
77
89
  --port <port> Dev port (SPA default: 8088, API default: 4321).
90
+ --frontend-mode <mode> Frontend publish mode for publish (defaults to bundle; bundle or ssg).
78
91
  --dry-run Report repair changes without writing files.
92
+ --json Emit machine-readable JSON for supported commands.
79
93
  -v, --verbose Enable verbose frontend watch diagnostics.
80
94
  --hmr-verbose Enable detailed hot-update diagnostics.
81
95
  -h, --help Show this help text.
@@ -89,27 +103,33 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
89
103
 
90
104
  const [command, ...rest] = argv;
91
105
  if (
92
- command !== 'init'
93
- && command !== 'add-page'
94
- && command !== 'add-test'
95
- && command !== 'add-route'
96
- && command !== 'add-job'
97
- && command !== 'backend-inspect'
98
- && command !== 'test'
99
- && command !== 'smoke'
100
- && command !== 'build'
101
- && command !== 'publish'
102
- && command !== 'enable'
103
- && command !== 'repair'
104
- && command !== 'refresh'
105
- && command !== 'watch'
106
+ command !== 'init' &&
107
+ command !== 'add-page' &&
108
+ command !== 'add-test' &&
109
+ command !== 'add-route' &&
110
+ command !== 'add-job' &&
111
+ command !== 'operations' &&
112
+ command !== 'mcp' &&
113
+ command !== 'agent' &&
114
+ command !== 'inspect' &&
115
+ command !== 'frontend-inspect' &&
116
+ command !== 'backend-inspect' &&
117
+ command !== 'doctor' &&
118
+ command !== 'test' &&
119
+ command !== 'smoke' &&
120
+ command !== 'build' &&
121
+ command !== 'publish' &&
122
+ command !== 'enable' &&
123
+ command !== 'repair' &&
124
+ command !== 'refresh' &&
125
+ command !== 'watch'
106
126
  ) {
107
127
  io.stderr.write(`Unknown command "${command}".\n\n${HELP_TEXT}`);
108
128
  return 1;
109
129
  }
110
130
 
111
131
  const options = parseCommandOptions(rest, {
112
- allowUnknownOptions: command === 'add-route' || command === 'add-job',
132
+ allowUnknownOptions: command === 'add-route' || command === 'add-job' || command === 'agent',
113
133
  });
114
134
  if (options.help) {
115
135
  io.stdout.write(HELP_TEXT);
@@ -126,8 +146,35 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
126
146
  return 1;
127
147
  }
128
148
 
149
+ if (
150
+ options.json &&
151
+ command !== 'operations' &&
152
+ command !== 'agent' &&
153
+ command !== 'inspect' &&
154
+ command !== 'frontend-inspect' &&
155
+ command !== 'backend-inspect' &&
156
+ command !== 'doctor' &&
157
+ command !== 'repair'
158
+ ) {
159
+ io.stderr.write(
160
+ `Only operations, agent, inspect, frontend-inspect, backend-inspect, doctor, and repair accept --json.\n\n${HELP_TEXT}`,
161
+ );
162
+ return 1;
163
+ }
164
+
165
+ if (options.frontendMode && command !== 'publish') {
166
+ io.stderr.write(`Only publish accepts --frontend-mode.\n\n${HELP_TEXT}`);
167
+ return 1;
168
+ }
169
+
129
170
  const workspaceRoot = options.workspaceRoot;
130
- if (command !== 'init' && command !== 'smoke' && !workspaceRoot) {
171
+ if (
172
+ command !== 'init' &&
173
+ command !== 'smoke' &&
174
+ command !== 'operations' &&
175
+ command !== 'mcp' &&
176
+ !workspaceRoot
177
+ ) {
131
178
  io.stderr.write(`Missing required --workspace <path>.\n\n${HELP_TEXT}`);
132
179
  return 1;
133
180
  }
@@ -139,6 +186,7 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
139
186
  return 1;
140
187
  }
141
188
 
189
+ const { runInit } = await import('./init.ts');
142
190
  const result = await runInit({
143
191
  args: options.positionals,
144
192
  workspaceRoot,
@@ -148,18 +196,63 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
148
196
  }
149
197
 
150
198
  const resolvedWorkspaceRoot = workspaceRoot ? path.resolve(workspaceRoot) : undefined;
199
+ const requireWorkspaceRoot = (): string => {
200
+ if (!resolvedWorkspaceRoot) {
201
+ throw new Error('Missing required --workspace <path>.');
202
+ }
203
+
204
+ return resolvedWorkspaceRoot;
205
+ };
206
+ if (command === 'operations') {
207
+ if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
208
+ io.stderr.write(`Operations does not accept watch options.\n\n${HELP_TEXT}`);
209
+ return 1;
210
+ }
211
+
212
+ if (options.positionals.length > 0) {
213
+ io.stderr.write(`Operations does not accept positional arguments.\n\n${HELP_TEXT}`);
214
+ return 1;
215
+ }
216
+
217
+ const { listOperations } = await import('./operations.ts');
218
+ const operations = listOperations();
219
+ io.stdout.write(
220
+ `${options.json ? formatOperationsJson(operations) : formatOperationsSummary(operations)}\n`,
221
+ );
222
+ return 0;
223
+ }
224
+
225
+ if (command === 'mcp') {
226
+ if (
227
+ options.host ||
228
+ options.port !== undefined ||
229
+ options.verbose ||
230
+ options.hmrVerbose ||
231
+ options.positionals.length > 0 ||
232
+ options.workspaceRoot ||
233
+ options.json
234
+ ) {
235
+ io.stderr.write(`MCP does not accept CLI options.\n\n${HELP_TEXT}`);
236
+ return 1;
237
+ }
238
+
239
+ const { runMcpServer } = await import('./mcp/server.ts');
240
+ await runMcpServer();
241
+ return 0;
242
+ }
151
243
  if (command === 'add-page') {
152
244
  if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
153
245
  io.stderr.write(`Add-page does not accept watch options.\n\n${HELP_TEXT}`);
154
246
  return 1;
155
247
  }
156
248
 
249
+ const { runAddPageCommand } = await import('./add.ts');
157
250
  const result = await runAddPageCommand({
158
- workspaceRoot: resolvedWorkspaceRoot!,
251
+ workspaceRoot: requireWorkspaceRoot(),
159
252
  args: options.positionals,
160
253
  });
161
254
  io.stdout.write(
162
- `${formatAddSummary('[webstir] add-page complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
255
+ `${formatAddSummary('[webstir] add-page complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`,
163
256
  );
164
257
  return 0;
165
258
  }
@@ -170,12 +263,13 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
170
263
  return 1;
171
264
  }
172
265
 
266
+ const { runAddTestCommand } = await import('./add.ts');
173
267
  const result = await runAddTestCommand({
174
- workspaceRoot: resolvedWorkspaceRoot!,
268
+ workspaceRoot: requireWorkspaceRoot(),
175
269
  args: options.positionals,
176
270
  });
177
271
  io.stdout.write(
178
- `${formatAddSummary('[webstir] add-test complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
272
+ `${formatAddSummary('[webstir] add-test complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`,
179
273
  );
180
274
  return 0;
181
275
  }
@@ -186,12 +280,13 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
186
280
  return 1;
187
281
  }
188
282
 
283
+ const { runAddRouteCommand } = await import('./add-backend.ts');
189
284
  const result = await runAddRouteCommand({
190
- workspaceRoot: resolvedWorkspaceRoot!,
285
+ workspaceRoot: requireWorkspaceRoot(),
191
286
  rawArgs: options.rawArgs,
192
287
  });
193
288
  io.stdout.write(
194
- `${formatAddSummary('[webstir] add-route complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
289
+ `${formatAddSummary('[webstir] add-route complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`,
195
290
  );
196
291
  return 0;
197
292
  }
@@ -202,12 +297,13 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
202
297
  return 1;
203
298
  }
204
299
 
300
+ const { runAddJobCommand } = await import('./add-backend.ts');
205
301
  const result = await runAddJobCommand({
206
- workspaceRoot: resolvedWorkspaceRoot!,
302
+ workspaceRoot: requireWorkspaceRoot(),
207
303
  rawArgs: options.rawArgs,
208
304
  });
209
305
  io.stdout.write(
210
- `${formatAddSummary('[webstir] add-job complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`
306
+ `${formatAddSummary('[webstir] add-job complete', result.target, result.workspaceRoot, result.changes, result.note)}\n`,
211
307
  );
212
308
  return 0;
213
309
  }
@@ -223,21 +319,130 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
223
319
  return 1;
224
320
  }
225
321
 
226
- const result = await runBackendInspect({
227
- workspaceRoot: resolvedWorkspaceRoot!,
322
+ const { runBackendInspect } = await import('./backend-inspect.ts');
323
+ const result = options.json
324
+ ? await withSuppressedStdout(() =>
325
+ runBackendInspect({
326
+ workspaceRoot: requireWorkspaceRoot(),
327
+ }),
328
+ )
329
+ : await runBackendInspect({
330
+ workspaceRoot: requireWorkspaceRoot(),
331
+ });
332
+ io.stdout.write(
333
+ `${options.json ? formatBackendInspectJson(result) : formatBackendInspectSummary(result)}\n`,
334
+ );
335
+ return 0;
336
+ }
337
+
338
+ if (command === 'frontend-inspect') {
339
+ if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
340
+ io.stderr.write(`Frontend-inspect does not accept watch options.\n\n${HELP_TEXT}`);
341
+ return 1;
342
+ }
343
+
344
+ if (options.positionals.length > 0) {
345
+ io.stderr.write(`Frontend-inspect does not accept positional arguments.\n\n${HELP_TEXT}`);
346
+ return 1;
347
+ }
348
+
349
+ const { runFrontendInspect } = await import('./frontend-inspect.ts');
350
+ const result = await runFrontendInspect({
351
+ workspaceRoot: requireWorkspaceRoot(),
228
352
  });
229
- io.stdout.write(`${formatBackendInspectSummary(result)}\n`);
353
+ io.stdout.write(
354
+ `${options.json ? formatFrontendInspectJson(result) : formatFrontendInspectSummary(result)}\n`,
355
+ );
230
356
  return 0;
231
357
  }
232
358
 
359
+ if (command === 'doctor') {
360
+ if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
361
+ io.stderr.write(`Doctor does not accept watch options.\n\n${HELP_TEXT}`);
362
+ return 1;
363
+ }
364
+
365
+ if (options.positionals.length > 0) {
366
+ io.stderr.write(`Doctor does not accept positional arguments.\n\n${HELP_TEXT}`);
367
+ return 1;
368
+ }
369
+
370
+ const { runDoctor } = await import('./doctor.ts');
371
+ const result = await withSuppressedStdout(() =>
372
+ runDoctor({
373
+ workspaceRoot: requireWorkspaceRoot(),
374
+ }),
375
+ );
376
+ io.stdout.write(`${options.json ? formatDoctorJson(result) : formatDoctorSummary(result)}\n`);
377
+ return result.healthy ? 0 : 1;
378
+ }
379
+
380
+ if (command === 'inspect') {
381
+ if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
382
+ io.stderr.write(`Inspect does not accept watch options.\n\n${HELP_TEXT}`);
383
+ return 1;
384
+ }
385
+
386
+ if (options.positionals.length > 0) {
387
+ io.stderr.write(`Inspect does not accept positional arguments.\n\n${HELP_TEXT}`);
388
+ return 1;
389
+ }
390
+
391
+ const { runInspect } = await import('./inspect.ts');
392
+ const result = await withSuppressedStdout(() =>
393
+ runInspect({
394
+ workspaceRoot: requireWorkspaceRoot(),
395
+ }),
396
+ );
397
+ io.stdout.write(
398
+ `${options.json ? formatInspectJson(result) : formatInspectSummary(result)}\n`,
399
+ );
400
+ return result.success ? 0 : 1;
401
+ }
402
+
403
+ if (command === 'agent') {
404
+ if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
405
+ io.stderr.write(`Agent does not accept watch options.\n\n${HELP_TEXT}`);
406
+ return 1;
407
+ }
408
+
409
+ const goal = options.positionals[0];
410
+ if (
411
+ goal !== 'inspect' &&
412
+ goal !== 'validate' &&
413
+ goal !== 'repair' &&
414
+ goal !== 'scaffold-page' &&
415
+ goal !== 'scaffold-route' &&
416
+ goal !== 'scaffold-job'
417
+ ) {
418
+ io.stderr.write(
419
+ `Agent requires one of: inspect, validate, repair, scaffold-page, scaffold-route, scaffold-job.\n\n${HELP_TEXT}`,
420
+ );
421
+ return 1;
422
+ }
423
+
424
+ const { runAgent } = await import('./agent.ts');
425
+ const result = await withSuppressedStdout(() =>
426
+ runAgent({
427
+ workspaceRoot: requireWorkspaceRoot(),
428
+ goal,
429
+ rawArgs: options.rawArgs,
430
+ positionals: options.positionals,
431
+ }),
432
+ );
433
+ io.stdout.write(`${options.json ? formatAgentJson(result) : formatAgentSummary(result)}\n`);
434
+ return result.success ? 0 : 1;
435
+ }
436
+
233
437
  if (command === 'test') {
234
438
  if (options.host || options.port !== undefined || options.verbose || options.hmrVerbose) {
235
439
  io.stderr.write(`Test does not accept watch options.\n\n${HELP_TEXT}`);
236
440
  return 1;
237
441
  }
238
442
 
443
+ const { runTest } = await import('./test.ts');
239
444
  const result = await runTest({
240
- workspaceRoot: resolvedWorkspaceRoot!,
445
+ workspaceRoot: requireWorkspaceRoot(),
241
446
  rawArgs: options.rawArgs,
242
447
  });
243
448
  io.stdout.write(`${formatTestSummary(result)}\n`);
@@ -255,6 +460,7 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
255
460
  return 1;
256
461
  }
257
462
 
463
+ const { runSmoke } = await import('./smoke.ts');
258
464
  const result = await runSmoke({
259
465
  workspaceRoot: resolvedWorkspaceRoot,
260
466
  });
@@ -268,8 +474,9 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
268
474
  return 1;
269
475
  }
270
476
 
477
+ const { runBuild } = await import('./build.ts');
271
478
  const result = await runBuild({
272
- workspaceRoot: resolvedWorkspaceRoot!,
479
+ workspaceRoot: requireWorkspaceRoot(),
273
480
  });
274
481
  io.stdout.write(`${formatBuildSummary(result)}\n`);
275
482
  return 0;
@@ -281,16 +488,19 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
281
488
  return 1;
282
489
  }
283
490
 
491
+ const { runPublish } = await import('./publish.ts');
284
492
  const result = await runPublish({
285
- workspaceRoot: resolvedWorkspaceRoot!,
493
+ workspaceRoot: requireWorkspaceRoot(),
494
+ env: options.frontendMode ? { WEBSTIR_FRONTEND_MODE: options.frontendMode } : undefined,
286
495
  });
287
496
  io.stdout.write(`${formatPublishSummary(result)}\n`);
288
497
  return 0;
289
498
  }
290
499
 
291
500
  if (command === 'enable') {
501
+ const { runEnable } = await import('./enable.ts');
292
502
  const result = await runEnable({
293
- workspaceRoot: resolvedWorkspaceRoot!,
503
+ workspaceRoot: requireWorkspaceRoot(),
294
504
  args: options.positionals,
295
505
  });
296
506
  io.stdout.write(`${formatEnableSummary(result)}\n`);
@@ -308,11 +518,12 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
308
518
  return 1;
309
519
  }
310
520
 
521
+ const { runRepair } = await import('./repair.ts');
311
522
  const result = await runRepair({
312
- workspaceRoot: resolvedWorkspaceRoot!,
523
+ workspaceRoot: requireWorkspaceRoot(),
313
524
  rawArgs: options.rawArgs,
314
525
  });
315
- io.stdout.write(`${formatRepairSummary(result)}\n`);
526
+ io.stdout.write(`${options.json ? formatRepairJson(result) : formatRepairSummary(result)}\n`);
316
527
  return 0;
317
528
  }
318
529
 
@@ -322,8 +533,9 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
322
533
  return 1;
323
534
  }
324
535
 
536
+ const { runRefresh } = await import('./refresh.ts');
325
537
  const result = await runRefresh({
326
- workspaceRoot: resolvedWorkspaceRoot!,
538
+ workspaceRoot: requireWorkspaceRoot(),
327
539
  args: options.positionals,
328
540
  });
329
541
  io.stdout.write(`${formatRefreshSummary(result)}\n`);
@@ -335,8 +547,9 @@ export async function runCli(argv: readonly string[], io: CliIo = defaultIo): Pr
335
547
  return 1;
336
548
  }
337
549
 
550
+ const { runWatch } = await import('./watch.ts');
338
551
  await runWatch({
339
- workspaceRoot: resolvedWorkspaceRoot!,
552
+ workspaceRoot: requireWorkspaceRoot(),
340
553
  host: options.host,
341
554
  port: options.port,
342
555
  verbose: options.verbose,
@@ -355,7 +568,9 @@ interface ParsedCommandOptions {
355
568
  readonly workspaceRoot?: string;
356
569
  readonly host?: string;
357
570
  readonly port?: number;
571
+ readonly frontendMode?: 'bundle' | 'ssg';
358
572
  readonly dryRun: boolean;
573
+ readonly json: boolean;
359
574
  readonly verbose: boolean;
360
575
  readonly hmrVerbose: boolean;
361
576
  readonly positionals: readonly string[];
@@ -366,12 +581,14 @@ interface ParsedCommandOptions {
366
581
 
367
582
  function parseCommandOptions(
368
583
  args: readonly string[],
369
- options: { readonly allowUnknownOptions?: boolean } = {}
584
+ options: { readonly allowUnknownOptions?: boolean } = {},
370
585
  ): ParsedCommandOptions {
371
586
  let workspaceRoot: string | undefined;
372
587
  let host: string | undefined;
373
588
  let port: number | undefined;
589
+ let frontendMode: 'bundle' | 'ssg' | undefined;
374
590
  let dryRun = false;
591
+ let json = false;
375
592
  let verbose = false;
376
593
  let hmrVerbose = false;
377
594
  const positionals: string[] = [];
@@ -390,7 +607,9 @@ function parseCommandOptions(
390
607
  workspaceRoot,
391
608
  host,
392
609
  port,
610
+ frontendMode,
393
611
  dryRun,
612
+ json,
394
613
  verbose,
395
614
  hmrVerbose,
396
615
  positionals,
@@ -405,6 +624,89 @@ function parseCommandOptions(
405
624
  continue;
406
625
  }
407
626
 
627
+ if (arg === '--frontend-mode') {
628
+ const next = args[index + 1];
629
+ if (!next || next.startsWith('-')) {
630
+ return {
631
+ workspaceRoot,
632
+ host,
633
+ port,
634
+ frontendMode,
635
+ dryRun,
636
+ json,
637
+ verbose,
638
+ hmrVerbose,
639
+ positionals,
640
+ rawArgs: args,
641
+ help: false,
642
+ error: 'Missing value for --frontend-mode.',
643
+ };
644
+ }
645
+
646
+ const normalizedMode = next.toLowerCase();
647
+ if (normalizedMode !== 'bundle' && normalizedMode !== 'ssg') {
648
+ return {
649
+ workspaceRoot,
650
+ host,
651
+ port,
652
+ frontendMode,
653
+ dryRun,
654
+ json,
655
+ verbose,
656
+ hmrVerbose,
657
+ positionals,
658
+ rawArgs: args,
659
+ help: false,
660
+ error: `Invalid --frontend-mode value "${next}". Expected bundle or ssg.`,
661
+ };
662
+ }
663
+
664
+ frontendMode = normalizedMode;
665
+ index += 1;
666
+ continue;
667
+ }
668
+
669
+ if (arg.startsWith('--frontend-mode=')) {
670
+ const rawMode = arg.slice('--frontend-mode='.length);
671
+ if (rawMode.length === 0) {
672
+ return {
673
+ workspaceRoot,
674
+ host,
675
+ port,
676
+ frontendMode,
677
+ dryRun,
678
+ json,
679
+ verbose,
680
+ hmrVerbose,
681
+ positionals,
682
+ rawArgs: args,
683
+ help: false,
684
+ error: 'Missing value for --frontend-mode.',
685
+ };
686
+ }
687
+
688
+ const normalizedMode = rawMode.toLowerCase();
689
+ if (normalizedMode !== 'bundle' && normalizedMode !== 'ssg') {
690
+ return {
691
+ workspaceRoot,
692
+ host,
693
+ port,
694
+ frontendMode,
695
+ dryRun,
696
+ json,
697
+ verbose,
698
+ hmrVerbose,
699
+ positionals,
700
+ rawArgs: args,
701
+ help: false,
702
+ error: `Invalid --frontend-mode value "${rawMode}". Expected bundle or ssg.`,
703
+ };
704
+ }
705
+
706
+ frontendMode = normalizedMode;
707
+ continue;
708
+ }
709
+
408
710
  if (arg === '--host') {
409
711
  const next = args[index + 1];
410
712
  if (!next || next.startsWith('-')) {
@@ -413,6 +715,7 @@ function parseCommandOptions(
413
715
  host,
414
716
  port,
415
717
  dryRun,
718
+ json,
416
719
  verbose,
417
720
  hmrVerbose,
418
721
  positionals,
@@ -436,6 +739,7 @@ function parseCommandOptions(
436
739
  host,
437
740
  port,
438
741
  dryRun,
742
+ json,
439
743
  verbose,
440
744
  hmrVerbose,
441
745
  positionals,
@@ -458,6 +762,7 @@ function parseCommandOptions(
458
762
  host,
459
763
  port,
460
764
  dryRun,
765
+ json,
461
766
  verbose,
462
767
  hmrVerbose,
463
768
  positionals,
@@ -480,6 +785,11 @@ function parseCommandOptions(
480
785
  continue;
481
786
  }
482
787
 
788
+ if (arg === '--json') {
789
+ json = true;
790
+ continue;
791
+ }
792
+
483
793
  if (arg === '--hmr-verbose') {
484
794
  hmrVerbose = true;
485
795
  continue;
@@ -491,6 +801,7 @@ function parseCommandOptions(
491
801
  host,
492
802
  port,
493
803
  dryRun,
804
+ json,
494
805
  verbose,
495
806
  hmrVerbose,
496
807
  positionals,
@@ -513,6 +824,7 @@ function parseCommandOptions(
513
824
  host,
514
825
  port,
515
826
  dryRun,
827
+ json,
516
828
  verbose,
517
829
  hmrVerbose,
518
830
  positionals,
@@ -526,7 +838,9 @@ function parseCommandOptions(
526
838
  workspaceRoot,
527
839
  host,
528
840
  port,
841
+ frontendMode,
529
842
  dryRun,
843
+ json,
530
844
  verbose,
531
845
  hmrVerbose,
532
846
  positionals,
@@ -567,3 +881,20 @@ function resolveRealpath(filePath: string): string {
567
881
  return path.resolve(filePath);
568
882
  }
569
883
  }
884
+
885
+ async function withSuppressedStdout<T>(callback: () => Promise<T>): Promise<T> {
886
+ const originalWrite = process.stdout.write.bind(process.stdout);
887
+ const originalLog = console.log;
888
+ const originalInfo = console.info;
889
+ process.stdout.write = (() => true) as typeof process.stdout.write;
890
+ console.log = (() => undefined) as typeof console.log;
891
+ console.info = (() => undefined) as typeof console.info;
892
+
893
+ try {
894
+ return await callback();
895
+ } finally {
896
+ process.stdout.write = originalWrite;
897
+ console.log = originalLog;
898
+ console.info = originalInfo;
899
+ }
900
+ }