zdev 0.1.0

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.
@@ -0,0 +1,80 @@
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
2
+ import { resolve, basename } from "path";
3
+ import { isGitRepo, getRepoName, run } from "../utils.js";
4
+ import { getSeedPath, SEEDS_DIR } from "../config.js";
5
+
6
+ export interface InitOptions {
7
+ seed?: boolean;
8
+ }
9
+
10
+ export async function init(projectPath: string = ".", options: InitOptions = {}): Promise<void> {
11
+ const fullPath = resolve(projectPath);
12
+
13
+ if (!existsSync(fullPath)) {
14
+ console.error(`āŒ Path does not exist: ${fullPath}`);
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!isGitRepo(fullPath)) {
19
+ console.error(`āŒ Not a git repository: ${fullPath}`);
20
+ process.exit(1);
21
+ }
22
+
23
+ const repoName = getRepoName(fullPath);
24
+ console.log(`šŸ‚ Initializing zdev for: ${repoName}`);
25
+
26
+ // Create .zdev directory in project
27
+ const zdevDir = resolve(fullPath, ".zdev");
28
+ if (!existsSync(zdevDir)) {
29
+ mkdirSync(zdevDir, { recursive: true });
30
+ console.log(` Created ${zdevDir}`);
31
+ }
32
+
33
+ // Create project config
34
+ const projectConfig = {
35
+ name: repoName,
36
+ path: fullPath,
37
+ initialized: new Date().toISOString(),
38
+ };
39
+
40
+ const configPath = resolve(zdevDir, "project.json");
41
+ writeFileSync(configPath, JSON.stringify(projectConfig, null, 2));
42
+ console.log(` Created project config`);
43
+
44
+ // Check for existing .gitignore and add .zdev if needed
45
+ const gitignorePath = resolve(fullPath, ".gitignore");
46
+ if (existsSync(gitignorePath)) {
47
+ const content = readFileSync(gitignorePath, "utf-8");
48
+ if (!content.includes(".zdev")) {
49
+ writeFileSync(gitignorePath, content + "\n.zdev/\n");
50
+ console.log(` Added .zdev/ to .gitignore`);
51
+ }
52
+ }
53
+
54
+ // Create seed if requested
55
+ if (options.seed) {
56
+ console.log(`\nšŸ“¦ Creating seed data...`);
57
+
58
+ // Check if convex directory exists
59
+ const convexDir = resolve(fullPath, "convex");
60
+ if (!existsSync(convexDir)) {
61
+ console.log(` No convex/ directory found, skipping seed`);
62
+ } else {
63
+ const seedPath = getSeedPath(repoName);
64
+ const result = run("bunx", ["convex", "export", "--path", seedPath], {
65
+ cwd: fullPath,
66
+ });
67
+
68
+ if (result.success) {
69
+ console.log(` Seed saved to: ${seedPath}`);
70
+ } else {
71
+ console.error(` Failed to create seed: ${result.stderr}`);
72
+ }
73
+ }
74
+ }
75
+
76
+ console.log(`\nāœ… zdev initialized for ${repoName}`);
77
+ console.log(`\nNext steps:`);
78
+ console.log(` zdev start <feature-name> Start working on a feature`);
79
+ console.log(` zdev list List active worktrees`);
80
+ }
@@ -0,0 +1,78 @@
1
+ import { existsSync } from "fs";
2
+ import {
3
+ loadConfig,
4
+ getWorktreePath,
5
+ ZEBU_HOME,
6
+ WORKTREES_DIR,
7
+ } from "../config.js";
8
+ import { isProcessRunning, getTraefikStatus } from "../utils.js";
9
+
10
+ export interface ListOptions {
11
+ json?: boolean;
12
+ }
13
+
14
+ export async function list(options: ListOptions = {}): Promise<void> {
15
+ const config = loadConfig();
16
+ const allocations = Object.entries(config.allocations);
17
+
18
+ if (options.json) {
19
+ console.log(JSON.stringify(config, null, 2));
20
+ return;
21
+ }
22
+
23
+ console.log(`šŸ‚ zdev Status\n`);
24
+ console.log(`šŸ“ Home: ${ZEBU_HOME}`);
25
+ console.log(`šŸ“ Worktrees: ${WORKTREES_DIR}`);
26
+
27
+ const traefikStatus = getTraefikStatus();
28
+ if (traefikStatus.running) {
29
+ console.log(`šŸ”— Traefik: running (*.${traefikStatus.devDomain || 'dev.example.com'})`);
30
+ } else {
31
+ console.log(`šŸ”— Traefik: not running`);
32
+ }
33
+
34
+ console.log(`\n${"─".repeat(60)}`);
35
+
36
+ if (allocations.length === 0) {
37
+ console.log(`\nNo active features.\n`);
38
+ console.log(`Start one with: zdev start <feature-name> --project <path>`);
39
+ return;
40
+ }
41
+
42
+ console.log(`\nšŸ“‹ Active Features (${allocations.length}):\n`);
43
+
44
+ for (const [name, alloc] of allocations) {
45
+ const worktreePath = getWorktreePath(name);
46
+ const worktreeExists = existsSync(worktreePath);
47
+
48
+ const frontendRunning = alloc.pids.frontend
49
+ ? isProcessRunning(alloc.pids.frontend)
50
+ : false;
51
+ const convexRunning = alloc.pids.convex
52
+ ? isProcessRunning(alloc.pids.convex)
53
+ : false;
54
+
55
+ const statusEmoji = frontendRunning && convexRunning ? "🟢" :
56
+ frontendRunning || convexRunning ? "🟔" : "šŸ”“";
57
+
58
+ console.log(`${statusEmoji} ${name}`);
59
+ console.log(` Project: ${alloc.project}`);
60
+ console.log(` Branch: ${alloc.branch}`);
61
+ console.log(` Path: ${worktreePath} ${worktreeExists ? "" : "(missing)"}`);
62
+ console.log(` Local: http://localhost:${alloc.frontendPort}`);
63
+
64
+ if (alloc.funnelPath && traefikStatus.devDomain) {
65
+ console.log(` Public: https://${alloc.funnelPath}.${traefikStatus.devDomain}`);
66
+ }
67
+
68
+ console.log(` Frontend: ${frontendRunning ? `running (PID: ${alloc.pids.frontend})` : "stopped"}`);
69
+ console.log(` Convex: ${convexRunning ? `running (PID: ${alloc.pids.convex})` : "stopped"}`);
70
+ console.log(` Started: ${new Date(alloc.started).toLocaleString()}`);
71
+ console.log();
72
+ }
73
+
74
+ console.log(`${"─".repeat(60)}`);
75
+ console.log(`\nCommands:`);
76
+ console.log(` zdev stop <feature> Stop servers for a feature`);
77
+ console.log(` zdev clean <feature> Remove worktree after merge`);
78
+ }
@@ -0,0 +1,98 @@
1
+ import { existsSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { isGitRepo, getRepoName, run } from "../utils.js";
4
+ import { getSeedPath, ensurezdevDirs } from "../config.js";
5
+
6
+ export interface SeedOptions {
7
+ project?: string;
8
+ }
9
+
10
+ export async function seedExport(
11
+ projectPath: string = ".",
12
+ options: SeedOptions = {}
13
+ ): Promise<void> {
14
+ const fullPath = resolve(projectPath);
15
+
16
+ if (!existsSync(fullPath)) {
17
+ console.error(`āŒ Path does not exist: ${fullPath}`);
18
+ process.exit(1);
19
+ }
20
+
21
+ if (!isGitRepo(fullPath)) {
22
+ console.error(`āŒ Not a git repository: ${fullPath}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ const repoName = getRepoName(fullPath);
27
+ const seedPath = getSeedPath(repoName);
28
+
29
+ console.log(`šŸ‚ Exporting seed data for: ${repoName}`);
30
+
31
+ ensurezdevDirs();
32
+
33
+ const result = run("bunx", ["convex", "export", "--path", seedPath], {
34
+ cwd: fullPath,
35
+ });
36
+
37
+ if (result.success) {
38
+ console.log(`\nāœ… Seed exported to: ${seedPath}`);
39
+ } else {
40
+ console.error(`\nāŒ Failed to export seed:`);
41
+ console.error(result.stderr);
42
+ process.exit(1);
43
+ }
44
+ }
45
+
46
+ export async function seedImport(
47
+ projectPath: string = ".",
48
+ options: SeedOptions = {}
49
+ ): Promise<void> {
50
+ const fullPath = resolve(projectPath);
51
+
52
+ if (!existsSync(fullPath)) {
53
+ console.error(`āŒ Path does not exist: ${fullPath}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ // Try to determine project name from .zdev/project.json or git
58
+ let repoName: string;
59
+
60
+ const projectConfigPath = resolve(fullPath, ".zdev", "project.json");
61
+ if (existsSync(projectConfigPath)) {
62
+ try {
63
+ const config = JSON.parse(await Bun.file(projectConfigPath).text());
64
+ repoName = config.name;
65
+ } catch {
66
+ repoName = getRepoName(fullPath);
67
+ }
68
+ } else if (isGitRepo(fullPath)) {
69
+ repoName = getRepoName(fullPath);
70
+ } else {
71
+ console.error(`āŒ Cannot determine project name`);
72
+ process.exit(1);
73
+ }
74
+
75
+ const seedPath = getSeedPath(repoName);
76
+
77
+ if (!existsSync(seedPath)) {
78
+ console.error(`āŒ No seed found for ${repoName}`);
79
+ console.log(` Expected: ${seedPath}`);
80
+ console.log(`\n Create one with: zdev seed export --project <main-repo-path>`);
81
+ process.exit(1);
82
+ }
83
+
84
+ console.log(`šŸ‚ Importing seed data for: ${repoName}`);
85
+ console.log(` From: ${seedPath}`);
86
+
87
+ const result = run("bunx", ["convex", "import", "--replace", seedPath], {
88
+ cwd: fullPath,
89
+ });
90
+
91
+ if (result.success) {
92
+ console.log(`\nāœ… Seed imported successfully`);
93
+ } else {
94
+ console.error(`\nāŒ Failed to import seed:`);
95
+ console.error(result.stderr);
96
+ process.exit(1);
97
+ }
98
+ }
@@ -0,0 +1,306 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { resolve, basename, join } from "path";
3
+ import {
4
+ isGitRepo,
5
+ getRepoName,
6
+ gitFetch,
7
+ createWorktree,
8
+ runBackground,
9
+ traefikAddRoute,
10
+ getTraefikStatus,
11
+ run,
12
+ } from "../utils.js";
13
+ import {
14
+ loadConfig,
15
+ saveConfig,
16
+ allocatePorts,
17
+ getWorktreePath,
18
+ getSeedPath,
19
+ type WorktreeAllocation,
20
+ } from "../config.js";
21
+
22
+ export interface StartOptions {
23
+ port?: number;
24
+ local?: boolean; // Skip public URL setup
25
+ seed?: boolean;
26
+ baseBranch?: string;
27
+ webDir?: string; // Subdirectory containing package.json (e.g., "web")
28
+ }
29
+
30
+ /**
31
+ * Detect the web/frontend directory in a project.
32
+ * Looks for common patterns: web/, frontend/, app/, or root package.json
33
+ */
34
+ function detectWebDir(worktreePath: string): string {
35
+ // Check common subdirectory names
36
+ const commonDirs = ["web", "frontend", "app", "client", "packages/web", "apps/web"];
37
+
38
+ for (const dir of commonDirs) {
39
+ const packagePath = join(worktreePath, dir, "package.json");
40
+ if (existsSync(packagePath)) {
41
+ return dir;
42
+ }
43
+ }
44
+
45
+ // Check root
46
+ if (existsSync(join(worktreePath, "package.json"))) {
47
+ return ".";
48
+ }
49
+
50
+ // Default to web/ if nothing found (will fail gracefully)
51
+ return "web";
52
+ }
53
+
54
+ export async function start(
55
+ featureName: string,
56
+ projectPath: string = ".",
57
+ options: StartOptions = {}
58
+ ): Promise<void> {
59
+ const fullPath = resolve(projectPath);
60
+
61
+ if (!existsSync(fullPath)) {
62
+ console.error(`āŒ Path does not exist: ${fullPath}`);
63
+ process.exit(1);
64
+ }
65
+
66
+ if (!isGitRepo(fullPath)) {
67
+ console.error(`āŒ Not a git repository: ${fullPath}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ const repoName = getRepoName(fullPath);
72
+ const worktreeName = `${repoName}-${featureName}`;
73
+ const worktreePath = getWorktreePath(worktreeName);
74
+ const branchName = `feature/${featureName}`;
75
+ const baseBranch = options.baseBranch || "origin/main";
76
+
77
+ console.log(`šŸ‚ Starting feature: ${featureName}`);
78
+ console.log(` Project: ${repoName}`);
79
+ console.log(` Branch: ${branchName}`);
80
+
81
+ // Load config
82
+ const config = loadConfig();
83
+
84
+ // Check if already exists
85
+ if (config.allocations[worktreeName]) {
86
+ console.error(`\nāŒ Feature "${featureName}" already exists for ${repoName}`);
87
+ console.log(` Run: zdev stop ${featureName} --project ${fullPath}`);
88
+ process.exit(1);
89
+ }
90
+
91
+ // Fetch latest
92
+ console.log(`\nšŸ“„ Fetching latest from origin...`);
93
+ if (!gitFetch(fullPath)) {
94
+ console.error(` Failed to fetch, continuing anyway...`);
95
+ }
96
+
97
+ // Create worktree
98
+ console.log(`\n🌳 Creating worktree...`);
99
+ if (existsSync(worktreePath)) {
100
+ console.error(` Worktree path already exists: ${worktreePath}`);
101
+ process.exit(1);
102
+ }
103
+
104
+ const worktreeResult = createWorktree(fullPath, worktreePath, branchName, baseBranch);
105
+ if (!worktreeResult.success) {
106
+ console.error(` Failed to create worktree: ${worktreeResult.error}`);
107
+ process.exit(1);
108
+ }
109
+ console.log(` Created: ${worktreePath}`);
110
+
111
+ // Detect web directory
112
+ const webDir = options.webDir || detectWebDir(worktreePath);
113
+ const webPath = webDir === "." ? worktreePath : join(worktreePath, webDir);
114
+
115
+ console.log(`\nšŸ“ Web directory: ${webDir === "." ? "(root)" : webDir}`);
116
+
117
+ // Copy configured files from main project to worktree
118
+ if (config.copyPatterns && config.copyPatterns.length > 0) {
119
+ console.log(`\nšŸ“‹ Copying config files...`);
120
+ const mainWebPath = webDir === "." ? fullPath : join(fullPath, webDir);
121
+
122
+ for (const pattern of config.copyPatterns) {
123
+ const srcPath = join(mainWebPath, pattern);
124
+ const destPath = join(webPath, pattern);
125
+
126
+ if (existsSync(srcPath) && !existsSync(destPath)) {
127
+ try {
128
+ const content = readFileSync(srcPath);
129
+ writeFileSync(destPath, content);
130
+ console.log(` Copied ${pattern}`);
131
+ } catch (e) {
132
+ console.log(` Could not copy ${pattern}`);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // Run setup script if exists
139
+ const setupScriptPath = join(worktreePath, ".zdev", "setup.sh");
140
+ if (existsSync(setupScriptPath)) {
141
+ console.log(`\nšŸ“¦ Running setup script...`);
142
+ const setupResult = run("bash", [setupScriptPath], { cwd: webPath });
143
+ if (!setupResult.success) {
144
+ console.error(` Setup script failed: ${setupResult.stderr}`);
145
+ } else {
146
+ console.log(` Setup complete`);
147
+ }
148
+ } else {
149
+ console.log(`\nāš ļø No .zdev/setup.sh found, skipping setup`);
150
+ console.log(` Create one in your project to automate dependency installation`);
151
+ }
152
+
153
+ // Check if this is a Convex project
154
+ const hasConvex = existsSync(join(webPath, "convex")) || existsSync(join(worktreePath, "convex"));
155
+
156
+ // Import seed data if requested, exists, and is a Convex project
157
+ const seedPath = getSeedPath(repoName);
158
+ if (options.seed && hasConvex && existsSync(seedPath)) {
159
+ console.log(`\n🌱 Importing seed data...`);
160
+ const seedResult = run("bunx", ["convex", "import", seedPath], {
161
+ cwd: webPath,
162
+ });
163
+ if (seedResult.success) {
164
+ console.log(` Seed data imported`);
165
+ } else {
166
+ console.error(` Failed to import seed: ${seedResult.stderr}`);
167
+ }
168
+ }
169
+
170
+ // Allocate ports
171
+ const ports = options.port
172
+ ? { frontend: options.port, convex: hasConvex ? options.port + 100 : 0 }
173
+ : allocatePorts(config, hasConvex);
174
+
175
+ console.log(`\nšŸ”Œ Allocated ports:`);
176
+ console.log(` Frontend: ${ports.frontend}`);
177
+ if (hasConvex) {
178
+ console.log(` Convex: ${ports.convex}`);
179
+ }
180
+
181
+ // Start Convex dev (only if this is a Convex project)
182
+ let convexPid: number | undefined;
183
+ if (hasConvex) {
184
+ console.log(`\nšŸš€ Starting Convex dev server...`);
185
+ convexPid = runBackground(
186
+ "bunx",
187
+ ["convex", "dev", "--tail-logs", "disable"],
188
+ { cwd: webPath }
189
+ );
190
+ console.log(` Convex PID: ${convexPid}`);
191
+
192
+ // Wait a moment for Convex to start
193
+ await new Promise((resolve) => setTimeout(resolve, 2000));
194
+ }
195
+
196
+ // Patch vite.config to allow external hosts (for Traefik access)
197
+ const viteConfigTsPath = join(webPath, "vite.config.ts");
198
+ const viteConfigJsPath = join(webPath, "vite.config.js");
199
+ const viteConfigPath = existsSync(viteConfigTsPath) ? viteConfigTsPath :
200
+ existsSync(viteConfigJsPath) ? viteConfigJsPath : null;
201
+
202
+ if (viteConfigPath) {
203
+ try {
204
+ let viteConfig = readFileSync(viteConfigPath, "utf-8");
205
+
206
+ // Only patch if allowedHosts not already present
207
+ if (!viteConfig.includes("allowedHosts")) {
208
+ let patched = false;
209
+
210
+ // Try to add allowedHosts to existing server block
211
+ if (viteConfig.includes("server:") || viteConfig.includes("server :")) {
212
+ // Add allowedHosts inside existing server block
213
+ viteConfig = viteConfig.replace(
214
+ /server\s*:\s*\{/,
215
+ "server: {\n allowedHosts: true,"
216
+ );
217
+ patched = true;
218
+ } else if (viteConfig.includes("defineConfig({")) {
219
+ // No server block, add new one
220
+ viteConfig = viteConfig.replace(
221
+ /defineConfig\(\{/,
222
+ "defineConfig({\n server: {\n allowedHosts: true,\n },"
223
+ );
224
+ patched = true;
225
+ }
226
+
227
+ if (patched) {
228
+ writeFileSync(viteConfigPath, viteConfig);
229
+ console.log(` Patched ${basename(viteConfigPath)} for external access`);
230
+
231
+ // Mark file as skip-worktree so it won't be committed
232
+ run("git", ["update-index", "--skip-worktree", basename(viteConfigPath)], { cwd: webPath });
233
+ }
234
+ }
235
+ } catch (e) {
236
+ console.log(` Could not patch vite config (non-critical)`);
237
+ }
238
+ }
239
+
240
+ // Start frontend (bind to all interfaces for Docker/Traefik access)
241
+ console.log(`\n🌐 Starting frontend dev server...`);
242
+ const frontendPid = runBackground(
243
+ "bun",
244
+ ["dev", "--port", String(ports.frontend), "--host", "0.0.0.0"],
245
+ { cwd: webPath }
246
+ );
247
+ console.log(` Frontend PID: ${frontendPid}`);
248
+
249
+ // Setup Traefik route for public URL
250
+ let routePath = "";
251
+ let publicUrl = "";
252
+
253
+ if (!options.local) {
254
+ const traefikStatus = getTraefikStatus();
255
+
256
+ if (traefikStatus.running && traefikStatus.devDomain) {
257
+ routePath = worktreeName;
258
+ console.log(`\nšŸ”— Setting up Traefik route...`);
259
+
260
+ // Wait for frontend to be ready
261
+ await new Promise((resolve) => setTimeout(resolve, 2000));
262
+
263
+ if (traefikAddRoute(worktreeName, ports.frontend)) {
264
+ publicUrl = `https://${worktreeName}.${traefikStatus.devDomain}`;
265
+ console.log(` Public URL: ${publicUrl}`);
266
+ } else {
267
+ console.error(` Failed to setup Traefik route`);
268
+ }
269
+ } else {
270
+ console.log(`\nāš ļø Traefik not configured or devDomain not set, skipping public URL`);
271
+ console.log(` Run: zdev config --set devDomain=dev.yourdomain.com`);
272
+ }
273
+ }
274
+
275
+ // Save allocation
276
+ const allocation: WorktreeAllocation = {
277
+ project: repoName,
278
+ projectPath: fullPath,
279
+ branch: branchName,
280
+ webDir,
281
+ frontendPort: ports.frontend,
282
+ convexPort: ports.convex,
283
+ funnelPath: routePath,
284
+ pids: {
285
+ frontend: frontendPid,
286
+ convex: convexPid,
287
+ },
288
+ started: new Date().toISOString(),
289
+ };
290
+
291
+ config.allocations[worktreeName] = allocation;
292
+ saveConfig(config);
293
+
294
+ // Summary
295
+ console.log(`\n${"─".repeat(50)}`);
296
+ console.log(`āœ… Feature "${featureName}" is ready!\n`);
297
+ console.log(`šŸ“ Worktree: ${worktreePath}`);
298
+ console.log(`🌐 Local: http://localhost:${ports.frontend}`);
299
+ if (publicUrl) {
300
+ console.log(`šŸ”— Public: ${publicUrl}`);
301
+ }
302
+ console.log(`\nšŸ“ Commands:`);
303
+ console.log(` cd ${worktreePath}`);
304
+ console.log(` zdev stop ${featureName} --project ${fullPath}`);
305
+ console.log(`${"─".repeat(50)}`);
306
+ }
@@ -0,0 +1,101 @@
1
+ import { existsSync } from "fs";
2
+ import { resolve } from "path";
3
+ import {
4
+ isGitRepo,
5
+ getRepoName,
6
+ killProcess,
7
+ isProcessRunning,
8
+ traefikRemoveRoute,
9
+ } from "../utils.js";
10
+ import {
11
+ loadConfig,
12
+ saveConfig,
13
+ getWorktreePath,
14
+ } from "../config.js";
15
+
16
+ export interface StopOptions {
17
+ project?: string;
18
+ keep?: boolean; // Keep worktree, just stop servers
19
+ }
20
+
21
+ export async function stop(
22
+ featureName: string,
23
+ options: StopOptions = {}
24
+ ): Promise<void> {
25
+ const config = loadConfig();
26
+
27
+ // Find the allocation
28
+ let worktreeName: string | undefined;
29
+ let allocation;
30
+
31
+ if (options.project) {
32
+ const projectPath = resolve(options.project);
33
+ const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options.project;
34
+ worktreeName = `${repoName}-${featureName}`;
35
+ allocation = config.allocations[worktreeName];
36
+ } else {
37
+ // Search for matching feature across all projects
38
+ for (const [name, alloc] of Object.entries(config.allocations)) {
39
+ if (name.endsWith(`-${featureName}`)) {
40
+ worktreeName = name;
41
+ allocation = alloc;
42
+ break;
43
+ }
44
+ }
45
+ }
46
+
47
+ if (!worktreeName || !allocation) {
48
+ console.error(`āŒ Feature "${featureName}" not found`);
49
+ console.log(`\nRun 'zdev list' to see active features`);
50
+ process.exit(1);
51
+ }
52
+
53
+ console.log(`šŸ‚ Stopping feature: ${featureName}`);
54
+ console.log(` Project: ${allocation.project}`);
55
+
56
+ // Stop frontend
57
+ if (allocation.pids.frontend && isProcessRunning(allocation.pids.frontend)) {
58
+ console.log(`\nšŸ›‘ Stopping frontend (PID: ${allocation.pids.frontend})...`);
59
+ if (killProcess(allocation.pids.frontend)) {
60
+ console.log(` Frontend stopped`);
61
+ } else {
62
+ console.error(` Failed to stop frontend`);
63
+ }
64
+ }
65
+
66
+ // Stop Convex
67
+ if (allocation.pids.convex && isProcessRunning(allocation.pids.convex)) {
68
+ console.log(`\nšŸ›‘ Stopping Convex (PID: ${allocation.pids.convex})...`);
69
+ if (killProcess(allocation.pids.convex)) {
70
+ console.log(` Convex stopped`);
71
+ } else {
72
+ console.error(` Failed to stop Convex`);
73
+ }
74
+ }
75
+
76
+ // Remove Traefik route
77
+ if (allocation.funnelPath) {
78
+ console.log(`\nšŸ”— Removing Traefik route...`);
79
+ if (traefikRemoveRoute(allocation.funnelPath)) {
80
+ console.log(` Route removed`);
81
+ } else {
82
+ console.error(` Failed to remove route (may already be removed)`);
83
+ }
84
+ }
85
+
86
+ // Remove allocation from config
87
+ delete config.allocations[worktreeName];
88
+ saveConfig(config);
89
+
90
+ const worktreePath = getWorktreePath(worktreeName);
91
+
92
+ if (options.keep) {
93
+ console.log(`\nāœ… Feature "${featureName}" stopped (worktree kept)`);
94
+ console.log(` Worktree: ${worktreePath}`);
95
+ console.log(`\n To remove worktree: zdev clean ${featureName}`);
96
+ } else {
97
+ console.log(`\nāœ… Feature "${featureName}" stopped`);
98
+ console.log(`\n Worktree still exists at: ${worktreePath}`);
99
+ console.log(` To remove: zdev clean ${featureName} --project ${allocation.projectPath}`);
100
+ }
101
+ }