@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/agent.ts ADDED
@@ -0,0 +1,393 @@
1
+ import type { AddCommandResult } from './add.ts';
2
+ import type { AddJobScaffoldOptions, AddRouteScaffoldOptions } from './add-backend.ts';
3
+ import type { BackendInspectResult } from './backend-inspect.ts';
4
+ import type { DoctorResult } from './doctor.ts';
5
+ import type { RepairResult } from './repair.ts';
6
+ import type { TestCommandResult } from './test.ts';
7
+
8
+ import { runAddPageCommand } from './add.ts';
9
+ import {
10
+ parseAddJobScaffoldArgs,
11
+ parseAddRouteScaffoldArgs,
12
+ runAddJobScaffold,
13
+ runAddRouteScaffold,
14
+ } from './add-backend.ts';
15
+ import { runBackendInspect } from './backend-inspect.ts';
16
+ import { runDoctor } from './doctor.ts';
17
+ import { runRepair } from './repair.ts';
18
+ import { runTest } from './test.ts';
19
+
20
+ export type AgentGoal =
21
+ | 'inspect'
22
+ | 'validate'
23
+ | 'repair'
24
+ | 'scaffold-page'
25
+ | 'scaffold-route'
26
+ | 'scaffold-job';
27
+
28
+ export interface RunAgentOptions {
29
+ readonly workspaceRoot: string;
30
+ readonly goal: AgentGoal;
31
+ readonly rawArgs: readonly string[];
32
+ readonly positionals: readonly string[];
33
+ readonly env?: Record<string, string | undefined>;
34
+ }
35
+
36
+ export interface AgentStepResult {
37
+ readonly id:
38
+ | 'doctor'
39
+ | 'backend-inspect'
40
+ | 'test'
41
+ | 'repair'
42
+ | 'add-page'
43
+ | 'add-route'
44
+ | 'add-job';
45
+ readonly status: 'completed' | 'skipped' | 'failed';
46
+ readonly summary: string;
47
+ }
48
+
49
+ export interface AgentResult {
50
+ readonly workspaceRoot: string;
51
+ readonly goal: AgentGoal;
52
+ readonly success: boolean;
53
+ readonly steps: readonly AgentStepResult[];
54
+ readonly doctor?: DoctorResult;
55
+ readonly inspect?: BackendInspectResult;
56
+ readonly repair?: RepairResult;
57
+ readonly test?: Pick<TestCommandResult, 'runtime' | 'builtTargets' | 'summary' | 'hadFailures'>;
58
+ readonly scaffold?: Pick<AddCommandResult, 'subject' | 'target' | 'changes' | 'note'>;
59
+ }
60
+
61
+ export interface RunAgentScaffoldPageOptions {
62
+ readonly workspaceRoot: string;
63
+ readonly pageName: string;
64
+ readonly env?: Record<string, string | undefined>;
65
+ }
66
+
67
+ export interface RunAgentScaffoldRouteOptions extends AddRouteScaffoldOptions {
68
+ readonly env?: Record<string, string | undefined>;
69
+ }
70
+
71
+ export interface RunAgentScaffoldJobOptions extends AddJobScaffoldOptions {
72
+ readonly env?: Record<string, string | undefined>;
73
+ }
74
+
75
+ export async function runAgent(options: RunAgentOptions): Promise<AgentResult> {
76
+ const steps: AgentStepResult[] = [];
77
+
78
+ if (options.goal === 'inspect') {
79
+ const doctor = await runDoctor({
80
+ workspaceRoot: options.workspaceRoot,
81
+ env: options.env,
82
+ });
83
+ steps.push({
84
+ id: 'doctor',
85
+ status: doctor.healthy ? 'completed' : 'failed',
86
+ summary: doctor.healthy
87
+ ? 'Workspace diagnosis completed without issues.'
88
+ : `Workspace diagnosis found ${doctor.issues.length} issue(s).`,
89
+ });
90
+
91
+ const backendSupported = doctor.workspace.mode === 'api' || doctor.workspace.mode === 'full';
92
+ let inspectFailed = false;
93
+ let inspect: BackendInspectResult | undefined;
94
+ if (backendSupported) {
95
+ try {
96
+ inspect = await runBackendInspect({
97
+ workspaceRoot: options.workspaceRoot,
98
+ env: options.env,
99
+ });
100
+ steps.push({
101
+ id: 'backend-inspect',
102
+ status: 'completed',
103
+ summary: `${inspect.manifest.routes?.length ?? 0} route(s), ${inspect.manifest.jobs?.length ?? 0} job(s), module ${inspect.manifest.name}@${inspect.manifest.version}.`,
104
+ });
105
+ } catch (error) {
106
+ inspectFailed = true;
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ steps.push({
109
+ id: 'backend-inspect',
110
+ status: 'failed',
111
+ summary: `Backend inspection failed: ${message}`,
112
+ });
113
+ }
114
+ }
115
+
116
+ return {
117
+ workspaceRoot: options.workspaceRoot,
118
+ goal: options.goal,
119
+ success: doctor.healthy && !inspectFailed,
120
+ steps,
121
+ doctor,
122
+ ...(inspect ? { inspect } : {}),
123
+ };
124
+ }
125
+
126
+ if (options.goal === 'validate') {
127
+ const doctor = await runDoctor({
128
+ workspaceRoot: options.workspaceRoot,
129
+ env: options.env,
130
+ });
131
+ steps.push({
132
+ id: 'doctor',
133
+ status: doctor.healthy ? 'completed' : 'failed',
134
+ summary: doctor.healthy
135
+ ? 'Workspace diagnosis completed without issues.'
136
+ : `Workspace diagnosis found ${doctor.issues.length} issue(s).`,
137
+ });
138
+
139
+ if (!doctor.healthy) {
140
+ steps.push({
141
+ id: 'test',
142
+ status: 'skipped',
143
+ summary: 'Skipped tests because diagnosis reported issues first.',
144
+ });
145
+ return {
146
+ workspaceRoot: options.workspaceRoot,
147
+ goal: options.goal,
148
+ success: false,
149
+ steps,
150
+ doctor,
151
+ };
152
+ }
153
+
154
+ const test = await runTest({
155
+ workspaceRoot: options.workspaceRoot,
156
+ rawArgs: [],
157
+ env: options.env,
158
+ quietInstall: true,
159
+ });
160
+ steps.push({
161
+ id: 'test',
162
+ status: test.hadFailures ? 'failed' : 'completed',
163
+ summary: `${test.summary.passed} passed, ${test.summary.failed} failed.`,
164
+ });
165
+
166
+ return {
167
+ workspaceRoot: options.workspaceRoot,
168
+ goal: options.goal,
169
+ success: !test.hadFailures,
170
+ steps,
171
+ doctor,
172
+ test: {
173
+ runtime: test.runtime,
174
+ builtTargets: test.builtTargets,
175
+ summary: test.summary,
176
+ hadFailures: test.hadFailures,
177
+ },
178
+ };
179
+ }
180
+
181
+ if (options.goal === 'repair') {
182
+ const initialDoctor = await runDoctor({
183
+ workspaceRoot: options.workspaceRoot,
184
+ env: options.env,
185
+ });
186
+ steps.push({
187
+ id: 'doctor',
188
+ status: initialDoctor.healthy ? 'completed' : 'failed',
189
+ summary: initialDoctor.healthy
190
+ ? 'Workspace diagnosis completed without issues.'
191
+ : `Workspace diagnosis found ${initialDoctor.issues.length} issue(s).`,
192
+ });
193
+
194
+ if (initialDoctor.healthy) {
195
+ steps.push({
196
+ id: 'repair',
197
+ status: 'skipped',
198
+ summary: 'Skipped repair because the workspace is already healthy.',
199
+ });
200
+ return {
201
+ workspaceRoot: options.workspaceRoot,
202
+ goal: options.goal,
203
+ success: true,
204
+ steps,
205
+ doctor: initialDoctor,
206
+ };
207
+ }
208
+
209
+ if (initialDoctor.repair.changes.length === 0) {
210
+ steps.push({
211
+ id: 'repair',
212
+ status: 'skipped',
213
+ summary: 'No scaffold-managed repair action was available for the reported issues.',
214
+ });
215
+ return {
216
+ workspaceRoot: options.workspaceRoot,
217
+ goal: options.goal,
218
+ success: false,
219
+ steps,
220
+ doctor: initialDoctor,
221
+ };
222
+ }
223
+
224
+ const repair = await runRepair({
225
+ workspaceRoot: options.workspaceRoot,
226
+ rawArgs: [],
227
+ });
228
+ steps.push({
229
+ id: 'repair',
230
+ status: 'completed',
231
+ summary: `Applied ${repair.changes.length} scaffold-managed change(s).`,
232
+ });
233
+
234
+ const finalDoctor = await runDoctor({
235
+ workspaceRoot: options.workspaceRoot,
236
+ env: options.env,
237
+ });
238
+ steps.push({
239
+ id: 'doctor',
240
+ status: finalDoctor.healthy ? 'completed' : 'failed',
241
+ summary: finalDoctor.healthy
242
+ ? 'Workspace diagnosis passed after repair.'
243
+ : `Workspace still has ${finalDoctor.issues.length} issue(s) after repair.`,
244
+ });
245
+
246
+ return {
247
+ workspaceRoot: options.workspaceRoot,
248
+ goal: options.goal,
249
+ success: finalDoctor.healthy,
250
+ steps,
251
+ doctor: finalDoctor,
252
+ repair,
253
+ };
254
+ }
255
+
256
+ if (options.goal === 'scaffold-page') {
257
+ const pageName = options.positionals[1];
258
+ if (!pageName) {
259
+ throw new Error('Usage: webstir agent scaffold-page <name> --workspace <path>.');
260
+ }
261
+
262
+ return await runAgentScaffoldPage({
263
+ workspaceRoot: options.workspaceRoot,
264
+ pageName,
265
+ env: options.env,
266
+ });
267
+ }
268
+
269
+ if (options.goal === 'scaffold-route') {
270
+ return await runAgentScaffoldRoute({
271
+ workspaceRoot: options.workspaceRoot,
272
+ ...parseAddRouteScaffoldArgs(stripGoalFromRawArgs(options.rawArgs, options.goal)),
273
+ env: options.env,
274
+ });
275
+ }
276
+
277
+ return await runAgentScaffoldJob({
278
+ workspaceRoot: options.workspaceRoot,
279
+ ...parseAddJobScaffoldArgs(stripGoalFromRawArgs(options.rawArgs, options.goal)),
280
+ env: options.env,
281
+ });
282
+ }
283
+
284
+ export async function runAgentScaffoldPage(
285
+ options: RunAgentScaffoldPageOptions,
286
+ ): Promise<AgentResult> {
287
+ const steps: AgentStepResult[] = [];
288
+ const scaffold = await runAddPageCommand({
289
+ workspaceRoot: options.workspaceRoot,
290
+ args: [options.pageName],
291
+ });
292
+ steps.push({
293
+ id: 'add-page',
294
+ status: 'completed',
295
+ summary: `Scaffolded page ${scaffold.target}.`,
296
+ });
297
+
298
+ const doctor = await runDoctor({
299
+ workspaceRoot: options.workspaceRoot,
300
+ env: options.env,
301
+ });
302
+ steps.push({
303
+ id: 'doctor',
304
+ status: doctor.healthy ? 'completed' : 'failed',
305
+ summary: doctor.healthy
306
+ ? 'Workspace remains healthy after page scaffolding.'
307
+ : `Workspace diagnosis found ${doctor.issues.length} issue(s) after scaffolding.`,
308
+ });
309
+
310
+ return {
311
+ workspaceRoot: options.workspaceRoot,
312
+ goal: 'scaffold-page',
313
+ success: doctor.healthy,
314
+ steps,
315
+ doctor,
316
+ scaffold,
317
+ };
318
+ }
319
+
320
+ export async function runAgentScaffoldRoute(
321
+ options: RunAgentScaffoldRouteOptions,
322
+ ): Promise<AgentResult> {
323
+ const steps: AgentStepResult[] = [];
324
+ const scaffold = await runAddRouteScaffold(options);
325
+ steps.push({
326
+ id: 'add-route',
327
+ status: 'completed',
328
+ summary: `Scaffolded route ${scaffold.target}.`,
329
+ });
330
+
331
+ const inspect = await runBackendInspect({
332
+ workspaceRoot: options.workspaceRoot,
333
+ env: options.env,
334
+ });
335
+ steps.push({
336
+ id: 'backend-inspect',
337
+ status: 'completed',
338
+ summary: `${inspect.manifest.routes?.length ?? 0} route(s), ${inspect.manifest.jobs?.length ?? 0} job(s), module ${inspect.manifest.name}@${inspect.manifest.version}.`,
339
+ });
340
+
341
+ return {
342
+ workspaceRoot: options.workspaceRoot,
343
+ goal: 'scaffold-route',
344
+ success: true,
345
+ steps,
346
+ inspect,
347
+ scaffold,
348
+ };
349
+ }
350
+
351
+ export async function runAgentScaffoldJob(
352
+ options: RunAgentScaffoldJobOptions,
353
+ ): Promise<AgentResult> {
354
+ const steps: AgentStepResult[] = [];
355
+ const scaffold = await runAddJobScaffold(options);
356
+ steps.push({
357
+ id: 'add-job',
358
+ status: 'completed',
359
+ summary: `Scaffolded job ${scaffold.target}.`,
360
+ });
361
+
362
+ const inspect = await runBackendInspect({
363
+ workspaceRoot: options.workspaceRoot,
364
+ env: options.env,
365
+ });
366
+ steps.push({
367
+ id: 'backend-inspect',
368
+ status: 'completed',
369
+ summary: `${inspect.manifest.routes?.length ?? 0} route(s), ${inspect.manifest.jobs?.length ?? 0} job(s), module ${inspect.manifest.name}@${inspect.manifest.version}.`,
370
+ });
371
+
372
+ return {
373
+ workspaceRoot: options.workspaceRoot,
374
+ goal: 'scaffold-job',
375
+ success: true,
376
+ steps,
377
+ inspect,
378
+ scaffold,
379
+ };
380
+ }
381
+
382
+ function stripGoalFromRawArgs(rawArgs: readonly string[], goal: AgentGoal): string[] {
383
+ const next = [...rawArgs];
384
+ const goalIndex = next.indexOf(goal);
385
+ if (goalIndex >= 0) {
386
+ next.splice(goalIndex, 1);
387
+ }
388
+ const jsonIndex = next.indexOf('--json');
389
+ if (jsonIndex >= 0) {
390
+ next.splice(jsonIndex, 1);
391
+ }
392
+ return next;
393
+ }
package/src/api-watch.ts CHANGED
@@ -15,12 +15,12 @@ export interface ApiWatchSession {
15
15
  export async function runApiWatch(
16
16
  workspace: WorkspaceDescriptor,
17
17
  options: WatchOptions,
18
- io: WatchIo
18
+ io: WatchIo,
19
19
  ): Promise<void> {
20
20
  const session = await startApiWatchSession(workspace, options, io);
21
21
 
22
22
  io.stdout.write(
23
- `[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${session.origin}\n`
23
+ `[webstir] watch starting\nworkspace: ${workspace.name}\nmode: ${workspace.mode}\nurl: ${session.origin}\n`,
24
24
  );
25
25
 
26
26
  const stopSignal = createStopSignal();
@@ -35,9 +35,12 @@ export async function runApiWatch(
35
35
  export async function startApiWatchSession(
36
36
  workspace: WorkspaceDescriptor,
37
37
  options: WatchOptions,
38
- io: WatchIo
38
+ io: WatchIo,
39
39
  ): Promise<ApiWatchSession> {
40
- const runtimeEnv = createWorkspaceRuntimeEnv(workspace.root, 'build', options.env);
40
+ const runtimeEnv = {
41
+ ...createWorkspaceRuntimeEnv(workspace.root, 'build', options.env),
42
+ WEBSTIR_FRONTEND_DEV_SERVER: '1',
43
+ };
41
44
  const runtime = new BackendRuntimeSupervisor({
42
45
  workspaceRoot: workspace.root,
43
46
  buildRoot: path.join(workspace.root, 'build', 'backend'),
@@ -1,10 +1,17 @@
1
1
  import type { ModuleManifest } from '@webstir-io/module-contract';
2
2
  import type { WorkspaceDescriptor } from './types.ts';
3
3
 
4
+ import path from 'node:path';
5
+ import { existsSync } from 'node:fs';
6
+ import { readdir } from 'node:fs/promises';
7
+
4
8
  import { loadProvider } from './providers.ts';
5
9
  import { createWorkspaceRuntimeEnv } from './runtime.ts';
6
10
  import { readWorkspaceDescriptor } from './workspace.ts';
7
11
 
12
+ const MIGRATIONS_TABLE_ENV_KEY = 'DATABASE_MIGRATIONS_TABLE';
13
+ const DEFAULT_MIGRATIONS_TABLE = '_webstir_migrations';
14
+
8
15
  export interface RunBackendInspectOptions {
9
16
  readonly workspaceRoot: string;
10
17
  readonly env?: Record<string, string | undefined>;
@@ -14,12 +21,34 @@ export interface BackendInspectResult {
14
21
  readonly workspace: WorkspaceDescriptor;
15
22
  readonly buildRoot: string;
16
23
  readonly manifest: ModuleManifest;
24
+ readonly data: BackendDataInspectResult;
25
+ }
26
+
27
+ export interface BackendDataInspectResult {
28
+ readonly migrations: BackendMigrationInspectResult;
17
29
  }
18
30
 
19
- export async function runBackendInspect(options: RunBackendInspectOptions): Promise<BackendInspectResult> {
31
+ export interface BackendMigrationInspectResult {
32
+ readonly runnerPresent: boolean;
33
+ readonly runnerPath: string;
34
+ readonly migrationsDirectoryPresent: boolean;
35
+ readonly migrationsDirectory: string;
36
+ readonly migrationFilesCount: number;
37
+ readonly migrationFiles: readonly string[];
38
+ readonly exampleMigrationPresent: boolean;
39
+ readonly tableEnvKey: typeof MIGRATIONS_TABLE_ENV_KEY;
40
+ readonly configuredTable: string;
41
+ readonly defaultTable: typeof DEFAULT_MIGRATIONS_TABLE;
42
+ }
43
+
44
+ export async function runBackendInspect(
45
+ options: RunBackendInspectOptions,
46
+ ): Promise<BackendInspectResult> {
20
47
  const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
21
48
  if (workspace.mode !== 'api' && workspace.mode !== 'full') {
22
- throw new Error(`backend-inspect only supports api and full workspaces. Received mode "${workspace.mode}".`);
49
+ throw new Error(
50
+ `backend-inspect only supports api and full workspaces. Received mode "${workspace.mode}".`,
51
+ );
23
52
  }
24
53
 
25
54
  const provider = await loadProvider('backend');
@@ -41,5 +70,44 @@ export async function runBackendInspect(options: RunBackendInspectOptions): Prom
41
70
  workspace,
42
71
  buildRoot: resolvedWorkspace.buildRoot,
43
72
  manifest,
73
+ data: await inspectBackendData(workspace.root, options.env),
74
+ };
75
+ }
76
+
77
+ async function inspectBackendData(
78
+ workspaceRoot: string,
79
+ env: Record<string, string | undefined> | undefined,
80
+ ): Promise<BackendDataInspectResult> {
81
+ return {
82
+ migrations: await inspectMigrations(workspaceRoot, env),
83
+ };
84
+ }
85
+
86
+ async function inspectMigrations(
87
+ workspaceRoot: string,
88
+ env: Record<string, string | undefined> | undefined,
89
+ ): Promise<BackendMigrationInspectResult> {
90
+ const runtimeEnv = env ?? process.env;
91
+ const runnerPath = path.join('src', 'backend', 'db', 'migrate.ts');
92
+ const migrationsDirectory = path.join('src', 'backend', 'db', 'migrations');
93
+ const absoluteRunnerPath = path.join(workspaceRoot, runnerPath);
94
+ const absoluteMigrationsDirectory = path.join(workspaceRoot, migrationsDirectory);
95
+ const migrationFiles = existsSync(absoluteMigrationsDirectory)
96
+ ? (await readdir(absoluteMigrationsDirectory))
97
+ .filter((file) => /\.[cm]?[jt]s$/.test(file))
98
+ .sort()
99
+ : [];
100
+
101
+ return {
102
+ runnerPresent: existsSync(absoluteRunnerPath),
103
+ runnerPath,
104
+ migrationsDirectoryPresent: existsSync(absoluteMigrationsDirectory),
105
+ migrationsDirectory,
106
+ migrationFilesCount: migrationFiles.length,
107
+ migrationFiles,
108
+ exampleMigrationPresent: migrationFiles.includes('0001-example.ts'),
109
+ tableEnvKey: MIGRATIONS_TABLE_ENV_KEY,
110
+ configuredTable: runtimeEnv[MIGRATIONS_TABLE_ENV_KEY] ?? DEFAULT_MIGRATIONS_TABLE,
111
+ defaultTable: DEFAULT_MIGRATIONS_TABLE,
44
112
  };
45
113
  }
@@ -148,19 +148,27 @@ export class BackendRuntimeSupervisor {
148
148
 
149
149
  let ready = false;
150
150
 
151
- exitPromise.then((code) => {
152
- if (!ready) {
153
- return;
154
- } else if (!processRecord.expectedExit && !this.isStopping && this.current === processRecord) {
155
- this.io.stderr.write(`[webstir] backend runtime exited unexpectedly with code ${code ?? 'null'}.\n`);
156
- }
157
- }).finally(() => {
158
- if (this.current === processRecord) {
159
- this.current = undefined;
160
- }
161
- stdoutReader.close();
162
- stderrReader.close();
163
- });
151
+ exitPromise
152
+ .then((code) => {
153
+ if (!ready) {
154
+ return;
155
+ } else if (
156
+ !processRecord.expectedExit &&
157
+ !this.isStopping &&
158
+ this.current === processRecord
159
+ ) {
160
+ this.io.stderr.write(
161
+ `[webstir] backend runtime exited unexpectedly with code ${code ?? 'null'}.\n`,
162
+ );
163
+ }
164
+ })
165
+ .finally(() => {
166
+ if (this.current === processRecord) {
167
+ this.current = undefined;
168
+ }
169
+ stdoutReader.close();
170
+ stderrReader.close();
171
+ });
164
172
 
165
173
  try {
166
174
  await waitForRuntimeReady(port, exitPromise);
@@ -232,7 +240,7 @@ function normalizeDisplayHost(host: string): string {
232
240
 
233
241
  async function waitForRuntimeReady(
234
242
  port: number,
235
- exitPromise: Promise<number | null>
243
+ exitPromise: Promise<number | null>,
236
244
  ): Promise<void> {
237
245
  const abortController = new AbortController();
238
246
 
package/src/build.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type {
2
- CommandExecutionResult,
3
- } from './types.ts';
1
+ import type { CommandExecutionResult } from './types.ts';
4
2
  import { runCommand, type RunCommandOptions } from './execute.ts';
5
3
 
6
4
  export type RunBuildOptions = RunCommandOptions;