zdev 0.2.5 → 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(`
@@ -3393,11 +3433,11 @@ ${diffStat}
3393
3433
  }
3394
3434
 
3395
3435
  // src/index.ts
3396
- import { readFileSync as readFileSync5 } from "fs";
3436
+ import { readFileSync as readFileSync6 } from "fs";
3397
3437
  import { fileURLToPath } from "url";
3398
3438
  import { dirname, join as join4 } from "path";
3399
3439
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
3400
- var pkg = JSON.parse(readFileSync5(join4(__dirname2, "..", "package.json"), "utf-8"));
3440
+ var pkg = JSON.parse(readFileSync6(join4(__dirname2, "..", "package.json"), "utf-8"));
3401
3441
  var program2 = new Command;
3402
3442
  program2.name("zdev").description(`\uD83D\uDC02 zdev v${pkg.version} - Multi-agent worktree development environment`).version(pkg.version);
3403
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.5",
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": {
@@ -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
+ }