@geekmidas/cli 1.10.36 → 1.10.37

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 (82) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/{config-U-gojtxn.mjs → config-Bbu1JKvf.mjs} +2 -2
  3. package/dist/{config-U-gojtxn.mjs.map → config-Bbu1JKvf.mjs.map} +1 -1
  4. package/dist/{config-QnuOcYXp.cjs → config-DujV8DbR.cjs} +2 -2
  5. package/dist/{config-QnuOcYXp.cjs.map → config-DujV8DbR.cjs.map} +1 -1
  6. package/dist/config.cjs +2 -2
  7. package/dist/config.d.cts +1 -1
  8. package/dist/config.d.mts +1 -1
  9. package/dist/config.mjs +2 -2
  10. package/dist/{fullstack-secrets-Bchl2MDd.cjs → fullstack-secrets-BYzi6Hwl.cjs} +3 -2
  11. package/dist/{fullstack-secrets-Bchl2MDd.cjs.map → fullstack-secrets-BYzi6Hwl.cjs.map} +1 -1
  12. package/dist/{fullstack-secrets-P84v8zWn.mjs → fullstack-secrets-l8s3II_H.mjs} +3 -2
  13. package/dist/{fullstack-secrets-P84v8zWn.mjs.map → fullstack-secrets-l8s3II_H.mjs.map} +1 -1
  14. package/dist/{index-D7iT4dnv.d.mts → index-BBvGMG_A.d.mts} +124 -67
  15. package/dist/index-BBvGMG_A.d.mts.map +1 -0
  16. package/dist/{index-DRQq26DF.d.cts → index-Dz2a7xQU.d.cts} +124 -67
  17. package/dist/index-Dz2a7xQU.d.cts.map +1 -0
  18. package/dist/index.cjs +244 -91
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +244 -91
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-Bb4UEyZN.cjs → openapi-0JUzU5Tv.cjs} +9 -2
  23. package/dist/{openapi-Bb4UEyZN.cjs.map → openapi-0JUzU5Tv.cjs.map} +1 -1
  24. package/dist/{openapi-BA0e3I_s.mjs → openapi-C5AycKTR.mjs} +10 -3
  25. package/dist/{openapi-BA0e3I_s.mjs.map → openapi-C5AycKTR.mjs.map} +1 -1
  26. package/dist/openapi.cjs +3 -3
  27. package/dist/openapi.mjs +3 -3
  28. package/dist/{reconcile-DAAKa0YR.mjs → reconcile-CYGmqXvq.mjs} +2 -2
  29. package/dist/{reconcile-DAAKa0YR.mjs.map → reconcile-CYGmqXvq.mjs.map} +1 -1
  30. package/dist/{reconcile-CZtzdrnc.cjs → reconcile-D93YG7G5.cjs} +2 -2
  31. package/dist/{reconcile-CZtzdrnc.cjs.map → reconcile-D93YG7G5.cjs.map} +1 -1
  32. package/dist/workspace/index.cjs +1 -1
  33. package/dist/workspace/index.d.cts +1 -1
  34. package/dist/workspace/index.d.mts +1 -1
  35. package/dist/workspace/index.mjs +1 -1
  36. package/dist/{workspace-BobDOIG9.cjs → workspace-CQTT7HJQ.cjs} +34 -25
  37. package/dist/workspace-CQTT7HJQ.cjs.map +1 -0
  38. package/dist/{workspace-Cgmvgwh8.mjs → workspace-D-37Ylqo.mjs} +34 -25
  39. package/dist/workspace-D-37Ylqo.mjs.map +1 -0
  40. package/docs/dev-server-resilience-design.md +189 -0
  41. package/package.json +5 -5
  42. package/src/build/__tests__/workspace-build.spec.ts +7 -7
  43. package/src/build/index.ts +25 -9
  44. package/src/credentials/__tests__/helpers.ts +1 -1
  45. package/src/credentials/__tests__/workspaceCredentials.spec.ts +1 -1
  46. package/src/deploy/__tests__/domain.spec.ts +12 -12
  47. package/src/deploy/__tests__/env-resolver.spec.ts +1 -1
  48. package/src/deploy/__tests__/index.spec.ts +14 -14
  49. package/src/deploy/__tests__/sniffer.spec.ts +9 -9
  50. package/src/deploy/domain.ts +19 -11
  51. package/src/deploy/index.ts +12 -1
  52. package/src/deploy/sniffer.ts +1 -1
  53. package/src/deploy/types.ts +1 -1
  54. package/src/dev/__tests__/index.spec.ts +11 -11
  55. package/src/dev/index.ts +14 -8
  56. package/src/docker/__tests__/compose.spec.ts +4 -4
  57. package/src/docker/compose.ts +5 -3
  58. package/src/docker/index.ts +34 -6
  59. package/src/docker/templates.ts +138 -0
  60. package/src/generators/OpenApiTsGenerator.ts +22 -0
  61. package/src/generators/__tests__/OpenApiTsGenerator.registry.spec.ts +77 -0
  62. package/src/init/__tests__/init.spec.ts +1 -1
  63. package/src/init/generators/monorepo.ts +3 -3
  64. package/src/secrets/__tests__/reconcile.spec.ts +1 -1
  65. package/src/setup/__tests__/reconcile-secrets.spec.ts +2 -2
  66. package/src/setup/fullstack-secrets.ts +7 -1
  67. package/src/setup/index.ts +2 -1
  68. package/src/test/__tests__/__fixtures__/workspace.ts +1 -1
  69. package/src/test/__tests__/web.spec.ts +2 -2
  70. package/src/workspace/__tests__/client-generator.spec.ts +6 -6
  71. package/src/workspace/__tests__/index.spec.ts +4 -4
  72. package/src/workspace/__tests__/schema.spec.ts +27 -30
  73. package/src/workspace/__tests__/type-inference.spec.ts +5 -5
  74. package/src/workspace/client-generator.ts +10 -7
  75. package/src/workspace/index.ts +1 -1
  76. package/src/workspace/publicEnv.ts +2 -4
  77. package/src/workspace/schema.ts +126 -27
  78. package/src/workspace/types.ts +40 -69
  79. package/dist/index-D7iT4dnv.d.mts.map +0 -1
  80. package/dist/index-DRQq26DF.d.cts.map +0 -1
  81. package/dist/workspace-BobDOIG9.cjs.map +0 -1
  82. package/dist/workspace-Cgmvgwh8.mjs.map +0 -1
@@ -0,0 +1,189 @@
1
+ # Dev Server Resilience Design
2
+
3
+ Status: parked / not yet started
4
+ Owner: TBD
5
+ Scope: `packages/cli/src/dev/index.ts`, `packages/cli/src/generators/Generator.ts`
6
+
7
+ ## Background
8
+
9
+ Running `gkm dev` against a real workspace (e.g. `rezgo`: api, auth, web, app/Expo)
10
+ exposes three classes of failure. None are caused by user code; they are
11
+ limitations of the current dev orchestrator and hot-reload implementation.
12
+
13
+ ## Issue 1 — Hot reload misses new files and serves stale transitive modules
14
+
15
+ ### Symptoms
16
+
17
+ - Adding a new route file does not trigger a rebuild.
18
+ - After adding a new export to a non-route file, route files that import it
19
+ fail with:
20
+ `SyntaxError: The requested module '~/.../foo.ts' does not provide an export named 'bar'`
21
+ - Some changes "just don't get picked up" until the dev server is restarted.
22
+
23
+ ### Root causes
24
+
25
+ 1. **`add`/`unlink` events are ignored.** The chokidar watcher in `dev/index.ts`
26
+ only listens for `change`:
27
+ ```ts
28
+ watcher.on('change', async (path) => { ... });
29
+ ```
30
+ New or deleted files never trigger a rebuild.
31
+
32
+ 2. **Cache-bust is shallow.** `Generator.ts:load()` appends `?t=<now>` only to
33
+ the top-level globbed route file URL. Node's ESM cache is keyed by URL, so
34
+ any *transitive* import (services, domain modules, env, etc.) is served from
35
+ cache forever within the CLI process. When a transitive module is edited,
36
+ the route file is re-evaluated against the stale cached version → the
37
+ "does not provide an export named" error above.
38
+
39
+ 3. **Watch patterns are too narrow.** Only `routes`, `functions`, `crons`,
40
+ `subscribers` (plus envParser / logger / hooks files) are watched. Edits to
41
+ `src/domain/**`, `src/services/**`, etc. don't fire any rebuild even though
42
+ those files are pulled in transitively at openapi/build time.
43
+
44
+ ### Proposed fix
45
+
46
+ - Subscribe to `add` and `unlink` on the watcher and re-resolve the glob set
47
+ before rebuilding.
48
+ - Broaden the watch pattern to cover the app's `src/**/*.{ts,tsx}` (configurable),
49
+ not just construct entry directories.
50
+ - Bust transitive caches. Two viable approaches:
51
+ - **Worker thread per rebuild** (preferred): move `EndpointGenerator.load` /
52
+ `FunctionGenerator.load` / etc. into a short-lived `worker_threads` worker
53
+ that gets recreated on each rebuild. Each worker has a fresh module graph,
54
+ so transitive imports are guaranteed fresh. Cost is one worker spawn per
55
+ rebuild (~50-100ms), but avoids the unbounded-memory leak of accumulating
56
+ `?t=N` URLs in the main process.
57
+ - **Custom loader hook**: install a Node loader that rewrites every resolved
58
+ URL to include the current rebuild generation. Cheaper per rebuild but
59
+ fragile (loader compatibility with tsx, source maps, etc.).
60
+
61
+ Worker thread is the recommended path.
62
+
63
+ ## Issue 2 — Ctrl+C orphans `tsx`/`node` processes
64
+
65
+ ### Symptoms
66
+
67
+ After pressing Ctrl+C in a workspace `gkm dev`, the shell returns but
68
+ `tsx` and `node` processes remain alive. Workaround is `killall node`.
69
+
70
+ ### Process chain in workspace mode
71
+
72
+ ```
73
+ shell
74
+ └── gkm dev (top-level, workspace)
75
+ └── pnpm turbo run dev [spawned with detached: true]
76
+ ├── @rezgo/api: gkm dev (per-app)
77
+ │ └── tsx [spawned with detached: true]
78
+ │ └── node (Hono server)
79
+ ├── @rezgo/auth: ...
80
+ └── @rezgo/app: ...
81
+ ```
82
+
83
+ ### Root cause
84
+
85
+ The per-app `gkm dev` spawns `tsx` with `detached: true`
86
+ (`dev/index.ts:1521`). That moves tsx into a *new process group* outside the
87
+ turbo subtree. The workspace shutdown handler only sends SIGTERM/SIGKILL to
88
+ turbo's pgid; tsx's pgid is never targeted. After SIGKILL hits turbo's group,
89
+ the per-app gkm processes die *before* their async shutdown finishes calling
90
+ `process.kill(-tsxPgid, 'SIGKILL')`. tsx and its node child are orphaned.
91
+
92
+ The `detached: true` flag exists because the user's running Hono server may
93
+ ignore SIGTERM, so we need to kill its whole group. The design conflict is:
94
+ detaching tsx is necessary to forcibly kill it, but it also escapes the
95
+ top-level reachability tree.
96
+
97
+ ### Proposed fix
98
+
99
+ Track every tsx PID centrally:
100
+
101
+ - Each `DevServer` registers its `tsx` pid into a shared `Set<number>` at the
102
+ workspace level (or writes to a state file under `.gkm/`).
103
+ - Workspace shutdown iterates that set and issues `process.kill(-pid, 'SIGKILL')`
104
+ for each known tsx pgid *before* falling back to the turbo-pgid SIGKILL.
105
+ - As belt-and-suspenders, drain the registry on per-app `gkm dev` exit so dead
106
+ pids are not retargeted.
107
+
108
+ This makes the kill reachable regardless of process-group escape.
109
+
110
+ ## Issue 3 — One app's crash brings down the whole workspace
111
+
112
+ ### Symptoms
113
+
114
+ `@rezgo/app` (Expo) crashed with a Metro `DependencyGraph._onHasteChange`
115
+ TypeError. Within seconds, `@rezgo/api`, `@rezgo/auth`, `@rezgo/web` all
116
+ received SIGTERM and shut down. The user lost 21 minutes of warmed dev state.
117
+
118
+ ### Root causes
119
+
120
+ 1. **Turbo's persistent-task semantics.** When a persistent task in
121
+ `turbo run dev` exits non-zero, turbo cancels sibling persistent tasks. There
122
+ is no flag (as of turbo 2.x) that keeps survivors running through a sibling
123
+ crash. `--continue` only affects scheduling of *queued* tasks, not running
124
+ persistent ones.
125
+ 2. **gkm's orchestrator treats turbo's non-zero exit as fatal.** In
126
+ `workspaceDevCommand`:
127
+ ```ts
128
+ turboProcess.on('exit', (code) => {
129
+ if (code !== null && code !== 0) {
130
+ reject(new Error(`Turbo exited with code ${code}`));
131
+ }
132
+ });
133
+ ```
134
+ The reject fires the shutdown handler, which kills any survivors that
135
+ turbo hadn't already torn down.
136
+
137
+ ### Proposed fix
138
+
139
+ Stop using `turbo run dev` for the persistent dev loop. Build an in-process
140
+ supervisor inside `workspaceDevCommand`:
141
+
142
+ - Resolve dependency order via the existing `getAppBuildOrder` (we still use
143
+ turbo's task graph indirectly via workspace config).
144
+ - For each app, `spawn` its dev command directly (`pnpm dev` in that app's
145
+ cwd, or for backend apps invoke the per-app gkm dev codepath directly without
146
+ going through pnpm/turbo).
147
+ - Supervise each child:
148
+ - `on('exit', code)`: log the crash, do **not** propagate, schedule a restart
149
+ with exponential backoff (start at 1s, cap at 30s, reset after a 60s clean
150
+ run).
151
+ - Stream stdout/stderr to the parent with an app-name prefix so output stays
152
+ readable.
153
+ - Centralize the PID registry from Issue 2 here.
154
+ - Ctrl+C handler: iterate registry, SIGKILL each child group, then exit.
155
+
156
+ Turbo remains the build-time orchestrator (`gkm build`, dependency-ordered
157
+ package builds). Only the dev persistent loop is moved off it.
158
+
159
+ ### Why not fix turbo's behavior instead
160
+
161
+ Tried first. `--continue=dependencies-successful` and friends don't apply to
162
+ sibling persistent tasks. Filing it upstream is fine, but the dev experience
163
+ needs to be fixed now and the supervisor pattern is also what makes Issues 1
164
+ and 2 solvable cleanly.
165
+
166
+ ## Implementation order (when unparked)
167
+
168
+ 1. Land the in-process supervisor (Issue 3 fix). This gives us a single owner
169
+ of the dev process tree.
170
+ 2. Move tsx PID registration into the supervisor (Issue 2 fix).
171
+ 3. Move hot-reload (worker thread + broadened watcher + `add`/`unlink`) into
172
+ the per-app branch of the supervisor (Issue 1 fix).
173
+
174
+ Each step is shippable independently. Order matters: 1 unblocks the cleanest
175
+ form of 2 and 3.
176
+
177
+ ## Out of scope
178
+
179
+ - Replacing `tsx` (e.g. with `node --experimental-strip-types`).
180
+ - Native Bun runtime for dev — separate decision.
181
+ - Replacing chokidar.
182
+
183
+ ## Open questions
184
+
185
+ - Should the supervisor expose a `--no-restart` flag for CI-like local runs?
186
+ - Restart backoff thresholds — should they be configurable per app in
187
+ workspace config, or fixed?
188
+ - Should we surface a small status line ("3 of 4 apps running") when one app
189
+ is dead but the rest are healthy?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.36",
3
+ "version": "1.10.37",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,11 +56,11 @@
56
56
  "prompts": "~2.4.2",
57
57
  "tsx": "~4.20.3",
58
58
  "yaml": "~2.8.2",
59
- "@geekmidas/constructs": "~3.0.9",
60
- "@geekmidas/errors": "~1.0.0",
61
- "@geekmidas/schema": "~1.0.0",
59
+ "@geekmidas/constructs": "~3.0.10",
62
60
  "@geekmidas/envkit": "~1.0.6",
63
- "@geekmidas/logger": "~1.0.1"
61
+ "@geekmidas/errors": "~1.0.0",
62
+ "@geekmidas/logger": "~1.0.1",
63
+ "@geekmidas/schema": "~1.0.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/lodash.kebabcase": "^4.1.9",
@@ -103,13 +103,13 @@ describe('Workspace Build Command', () => {
103
103
  it('should have correct structure for failed build', () => {
104
104
  const result: AppBuildResult = {
105
105
  appName: 'web',
106
- type: 'frontend',
106
+ type: 'web',
107
107
  success: false,
108
108
  error: 'Build failed',
109
109
  };
110
110
 
111
111
  expect(result.appName).toBe('web');
112
- expect(result.type).toBe('frontend');
112
+ expect(result.type).toBe('web');
113
113
  expect(result.success).toBe(false);
114
114
  expect(result.error).toBe('Build failed');
115
115
  expect(result.outputPath).toBeUndefined();
@@ -137,14 +137,14 @@ describe('Workspace Build Command', () => {
137
137
  routes: './src/**/*.ts',
138
138
  },
139
139
  web: {
140
- type: 'frontend',
140
+ type: 'web',
141
141
  path: 'apps/web',
142
142
  port: 3002,
143
143
  dependencies: ['api', 'auth'],
144
144
  framework: 'nextjs',
145
145
  },
146
146
  admin: {
147
- type: 'frontend',
147
+ type: 'web',
148
148
  path: 'apps/admin',
149
149
  port: 3003,
150
150
  dependencies: ['api'],
@@ -159,7 +159,7 @@ describe('Workspace Build Command', () => {
159
159
 
160
160
  const apps = Object.entries(workspace.apps);
161
161
  const backendApps = apps.filter(([, app]) => app.type === 'backend');
162
- const frontendApps = apps.filter(([, app]) => app.type === 'frontend');
162
+ const frontendApps = apps.filter(([, app]) => app.type === 'web');
163
163
 
164
164
  expect(backendApps).toHaveLength(2);
165
165
  expect(frontendApps).toHaveLength(2);
@@ -181,7 +181,7 @@ describe('Workspace Build Command', () => {
181
181
  dependencies: [],
182
182
  },
183
183
  web: {
184
- type: 'frontend',
184
+ type: 'web',
185
185
  path: 'apps/web',
186
186
  port: 3001,
187
187
  dependencies: [],
@@ -207,7 +207,7 @@ describe('Workspace Build Command', () => {
207
207
  const frontendOutputPath = join(
208
208
  workspace.root,
209
209
  frontendApp.path,
210
- frontendApp.type === 'frontend' ? '.next' : '.gkm',
210
+ frontendApp.type === 'web' ? '.next' : '.gkm',
211
211
  );
212
212
  expect(frontendOutputPath).toBe('/workspace/apps/web/.next');
213
213
  });
@@ -362,7 +362,7 @@ async function buildForProvider(
362
362
  */
363
363
  export interface AppBuildResult {
364
364
  appName: string;
365
- type: 'backend' | 'frontend';
365
+ type: 'backend' | 'web' | 'mobile';
366
366
  success: boolean;
367
367
  outputPath?: string;
368
368
  error?: string;
@@ -415,7 +415,7 @@ export async function workspaceBuildCommand(
415
415
  const results: AppBuildResult[] = [];
416
416
  const apps = Object.entries(workspace.apps);
417
417
  const backendApps = apps.filter(([, app]) => app.type === 'backend');
418
- const frontendApps = apps.filter(([, app]) => app.type === 'frontend');
418
+ const frontendApps = apps.filter(([, app]) => app.type === 'web');
419
419
 
420
420
  logger.log(`\n🏗️ Building workspace: ${workspace.name}`);
421
421
  logger.log(
@@ -486,7 +486,12 @@ export async function workspaceBuildCommand(
486
486
  // Summary
487
487
  logger.log(`\n📋 Build Summary:`);
488
488
  for (const result of results) {
489
- const icon = result.type === 'backend' ? '⚙️' : '🌐';
489
+ const icon =
490
+ result.type === 'backend'
491
+ ? '⚙️'
492
+ : result.type === 'mobile'
493
+ ? '📱'
494
+ : '🌐';
490
495
  logger.log(
491
496
  ` ${icon} ${result.appName}: ${result.outputPath || 'built'}`,
492
497
  );
@@ -522,13 +527,24 @@ function getAppOutputPath(
522
527
  ): string {
523
528
  const appPath = join(workspace.root, app.path);
524
529
 
525
- if (app.type === 'frontend') {
526
- // Next.js standalone output
527
- return join(appPath, '.next');
528
- } else {
529
- // Backend .gkm output
530
- return join(appPath, '.gkm');
530
+ if (app.type === 'mobile') {
531
+ // Mobile builds are produced by the framework's own toolchain
532
+ // (e.g. EAS Build) — no local output path.
533
+ return '';
534
+ }
535
+ if (app.type === 'web') {
536
+ switch (app.framework) {
537
+ case 'vite':
538
+ case 'tanstack-start':
539
+ return join(appPath, 'dist');
540
+ case 'remix':
541
+ return join(appPath, 'build');
542
+ default:
543
+ return join(appPath, '.next');
544
+ }
531
545
  }
546
+ // Backend .gkm output
547
+ return join(appPath, '.gkm');
532
548
  }
533
549
 
534
550
  /**
@@ -76,7 +76,7 @@ export function createWorkspaceConfig(
76
76
  const deps = app.dependencies
77
77
  ? `dependencies: [${app.dependencies.map((d) => `'${d}'`).join(', ')}],`
78
78
  : '';
79
- if (app.type === 'frontend') {
79
+ if (app.type === 'web') {
80
80
  const framework = app.framework ?? 'nextjs';
81
81
  return ` ${name}: {
82
82
  type: '${app.type}',
@@ -45,7 +45,7 @@ describe('workspace credentials', () => {
45
45
  {
46
46
  api: { type: 'backend', path: 'apps/api', port: 3001 },
47
47
  web: {
48
- type: 'frontend',
48
+ type: 'web',
49
49
  path: 'apps/web',
50
50
  port: 3002,
51
51
  dependencies: ['api'],
@@ -55,7 +55,7 @@ describe('resolveHost', () => {
55
55
  });
56
56
 
57
57
  it('should return base domain for main frontend app', () => {
58
- const app = createApp({ type: 'frontend' });
58
+ const app = createApp({ type: 'web' });
59
59
  const host = resolveHost('web', app, 'production', dokployConfig, true);
60
60
  expect(host).toBe('myapp.com');
61
61
  });
@@ -100,7 +100,7 @@ describe('resolveHost', () => {
100
100
  });
101
101
 
102
102
  describe('isMainFrontendApp', () => {
103
- const createApp = (type: 'backend' | 'frontend'): NormalizedAppConfig => ({
103
+ const createApp = (type: 'backend' | 'web'): NormalizedAppConfig => ({
104
104
  type,
105
105
  path: 'apps/test',
106
106
  port: 3000,
@@ -111,7 +111,7 @@ describe('isMainFrontendApp', () => {
111
111
  it('should return false for backend apps', () => {
112
112
  const apps = {
113
113
  api: createApp('backend'),
114
- web: createApp('frontend'),
114
+ web: createApp('web'),
115
115
  };
116
116
  expect(isMainFrontendApp('api', apps.api, apps)).toBe(false);
117
117
  });
@@ -119,8 +119,8 @@ describe('isMainFrontendApp', () => {
119
119
  it('should return true for app named "web" if it is frontend', () => {
120
120
  const apps = {
121
121
  api: createApp('backend'),
122
- web: createApp('frontend'),
123
- admin: createApp('frontend'),
122
+ web: createApp('web'),
123
+ admin: createApp('web'),
124
124
  };
125
125
  expect(isMainFrontendApp('web', apps.web, apps)).toBe(true);
126
126
  });
@@ -128,8 +128,8 @@ describe('isMainFrontendApp', () => {
128
128
  it('should return true for first frontend app when no "web" app', () => {
129
129
  const apps = {
130
130
  api: createApp('backend'),
131
- dashboard: createApp('frontend'),
132
- admin: createApp('frontend'),
131
+ dashboard: createApp('web'),
132
+ admin: createApp('web'),
133
133
  };
134
134
  expect(isMainFrontendApp('dashboard', apps.dashboard, apps)).toBe(true);
135
135
  expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
@@ -138,22 +138,22 @@ describe('isMainFrontendApp', () => {
138
138
  it('should return false for non-first frontend when no "web" app', () => {
139
139
  const apps = {
140
140
  api: createApp('backend'),
141
- dashboard: createApp('frontend'),
142
- admin: createApp('frontend'),
141
+ dashboard: createApp('web'),
142
+ admin: createApp('web'),
143
143
  };
144
144
  expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
145
145
  });
146
146
 
147
147
  it('should return false when no frontend apps in allApps (edge case)', () => {
148
148
  // Edge case: checking a frontend app against an empty allApps
149
- const frontendApp = createApp('frontend');
149
+ const frontendApp = createApp('web');
150
150
  expect(isMainFrontendApp('myapp', frontendApp, {})).toBe(false);
151
151
  });
152
152
  });
153
153
 
154
154
  describe('generatePublicUrlBuildArgs', () => {
155
155
  const createApp = (dependencies: string[]): NormalizedAppConfig => ({
156
- type: 'frontend',
156
+ type: 'web',
157
157
  path: 'apps/web',
158
158
  port: 3001,
159
159
  dependencies,
@@ -214,7 +214,7 @@ describe('generatePublicUrlBuildArgs', () => {
214
214
 
215
215
  describe('getPublicUrlArgNames', () => {
216
216
  const createApp = (dependencies: string[]): NormalizedAppConfig => ({
217
- type: 'frontend',
217
+ type: 'web',
218
218
  path: 'apps/web',
219
219
  port: 3001,
220
220
  dependencies,
@@ -738,7 +738,7 @@ describe('Docker build arg extraction', () => {
738
738
  overrides: Partial<EnvResolverContext> = {},
739
739
  ): EnvResolverContext => ({
740
740
  app: {
741
- type: 'frontend',
741
+ type: 'web',
742
742
  path: 'apps/web',
743
743
  port: 3001,
744
744
  dependencies: ['api', 'auth'],
@@ -430,7 +430,7 @@ describe('workspaceDeployCommand', () => {
430
430
  resolvedDeployTarget: 'dokploy',
431
431
  },
432
432
  web: {
433
- type: 'frontend',
433
+ type: 'web',
434
434
  path: 'apps/web',
435
435
  port: 3001,
436
436
  dependencies: ['api'],
@@ -527,7 +527,7 @@ describe('workspaceDeployCommand', () => {
527
527
  resolvedDeployTarget: 'dokploy',
528
528
  },
529
529
  web: {
530
- type: 'frontend',
530
+ type: 'web',
531
531
  path: 'apps/web',
532
532
  port: 3002,
533
533
  dependencies: ['api', 'auth'],
@@ -566,7 +566,7 @@ describe('workspaceDeployCommand', () => {
566
566
  resolvedDeployTarget: 'dokploy',
567
567
  },
568
568
  web: {
569
- type: 'frontend',
569
+ type: 'web',
570
570
  path: 'apps/web',
571
571
  port: 3002,
572
572
  dependencies: ['api'],
@@ -614,7 +614,7 @@ describe('workspaceDeployCommand', () => {
614
614
  };
615
615
 
616
616
  const app = {
617
- type: 'frontend' as const,
617
+ type: 'web' as const,
618
618
  path: 'apps/web',
619
619
  port: 3000,
620
620
  dependencies: ['api', 'auth'],
@@ -645,7 +645,7 @@ describe('workspaceDeployCommand', () => {
645
645
  };
646
646
 
647
647
  const app = {
648
- type: 'frontend' as const,
648
+ type: 'web' as const,
649
649
  path: 'apps/web',
650
650
  port: 3000,
651
651
  dependencies: ['api', 'auth'], // wants both api and auth
@@ -719,7 +719,7 @@ describe('workspaceDeployCommand', () => {
719
719
  it('should inject DATABASE_URL for backend apps', () => {
720
720
  const workspaceName = 'test-workspace';
721
721
  const hasPostgres = true;
722
- const appType: 'backend' | 'frontend' = 'backend';
722
+ const appType: 'backend' | 'web' = 'backend';
723
723
 
724
724
  const envVars: string[] = [];
725
725
 
@@ -735,7 +735,7 @@ describe('workspaceDeployCommand', () => {
735
735
 
736
736
  it('should not inject DATABASE_URL for frontend apps', () => {
737
737
  const hasPostgres = true;
738
- const appType = 'frontend' as 'backend' | 'frontend';
738
+ const appType = 'web' as 'backend' | 'web';
739
739
 
740
740
  const envVars: string[] = [];
741
741
 
@@ -794,7 +794,7 @@ describe('workspaceDeployCommand', () => {
794
794
  it('should have correct structure for failed AppDeployResult', () => {
795
795
  const failedResult = {
796
796
  appName: 'web',
797
- type: 'frontend' as const,
797
+ type: 'web' as const,
798
798
  success: false,
799
799
  error: 'Build failed',
800
800
  };
@@ -809,7 +809,7 @@ describe('workspaceDeployCommand', () => {
809
809
  { appName: 'api', type: 'backend' as const, success: true },
810
810
  {
811
811
  appName: 'web',
812
- type: 'frontend' as const,
812
+ type: 'web' as const,
813
813
  success: false,
814
814
  error: 'Failed',
815
815
  },
@@ -834,7 +834,7 @@ describe('workspaceDeployCommand', () => {
834
834
  };
835
835
 
836
836
  const webApp = {
837
- type: 'frontend' as const,
837
+ type: 'web' as const,
838
838
  path: 'apps/web',
839
839
  port: 3000,
840
840
  dependencies: ['api', 'auth'],
@@ -873,7 +873,7 @@ describe('workspaceDeployCommand', () => {
873
873
  };
874
874
 
875
875
  const webApp = {
876
- type: 'frontend' as const,
876
+ type: 'web' as const,
877
877
  path: 'apps/web',
878
878
  port: 3001,
879
879
  dependencies: ['api', 'payments'],
@@ -922,7 +922,7 @@ describe('workspaceDeployCommand', () => {
922
922
  };
923
923
 
924
924
  const webApp = {
925
- type: 'frontend' as const,
925
+ type: 'web' as const,
926
926
  path: 'apps/web',
927
927
  port: 3001,
928
928
  dependencies: ['api', 'auth'],
@@ -997,7 +997,7 @@ describe('workspaceDeployCommand', () => {
997
997
 
998
998
  // Step 2: Deploy web (depends on api)
999
999
  const webApp = {
1000
- type: 'frontend' as const,
1000
+ type: 'web' as const,
1001
1001
  path: 'apps/web',
1002
1002
  port: 3001,
1003
1003
  dependencies: ['api'],
@@ -1048,7 +1048,7 @@ describe('workspaceDeployCommand', () => {
1048
1048
  };
1049
1049
 
1050
1050
  const webApp = {
1051
- type: 'frontend' as const,
1051
+ type: 'web' as const,
1052
1052
  path: 'apps/web',
1053
1053
  port: 3000,
1054
1054
  dependencies: ['api', 'auth', 'notifications'],
@@ -32,7 +32,7 @@ describe('sniffAppEnvironment', () => {
32
32
 
33
33
  describe('frontend apps', () => {
34
34
  it('should return empty env vars for frontend apps with no dependencies', async () => {
35
- const app = createApp({ type: 'frontend', dependencies: [] });
35
+ const app = createApp({ type: 'web', dependencies: [] });
36
36
 
37
37
  const result = await sniffAppEnvironment(app, 'web', workspacePath);
38
38
 
@@ -42,7 +42,7 @@ describe('sniffAppEnvironment', () => {
42
42
 
43
43
  it('should return NEXT_PUBLIC_{DEP}_URL for frontend dependencies', async () => {
44
44
  const app = createApp({
45
- type: 'frontend',
45
+ type: 'web',
46
46
  dependencies: ['api', 'auth'],
47
47
  });
48
48
 
@@ -56,7 +56,7 @@ describe('sniffAppEnvironment', () => {
56
56
 
57
57
  it('should generate uppercase dep names in NEXT_PUBLIC_{DEP}_URL', async () => {
58
58
  const app = createApp({
59
- type: 'frontend',
59
+ type: 'web',
60
60
  dependencies: ['payments-service', 'notification_api'],
61
61
  });
62
62
 
@@ -73,7 +73,7 @@ describe('sniffAppEnvironment', () => {
73
73
  describe('config sniffing', () => {
74
74
  it('should sniff env vars from config.client path', async () => {
75
75
  const app = createApp({
76
- type: 'frontend',
76
+ type: 'web',
77
77
  path: fixturesPath,
78
78
  dependencies: [],
79
79
  config: {
@@ -90,7 +90,7 @@ describe('sniffAppEnvironment', () => {
90
90
 
91
91
  it('should sniff env vars from config.server path', async () => {
92
92
  const app = createApp({
93
- type: 'frontend',
93
+ type: 'web',
94
94
  path: fixturesPath,
95
95
  dependencies: [],
96
96
  config: {
@@ -107,7 +107,7 @@ describe('sniffAppEnvironment', () => {
107
107
 
108
108
  it('should combine vars from both config.client and config.server', async () => {
109
109
  const app = createApp({
110
- type: 'frontend',
110
+ type: 'web',
111
111
  path: fixturesPath,
112
112
  dependencies: ['api'],
113
113
  config: {
@@ -129,7 +129,7 @@ describe('sniffAppEnvironment', () => {
129
129
 
130
130
  it('should deduplicate vars from both config files', async () => {
131
131
  const app = createApp({
132
- type: 'frontend',
132
+ type: 'web',
133
133
  path: fixturesPath,
134
134
  dependencies: [],
135
135
  config: {
@@ -306,7 +306,7 @@ describe('sniffAppEnvironment', () => {
306
306
 
307
307
  it('returns empty optionalEnvVars for frontend apps', async () => {
308
308
  const app = createApp({
309
- type: 'frontend',
309
+ type: 'web',
310
310
  dependencies: ['api'],
311
311
  });
312
312
 
@@ -339,7 +339,7 @@ describe('sniffAllApps', () => {
339
339
  // No entry, routes, or envParser - will return empty
340
340
  },
341
341
  web: {
342
- type: 'frontend',
342
+ type: 'web',
343
343
  path: 'apps/web',
344
344
  port: 3001,
345
345
  dependencies: ['api', 'auth'],