zdev 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2461,8 +2461,65 @@ Next steps:`);
2461
2461
  }
2462
2462
 
2463
2463
  // src/commands/start.ts
2464
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2464
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
2465
2465
  import { resolve as resolve4, basename as basename3, join as join3 } from "path";
2466
+
2467
+ // src/vite-patch.ts
2468
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2469
+ function patchViteAllowedHosts(viteConfigPath, devDomain) {
2470
+ if (!devDomain) {
2471
+ return { patched: false, reason: "no devDomain configured" };
2472
+ }
2473
+ let content;
2474
+ try {
2475
+ content = readFileSync4(viteConfigPath, "utf-8");
2476
+ } catch {
2477
+ return { patched: false, reason: "could not read vite config" };
2478
+ }
2479
+ const domainPattern = `.${devDomain}`;
2480
+ if (content.includes(domainPattern)) {
2481
+ return { patched: false, reason: "domain already present" };
2482
+ }
2483
+ if (/allowedHosts\s*:\s*true/.test(content)) {
2484
+ return { patched: false, reason: "already allows all hosts (true)" };
2485
+ }
2486
+ if (/allowedHosts\s*:\s*["']all["']/.test(content)) {
2487
+ return { patched: false, reason: 'already allows all hosts ("all")' };
2488
+ }
2489
+ const entry = `"${domainPattern}"`;
2490
+ let patched = false;
2491
+ if (/allowedHosts\s*:\s*\[/.test(content)) {
2492
+ content = content.replace(/(allowedHosts\s*:\s*\[)/, `$1${entry}, `);
2493
+ patched = true;
2494
+ } else if (/server\s*:\s*\{/.test(content)) {
2495
+ content = content.replace(/(server\s*:\s*\{)/, `$1
2496
+ allowedHosts: [${entry}],`);
2497
+ patched = true;
2498
+ } else if (/defineConfig\s*\(\s*\{/.test(content)) {
2499
+ content = content.replace(/(defineConfig\s*\(\s*\{)/, `$1
2500
+ server: {
2501
+ allowedHosts: [${entry}],
2502
+ },`);
2503
+ patched = true;
2504
+ } else if (/export\s+default\s*\{/.test(content)) {
2505
+ content = content.replace(/(export\s+default\s*\{)/, `$1
2506
+ server: {
2507
+ allowedHosts: [${entry}],
2508
+ },`);
2509
+ patched = true;
2510
+ }
2511
+ if (!patched) {
2512
+ return { patched: false, reason: "unrecognized config format" };
2513
+ }
2514
+ try {
2515
+ writeFileSync5(viteConfigPath, content);
2516
+ } catch {
2517
+ return { patched: false, reason: "could not write vite config" };
2518
+ }
2519
+ return { patched: true, reason: "added allowedHosts" };
2520
+ }
2521
+
2522
+ // src/commands/start.ts
2466
2523
  function detectWebDir(worktreePath) {
2467
2524
  const commonDirs = ["web", "frontend", "app", "client", "packages/web", "apps/web"];
2468
2525
  for (const dir of commonDirs) {
@@ -2548,8 +2605,8 @@ async function start(featureName, projectPath = ".", options = {}) {
2548
2605
  const destPath = join3(webPath, pattern);
2549
2606
  if (existsSync5(srcPath) && !existsSync5(destPath)) {
2550
2607
  try {
2551
- const content = readFileSync4(srcPath);
2552
- writeFileSync5(destPath, content);
2608
+ const content = readFileSync5(srcPath);
2609
+ writeFileSync6(destPath, content);
2553
2610
  console.log(` Copied ${pattern}`);
2554
2611
  } catch (e) {
2555
2612
  console.log(` Could not copy ${pattern}`);
@@ -2604,30 +2661,13 @@ async function start(featureName, projectPath = ".", options = {}) {
2604
2661
  const viteConfigTsPath = join3(webPath, "vite.config.ts");
2605
2662
  const viteConfigJsPath = join3(webPath, "vite.config.js");
2606
2663
  const viteConfigPath = existsSync5(viteConfigTsPath) ? viteConfigTsPath : existsSync5(viteConfigJsPath) ? viteConfigJsPath : null;
2607
- if (viteConfigPath) {
2608
- try {
2609
- let viteConfig = readFileSync4(viteConfigPath, "utf-8");
2610
- if (!viteConfig.includes("allowedHosts")) {
2611
- let patched = false;
2612
- if (viteConfig.includes("server:") || viteConfig.includes("server :")) {
2613
- viteConfig = viteConfig.replace(/server\s*:\s*\{/, `server: {
2614
- allowedHosts: true,`);
2615
- patched = true;
2616
- } else if (viteConfig.includes("defineConfig({")) {
2617
- viteConfig = viteConfig.replace(/defineConfig\(\{/, `defineConfig({
2618
- server: {
2619
- allowedHosts: true,
2620
- },`);
2621
- patched = true;
2622
- }
2623
- if (patched) {
2624
- writeFileSync5(viteConfigPath, viteConfig);
2625
- console.log(` Patched ${basename3(viteConfigPath)} for external access`);
2626
- run("git", ["update-index", "--skip-worktree", basename3(viteConfigPath)], { cwd: webPath });
2627
- }
2628
- }
2629
- } catch (e) {
2630
- console.log(` Could not patch vite config (non-critical)`);
2664
+ if (viteConfigPath && config.devDomain) {
2665
+ const result = patchViteAllowedHosts(viteConfigPath, config.devDomain);
2666
+ if (result.patched) {
2667
+ console.log(` Patched ${basename3(viteConfigPath)}: ${result.reason}`);
2668
+ run("git", ["update-index", "--skip-worktree", basename3(viteConfigPath)], { cwd: webPath });
2669
+ } else {
2670
+ console.log(` Vite config: ${result.reason}`);
2631
2671
  }
2632
2672
  }
2633
2673
  console.log(`
@@ -2773,8 +2813,10 @@ function getTmuxSessions(pattern) {
2773
2813
  }
2774
2814
  if (!result.success)
2775
2815
  return [];
2776
- return result.stdout.split(`
2777
- `).filter(Boolean).filter((name) => name.toLowerCase().includes(pattern.toLowerCase()));
2816
+ const sessions = result.stdout.split(`
2817
+ `).filter(Boolean);
2818
+ const patternWords = pattern.split("-").filter((w) => w.length > 2);
2819
+ return sessions.filter((name) => patternWords.some((word) => name.toLowerCase().includes(word.toLowerCase())));
2778
2820
  }
2779
2821
  async function list(options = {}) {
2780
2822
  const config = loadConfig();
@@ -2811,7 +2853,15 @@ No active features.
2811
2853
  const frontendRunning = alloc.pids.frontend ? isProcessRunning(alloc.pids.frontend) : false;
2812
2854
  const convexRunning = alloc.pids.convex ? isProcessRunning(alloc.pids.convex) : false;
2813
2855
  const featureSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
2814
- const tmuxSessions = getTmuxSessions(featureSlug);
2856
+ const projectSlug = alloc.project.toLowerCase().replace(/[^a-z0-9]/g, "-");
2857
+ const featureOnly = alloc.branch.replace("feature/", "").toLowerCase().replace(/[^a-z0-9]/g, "-");
2858
+ let tmuxSessions = getTmuxSessions(featureSlug);
2859
+ if (tmuxSessions.length === 0) {
2860
+ tmuxSessions = getTmuxSessions(projectSlug);
2861
+ }
2862
+ if (tmuxSessions.length === 0 && featureOnly !== projectSlug) {
2863
+ tmuxSessions = getTmuxSessions(featureOnly);
2864
+ }
2815
2865
  const hasTmux = tmuxSessions.length > 0;
2816
2866
  const isRunning = frontendRunning || convexRunning || hasTmux;
2817
2867
  const isFullyRunning = frontendRunning && convexRunning || hasTmux && tmuxSessions.length >= 2;
@@ -3383,11 +3433,11 @@ ${diffStat}
3383
3433
  }
3384
3434
 
3385
3435
  // src/index.ts
3386
- import { readFileSync as readFileSync5 } from "fs";
3436
+ import { readFileSync as readFileSync6 } from "fs";
3387
3437
  import { fileURLToPath } from "url";
3388
3438
  import { dirname, join as join4 } from "path";
3389
3439
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
3390
- var pkg = JSON.parse(readFileSync5(join4(__dirname2, "..", "package.json"), "utf-8"));
3440
+ var pkg = JSON.parse(readFileSync6(join4(__dirname2, "..", "package.json"), "utf-8"));
3391
3441
  var program2 = new Command;
3392
3442
  program2.name("zdev").description(`\uD83D\uDC02 zdev v${pkg.version} - Multi-agent worktree development environment`).version(pkg.version);
3393
3443
  program2.command("create <name>").description("Create a new TanStack Start project").option("--convex", "Add Convex backend integration").option("--flat", "Flat structure (no monorepo)").action(async (name, options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdev",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Multi-agent worktree development environment for cloud dev with preview URLs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,10 +22,14 @@ function getTmuxSessions(pattern: string): string[] {
22
22
 
23
23
  if (!result.success) return [];
24
24
 
25
- return result.stdout
26
- .split("\n")
27
- .filter(Boolean)
28
- .filter(name => name.toLowerCase().includes(pattern.toLowerCase()));
25
+ const sessions = result.stdout.split("\n").filter(Boolean);
26
+
27
+ // Match if session name contains any word from the pattern
28
+ const patternWords = pattern.split("-").filter(w => w.length > 2);
29
+
30
+ return sessions.filter(name =>
31
+ patternWords.some(word => name.toLowerCase().includes(word.toLowerCase()))
32
+ );
29
33
  }
30
34
 
31
35
  export interface ListOptions {
@@ -74,8 +78,18 @@ export async function list(options: ListOptions = {}): Promise<void> {
74
78
  : false;
75
79
 
76
80
  // Check for tmux sessions related to this feature
81
+ // Try multiple patterns: full name, project name, feature name
77
82
  const featureSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
78
- const tmuxSessions = getTmuxSessions(featureSlug);
83
+ const projectSlug = alloc.project.toLowerCase().replace(/[^a-z0-9]/g, "-");
84
+ const featureOnly = alloc.branch.replace("feature/", "").toLowerCase().replace(/[^a-z0-9]/g, "-");
85
+
86
+ let tmuxSessions = getTmuxSessions(featureSlug);
87
+ if (tmuxSessions.length === 0) {
88
+ tmuxSessions = getTmuxSessions(projectSlug);
89
+ }
90
+ if (tmuxSessions.length === 0 && featureOnly !== projectSlug) {
91
+ tmuxSessions = getTmuxSessions(featureOnly);
92
+ }
79
93
  const hasTmux = tmuxSessions.length > 0;
80
94
 
81
95
  const isRunning = frontendRunning || convexRunning || hasTmux;
@@ -1,5 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
2
  import { resolve, basename, join } from "path";
3
+ import { patchViteAllowedHosts } from "../vite-patch.js";
3
4
  import {
4
5
  isGitRepo,
5
6
  getRepoName,
@@ -219,41 +220,14 @@ export async function start(
219
220
  const viteConfigPath = existsSync(viteConfigTsPath) ? viteConfigTsPath :
220
221
  existsSync(viteConfigJsPath) ? viteConfigJsPath : null;
221
222
 
222
- if (viteConfigPath) {
223
- try {
224
- let viteConfig = readFileSync(viteConfigPath, "utf-8");
225
-
226
- // Only patch if allowedHosts not already present
227
- if (!viteConfig.includes("allowedHosts")) {
228
- let patched = false;
229
-
230
- // Try to add allowedHosts to existing server block
231
- if (viteConfig.includes("server:") || viteConfig.includes("server :")) {
232
- // Add allowedHosts inside existing server block
233
- viteConfig = viteConfig.replace(
234
- /server\s*:\s*\{/,
235
- "server: {\n allowedHosts: true,"
236
- );
237
- patched = true;
238
- } else if (viteConfig.includes("defineConfig({")) {
239
- // No server block, add new one
240
- viteConfig = viteConfig.replace(
241
- /defineConfig\(\{/,
242
- "defineConfig({\n server: {\n allowedHosts: true,\n },"
243
- );
244
- patched = true;
245
- }
246
-
247
- if (patched) {
248
- writeFileSync(viteConfigPath, viteConfig);
249
- console.log(` Patched ${basename(viteConfigPath)} for external access`);
250
-
251
- // Mark file as skip-worktree so it won't be committed
252
- run("git", ["update-index", "--skip-worktree", basename(viteConfigPath)], { cwd: webPath });
253
- }
254
- }
255
- } catch (e) {
256
- console.log(` Could not patch vite config (non-critical)`);
223
+ if (viteConfigPath && config.devDomain) {
224
+ const result = patchViteAllowedHosts(viteConfigPath, config.devDomain);
225
+ if (result.patched) {
226
+ console.log(` Patched ${basename(viteConfigPath)}: ${result.reason}`);
227
+ // Mark file as skip-worktree so it won't be committed
228
+ run("git", ["update-index", "--skip-worktree", basename(viteConfigPath)], { cwd: webPath });
229
+ } else {
230
+ console.log(` Vite config: ${result.reason}`);
257
231
  }
258
232
  }
259
233
 
@@ -0,0 +1,271 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
+ import { patchViteAllowedHosts } from "./vite-patch.js";
3
+ import { writeFileSync, readFileSync, mkdtempSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+
7
+ const DEV_DOMAIN = "dev.web3citadel.com";
8
+ const DOMAIN_PATTERN = ".dev.web3citadel.com";
9
+
10
+ let tmpDir: string;
11
+ let configPath: string;
12
+
13
+ beforeEach(() => {
14
+ tmpDir = mkdtempSync(join(tmpdir(), "vite-patch-test-"));
15
+ configPath = join(tmpDir, "vite.config.ts");
16
+ });
17
+
18
+ afterEach(() => {
19
+ rmSync(tmpDir, { recursive: true, force: true });
20
+ });
21
+
22
+ function writeConfig(content: string): string {
23
+ writeFileSync(configPath, content);
24
+ return configPath;
25
+ }
26
+
27
+ function readConfig(): string {
28
+ return readFileSync(configPath, "utf-8");
29
+ }
30
+
31
+ describe("patchViteAllowedHosts", () => {
32
+ // ── Skip conditions ─────────────────────────────────────────────
33
+
34
+ it("skips when devDomain is empty", () => {
35
+ writeConfig(`export default defineConfig({ plugins: [] })`);
36
+ const result = patchViteAllowedHosts(configPath, "");
37
+ expect(result.patched).toBe(false);
38
+ expect(result.reason).toBe("no devDomain configured");
39
+ });
40
+
41
+ it("skips when file cannot be read", () => {
42
+ const result = patchViteAllowedHosts("/nonexistent/vite.config.ts", DEV_DOMAIN);
43
+ expect(result.patched).toBe(false);
44
+ expect(result.reason).toBe("could not read vite config");
45
+ });
46
+
47
+ it("skips when domain pattern already present", () => {
48
+ writeConfig(`
49
+ export default defineConfig({
50
+ server: {
51
+ allowedHosts: ["${DOMAIN_PATTERN}"],
52
+ },
53
+ plugins: [],
54
+ })`);
55
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
56
+ expect(result.patched).toBe(false);
57
+ expect(result.reason).toBe("domain already present");
58
+ });
59
+
60
+ it("skips when allowedHosts is true", () => {
61
+ writeConfig(`
62
+ export default defineConfig({
63
+ server: {
64
+ allowedHosts: true,
65
+ },
66
+ plugins: [],
67
+ })`);
68
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
69
+ expect(result.patched).toBe(false);
70
+ expect(result.reason).toBe("already allows all hosts (true)");
71
+ });
72
+
73
+ it('skips when allowedHosts is "all"', () => {
74
+ writeConfig(`
75
+ export default defineConfig({
76
+ server: {
77
+ allowedHosts: "all",
78
+ },
79
+ plugins: [],
80
+ })`);
81
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
82
+ expect(result.patched).toBe(false);
83
+ expect(result.reason).toBe('already allows all hosts ("all")');
84
+ });
85
+
86
+ it("returns unrecognized for non-matching format", () => {
87
+ writeConfig(`// just a comment, no config object`);
88
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
89
+ expect(result.patched).toBe(false);
90
+ expect(result.reason).toBe("unrecognized config format");
91
+ });
92
+
93
+ // ── Inline defineConfig ─────────────────────────────────────────
94
+
95
+ it("injects server block into inline defineConfig", () => {
96
+ writeConfig(`import { defineConfig } from "vite"
97
+ import react from "@vitejs/plugin-react"
98
+
99
+ export default defineConfig({
100
+ plugins: [react()],
101
+ })`);
102
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
103
+ expect(result.patched).toBe(true);
104
+ const output = readConfig();
105
+ expect(output).toContain(`server: {`);
106
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
107
+ expect(output).toContain(`plugins: [react()]`);
108
+ });
109
+
110
+ // ── Variable-assigned defineConfig ──────────────────────────────
111
+
112
+ it("injects server block into variable-assigned defineConfig", () => {
113
+ writeConfig(`import { defineConfig } from 'vite'
114
+ import viteReact from '@vitejs/plugin-react'
115
+
116
+ const config = defineConfig({
117
+ plugins: [
118
+ viteReact(),
119
+ ],
120
+ })
121
+
122
+ export default config`);
123
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
124
+ expect(result.patched).toBe(true);
125
+ const output = readConfig();
126
+ expect(output).toContain(`server: {`);
127
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
128
+ expect(output).toContain(`export default config`);
129
+ });
130
+
131
+ // ── Plain export default { } ────────────────────────────────────
132
+
133
+ it("injects server block into plain export default object", () => {
134
+ writeConfig(`export default {
135
+ plugins: [],
136
+ }`);
137
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
138
+ expect(result.patched).toBe(true);
139
+ const output = readConfig();
140
+ expect(output).toContain(`server: {`);
141
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
142
+ });
143
+
144
+ // ── Existing server block, no allowedHosts ──────────────────────
145
+
146
+ it("merges allowedHosts into existing server block", () => {
147
+ writeConfig(`export default defineConfig({
148
+ server: {
149
+ port: 3000,
150
+ host: "0.0.0.0",
151
+ },
152
+ plugins: [],
153
+ })`);
154
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
155
+ expect(result.patched).toBe(true);
156
+ const output = readConfig();
157
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
158
+ expect(output).toContain(`port: 3000`);
159
+ expect(output).toContain(`host: "0.0.0.0"`);
160
+ });
161
+
162
+ // ── Existing allowedHosts array, different domain ───────────────
163
+
164
+ it("appends domain to existing allowedHosts array", () => {
165
+ writeConfig(`export default defineConfig({
166
+ server: {
167
+ allowedHosts: ["other.example.com"],
168
+ },
169
+ plugins: [],
170
+ })`);
171
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
172
+ expect(result.patched).toBe(true);
173
+ const output = readConfig();
174
+ expect(output).toContain(`"${DOMAIN_PATTERN}"`);
175
+ expect(output).toContain(`"other.example.com"`);
176
+ });
177
+
178
+ // ── Real-world configs from Workstellar repos ───────────────────
179
+
180
+ it("patches nft-indexer-provisioner style config", () => {
181
+ writeConfig(`import { defineConfig } from "vite"
182
+ import react from "@vitejs/plugin-react"
183
+ import tailwindcss from "@tailwindcss/vite"
184
+ import { resolve } from "path"
185
+
186
+ export default defineConfig({
187
+ plugins: [react(), tailwindcss()],
188
+ resolve: {
189
+ alias: {
190
+ "@": resolve(__dirname, "./src"),
191
+ },
192
+ },
193
+ })`);
194
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
195
+ expect(result.patched).toBe(true);
196
+ const output = readConfig();
197
+ expect(output).toContain(`server: {`);
198
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
199
+ // Preserve existing content
200
+ expect(output).toContain(`plugins: [react(), tailwindcss()]`);
201
+ expect(output).toContain(`resolve:`);
202
+ });
203
+
204
+ it("patches clean-it-platform style config (variable assignment)", () => {
205
+ writeConfig(`import { defineConfig } from 'vite'
206
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
207
+ import viteReact from '@vitejs/plugin-react'
208
+ import tailwindcss from '@tailwindcss/vite'
209
+
210
+ const config = defineConfig({
211
+ plugins: [
212
+ tailwindcss(),
213
+ tanstackStart(),
214
+ viteReact(),
215
+ ],
216
+ })
217
+
218
+ export default config`);
219
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
220
+ expect(result.patched).toBe(true);
221
+ const output = readConfig();
222
+ expect(output).toContain(`server: {`);
223
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
224
+ expect(output).toContain(`export default config`);
225
+ });
226
+
227
+ it("skips nft-marketplace style config (already has allowedHosts: true)", () => {
228
+ writeConfig(`import { defineConfig } from 'vite'
229
+ import viteReact from '@vitejs/plugin-react'
230
+
231
+ const config = defineConfig({
232
+ server: {
233
+ allowedHosts: true,
234
+ },
235
+ plugins: [
236
+ viteReact(),
237
+ ],
238
+ })
239
+
240
+ export default config`);
241
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
242
+ expect(result.patched).toBe(false);
243
+ expect(result.reason).toBe("already allows all hosts (true)");
244
+ });
245
+
246
+ it("patches degen-safe style config (variable, no server block, extra options)", () => {
247
+ writeConfig(`import { defineConfig } from 'vite'
248
+ import viteReact from '@vitejs/plugin-react'
249
+
250
+ const config = defineConfig({
251
+ plugins: [
252
+ viteReact(),
253
+ ],
254
+ optimizeDeps: {
255
+ include: ['@raydium-io/raydium-sdk-v2'],
256
+ },
257
+ ssr: {
258
+ external: ['lodash'],
259
+ },
260
+ })
261
+
262
+ export default config`);
263
+ const result = patchViteAllowedHosts(configPath, DEV_DOMAIN);
264
+ expect(result.patched).toBe(true);
265
+ const output = readConfig();
266
+ expect(output).toContain(`server: {`);
267
+ expect(output).toContain(`allowedHosts: ["${DOMAIN_PATTERN}"]`);
268
+ expect(output).toContain(`optimizeDeps:`);
269
+ expect(output).toContain(`ssr:`);
270
+ });
271
+ });
@@ -0,0 +1,100 @@
1
+ import { readFileSync, writeFileSync } from "fs";
2
+
3
+ export interface PatchResult {
4
+ patched: boolean;
5
+ reason: string;
6
+ }
7
+
8
+ /**
9
+ * Patch a Vite config file to include `server.allowedHosts` with the given devDomain.
10
+ *
11
+ * Handles these real-world patterns:
12
+ * 1. `export default defineConfig({ ... })`
13
+ * 2. `const config = defineConfig({ ... }); export default config`
14
+ * 3. `export default { ... }` (plain object, no defineConfig)
15
+ * 4. Existing `server:` block — merges allowedHosts into it
16
+ * 5. Existing `allowedHosts` array — appends domain if missing
17
+ *
18
+ * Skip conditions:
19
+ * - File already contains the domain pattern string
20
+ * - `allowedHosts` is set to `true` or `"all"` (already permissive)
21
+ * - devDomain is empty
22
+ */
23
+ export function patchViteAllowedHosts(
24
+ viteConfigPath: string,
25
+ devDomain: string
26
+ ): PatchResult {
27
+ if (!devDomain) {
28
+ return { patched: false, reason: "no devDomain configured" };
29
+ }
30
+
31
+ let content: string;
32
+ try {
33
+ content = readFileSync(viteConfigPath, "utf-8");
34
+ } catch {
35
+ return { patched: false, reason: "could not read vite config" };
36
+ }
37
+
38
+ const domainPattern = `.${devDomain}`;
39
+
40
+ // Skip if domain pattern already present in the file
41
+ if (content.includes(domainPattern)) {
42
+ return { patched: false, reason: "domain already present" };
43
+ }
44
+
45
+ // Skip if allowedHosts is already set to a permissive value
46
+ if (/allowedHosts\s*:\s*true/.test(content)) {
47
+ return { patched: false, reason: "already allows all hosts (true)" };
48
+ }
49
+ if (/allowedHosts\s*:\s*["']all["']/.test(content)) {
50
+ return { patched: false, reason: 'already allows all hosts ("all")' };
51
+ }
52
+
53
+ const entry = `"${domainPattern}"`;
54
+ let patched = false;
55
+
56
+ // Case 1: Existing allowedHosts array — append our domain
57
+ if (/allowedHosts\s*:\s*\[/.test(content)) {
58
+ content = content.replace(
59
+ /(allowedHosts\s*:\s*\[)/,
60
+ `$1${entry}, `
61
+ );
62
+ patched = true;
63
+ }
64
+ // Case 2: Existing server block without allowedHosts — inject allowedHosts
65
+ else if (/server\s*:\s*\{/.test(content)) {
66
+ content = content.replace(
67
+ /(server\s*:\s*\{)/,
68
+ `$1\n allowedHosts: [${entry}],`
69
+ );
70
+ patched = true;
71
+ }
72
+ // Case 3: defineConfig({ — add server block
73
+ else if (/defineConfig\s*\(\s*\{/.test(content)) {
74
+ content = content.replace(
75
+ /(defineConfig\s*\(\s*\{)/,
76
+ `$1\n server: {\n allowedHosts: [${entry}],\n },`
77
+ );
78
+ patched = true;
79
+ }
80
+ // Case 4: export default { — add server block
81
+ else if (/export\s+default\s*\{/.test(content)) {
82
+ content = content.replace(
83
+ /(export\s+default\s*\{)/,
84
+ `$1\n server: {\n allowedHosts: [${entry}],\n },`
85
+ );
86
+ patched = true;
87
+ }
88
+
89
+ if (!patched) {
90
+ return { patched: false, reason: "unrecognized config format" };
91
+ }
92
+
93
+ try {
94
+ writeFileSync(viteConfigPath, content);
95
+ } catch {
96
+ return { patched: false, reason: "could not write vite config" };
97
+ }
98
+
99
+ return { patched: true, reason: "added allowedHosts" };
100
+ }