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.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "zdev",
3
+ "version": "0.1.0",
4
+ "description": "Multi-agent worktree development environment for cloud dev with preview URLs",
5
+ "type": "module",
6
+ "bin": {
7
+ "zdev": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build ./src/index.ts --outdir ./dist --target node",
11
+ "dev": "bun run ./src/index.ts"
12
+ },
13
+ "keywords": [
14
+ "clawdbot",
15
+ "convex",
16
+ "worktree",
17
+ "development",
18
+ "cli",
19
+ "multi-agent",
20
+ "tanstack",
21
+ "vite"
22
+ ],
23
+ "author": "5hanth",
24
+ "license": "WTFPL",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/5hanth/zdev"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "latest",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "dependencies": {
34
+ "commander": "^12.0.0"
35
+ }
36
+ }
@@ -0,0 +1,122 @@
1
+ import { existsSync, rmSync } from "fs";
2
+ import { resolve } from "path";
3
+ import {
4
+ isGitRepo,
5
+ getRepoName,
6
+ removeWorktree,
7
+ killProcess,
8
+ isProcessRunning,
9
+ traefikRemoveRoute,
10
+ } from "../utils.js";
11
+ import {
12
+ loadConfig,
13
+ saveConfig,
14
+ getWorktreePath,
15
+ } from "../config.js";
16
+
17
+ export interface CleanOptions {
18
+ project?: string;
19
+ force?: boolean;
20
+ }
21
+
22
+ export async function clean(
23
+ featureName: string,
24
+ options: CleanOptions = {}
25
+ ): Promise<void> {
26
+ const config = loadConfig();
27
+
28
+ // Find the allocation or worktree
29
+ let worktreeName: string | undefined;
30
+ let allocation;
31
+ let projectPath: string | undefined;
32
+
33
+ if (options.project) {
34
+ projectPath = resolve(options.project);
35
+ const repoName = isGitRepo(projectPath) ? getRepoName(projectPath) : options.project;
36
+ worktreeName = `${repoName}-${featureName}`;
37
+ allocation = config.allocations[worktreeName];
38
+ } else {
39
+ // Search for matching feature across all projects
40
+ for (const [name, alloc] of Object.entries(config.allocations)) {
41
+ if (name.endsWith(`-${featureName}`)) {
42
+ worktreeName = name;
43
+ allocation = alloc;
44
+ projectPath = alloc.projectPath;
45
+ break;
46
+ }
47
+ }
48
+
49
+ // If not found in allocations, try to find by worktree name pattern
50
+ if (!worktreeName) {
51
+ // Try common pattern
52
+ const entries = Object.keys(config.allocations);
53
+ console.error(`โŒ Feature "${featureName}" not found in active allocations`);
54
+ if (entries.length > 0) {
55
+ console.log(`\nActive features:`);
56
+ entries.forEach(e => console.log(` - ${e}`));
57
+ }
58
+ process.exit(1);
59
+ }
60
+ }
61
+
62
+ const worktreePath = getWorktreePath(worktreeName);
63
+
64
+ console.log(`๐Ÿ‚ Cleaning feature: ${featureName}`);
65
+
66
+ // Stop processes if still running
67
+ if (allocation) {
68
+ if (allocation.pids.frontend && isProcessRunning(allocation.pids.frontend)) {
69
+ console.log(`\n๐Ÿ›‘ Stopping frontend...`);
70
+ killProcess(allocation.pids.frontend);
71
+ }
72
+
73
+ if (allocation.pids.convex && isProcessRunning(allocation.pids.convex)) {
74
+ console.log(`๐Ÿ›‘ Stopping Convex...`);
75
+ killProcess(allocation.pids.convex);
76
+ }
77
+
78
+ if (allocation.funnelPath) {
79
+ console.log(`๐Ÿ”— Removing Traefik route...`);
80
+ traefikRemoveRoute(allocation.funnelPath);
81
+ }
82
+
83
+ projectPath = allocation.projectPath;
84
+ }
85
+
86
+ // Remove worktree
87
+ if (existsSync(worktreePath)) {
88
+ console.log(`\n๐Ÿ—‘๏ธ Removing worktree...`);
89
+
90
+ if (projectPath && isGitRepo(projectPath)) {
91
+ const result = removeWorktree(projectPath, worktreePath);
92
+ if (!result.success) {
93
+ if (options.force) {
94
+ console.log(` Git worktree remove failed, force removing directory...`);
95
+ rmSync(worktreePath, { recursive: true, force: true });
96
+ } else {
97
+ console.error(` Failed to remove worktree: ${result.error}`);
98
+ console.log(` Use --force to force remove`);
99
+ process.exit(1);
100
+ }
101
+ }
102
+ } else if (options.force) {
103
+ rmSync(worktreePath, { recursive: true, force: true });
104
+ } else {
105
+ console.error(` Cannot remove worktree: project path unknown`);
106
+ console.log(` Use --force to force remove, or specify --project`);
107
+ process.exit(1);
108
+ }
109
+
110
+ console.log(` Worktree removed`);
111
+ } else {
112
+ console.log(`\n Worktree already removed`);
113
+ }
114
+
115
+ // Remove from config
116
+ if (worktreeName && config.allocations[worktreeName]) {
117
+ delete config.allocations[worktreeName];
118
+ saveConfig(config);
119
+ }
120
+
121
+ console.log(`\nโœ… Feature "${featureName}" cleaned up`);
122
+ }
@@ -0,0 +1,105 @@
1
+ import { loadConfig, saveConfig, CONFIG_PATH } from "../config.js";
2
+
3
+ export interface ConfigOptions {
4
+ add?: string;
5
+ remove?: string;
6
+ list?: boolean;
7
+ set?: string;
8
+ }
9
+
10
+ export async function configCmd(options: ConfigOptions = {}): Promise<void> {
11
+ const config = loadConfig();
12
+
13
+ if (options.set) {
14
+ // Parse key=value
15
+ const [key, ...valueParts] = options.set.split("=");
16
+ const value = valueParts.join("=");
17
+
18
+ if (!value) {
19
+ console.error(`Usage: zdev config --set key=value`);
20
+ console.log(`\nConfigurable keys:`);
21
+ console.log(` devDomain Dev domain for public URLs`);
22
+ console.log(` dockerHostIp Docker host IP for Traefik`);
23
+ console.log(` traefikConfigDir Traefik dynamic config directory`);
24
+ return;
25
+ }
26
+
27
+ if (key === "devDomain") {
28
+ config.devDomain = value;
29
+ saveConfig(config);
30
+ console.log(`โœ… Set devDomain = ${value}`);
31
+ } else if (key === "dockerHostIp") {
32
+ config.dockerHostIp = value;
33
+ saveConfig(config);
34
+ console.log(`โœ… Set dockerHostIp = ${value}`);
35
+ } else if (key === "traefikConfigDir") {
36
+ config.traefikConfigDir = value;
37
+ saveConfig(config);
38
+ console.log(`โœ… Set traefikConfigDir = ${value}`);
39
+ } else {
40
+ console.error(`Unknown config key: ${key}`);
41
+ }
42
+ return;
43
+ }
44
+
45
+ if (options.list || (!options.add && !options.remove)) {
46
+ console.log(`๐Ÿ‚ zdev Configuration\n`);
47
+ console.log(`๐Ÿ“ Config file: ${CONFIG_PATH}`);
48
+
49
+ console.log(`\n๐ŸŒ Traefik / Public URLs:`);
50
+ console.log(` Dev domain: ${config.devDomain}`);
51
+ console.log(` Docker host IP: ${config.dockerHostIp}`);
52
+ console.log(` Config dir: ${config.traefikConfigDir}`);
53
+
54
+ console.log(`\n๐Ÿ“‹ Copy patterns (files auto-copied to worktrees):`);
55
+ if (config.copyPatterns && config.copyPatterns.length > 0) {
56
+ for (const pattern of config.copyPatterns) {
57
+ console.log(` - ${pattern}`);
58
+ }
59
+ } else {
60
+ console.log(` (none)`);
61
+ }
62
+
63
+ console.log(`\n๐Ÿ”Œ Port allocation:`);
64
+ console.log(` Next frontend port: ${config.nextFrontendPort}`);
65
+ console.log(` Next Convex port: ${config.nextConvexPort}`);
66
+
67
+ console.log(`\nCommands:`);
68
+ console.log(` zdev config --set devDomain=dev.example.com`);
69
+ console.log(` zdev config --add ".env.local"`);
70
+ console.log(` zdev config --remove ".env.local"`);
71
+ return;
72
+ }
73
+
74
+ if (options.add) {
75
+ if (!config.copyPatterns) {
76
+ config.copyPatterns = [];
77
+ }
78
+
79
+ if (config.copyPatterns.includes(options.add)) {
80
+ console.log(`Pattern "${options.add}" already exists`);
81
+ } else {
82
+ config.copyPatterns.push(options.add);
83
+ saveConfig(config);
84
+ console.log(`โœ… Added copy pattern: ${options.add}`);
85
+ }
86
+ return;
87
+ }
88
+
89
+ if (options.remove) {
90
+ if (!config.copyPatterns) {
91
+ console.log(`Pattern "${options.remove}" not found`);
92
+ return;
93
+ }
94
+
95
+ const index = config.copyPatterns.indexOf(options.remove);
96
+ if (index === -1) {
97
+ console.log(`Pattern "${options.remove}" not found`);
98
+ } else {
99
+ config.copyPatterns.splice(index, 1);
100
+ saveConfig(config);
101
+ console.log(`โœ… Removed copy pattern: ${options.remove}`);
102
+ }
103
+ return;
104
+ }
105
+ }
@@ -0,0 +1,381 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync, readFileSync } from "fs";
2
+ import { resolve, join } from "path";
3
+ import { run } from "../utils.js";
4
+
5
+ export interface CreateOptions {
6
+ convex?: boolean;
7
+ flat?: boolean;
8
+ }
9
+
10
+ const ZEBU_INDEX_PAGE = `import { createFileRoute } from '@tanstack/react-router'
11
+
12
+ export const Route = createFileRoute('/')({
13
+ component: Home,
14
+ })
15
+
16
+ function Home() {
17
+ return (
18
+ <div
19
+ style={{
20
+ minHeight: '100vh',
21
+ background: 'linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%)',
22
+ display: 'flex',
23
+ flexDirection: 'column',
24
+ alignItems: 'center',
25
+ justifyContent: 'center',
26
+ fontFamily: 'system-ui, -apple-system, sans-serif',
27
+ color: '#e8e8e8',
28
+ padding: '2rem',
29
+ position: 'relative',
30
+ overflow: 'hidden',
31
+ }}
32
+ >
33
+ {/* Glowing orbs */}
34
+ <div style={{
35
+ position: 'absolute',
36
+ top: '10%',
37
+ left: '15%',
38
+ width: '300px',
39
+ height: '300px',
40
+ background: 'radial-gradient(circle, rgba(99, 102, 241, 0.3) 0%, transparent 70%)',
41
+ borderRadius: '50%',
42
+ filter: 'blur(40px)',
43
+ }} />
44
+ <div style={{
45
+ position: 'absolute',
46
+ bottom: '20%',
47
+ right: '10%',
48
+ width: '400px',
49
+ height: '400px',
50
+ background: 'radial-gradient(circle, rgba(16, 185, 129, 0.25) 0%, transparent 70%)',
51
+ borderRadius: '50%',
52
+ filter: 'blur(50px)',
53
+ }} />
54
+
55
+ <div style={{ fontSize: '7rem', marginBottom: '1.5rem', zIndex: 1 }}>
56
+ ๐Ÿ‚
57
+ </div>
58
+ <h1
59
+ style={{
60
+ fontSize: '3.5rem',
61
+ fontWeight: 800,
62
+ margin: 0,
63
+ background: 'linear-gradient(90deg, #818cf8, #34d399)',
64
+ WebkitBackgroundClip: 'text',
65
+ WebkitTextFillColor: 'transparent',
66
+ backgroundClip: 'text',
67
+ zIndex: 1,
68
+ }}
69
+ >
70
+ Ready to build
71
+ </h1>
72
+ <p
73
+ style={{
74
+ fontSize: '1.25rem',
75
+ color: 'rgba(255,255,255,0.6)',
76
+ marginTop: '1.5rem',
77
+ textAlign: 'center',
78
+ zIndex: 1,
79
+ maxWidth: '500px',
80
+ lineHeight: 1.6,
81
+ }}
82
+ >
83
+ Your TanStack Start app is ready.
84
+ <br />
85
+ Edit <code style={{
86
+ color: '#818cf8',
87
+ background: 'rgba(129, 140, 248, 0.1)',
88
+ padding: '0.2rem 0.6rem',
89
+ borderRadius: '6px',
90
+ border: '1px solid rgba(129, 140, 248, 0.2)',
91
+ }}>src/routes/index.tsx</code> to get started.
92
+ </p>
93
+ </div>
94
+ )
95
+ }
96
+ `;
97
+
98
+ const CONVEX_PROVIDER = `import { ConvexProvider, ConvexReactClient } from "convex/react";
99
+ import { ReactNode } from "react";
100
+
101
+ const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
102
+
103
+ export function ConvexClientProvider({ children }: { children: ReactNode }) {
104
+ return <ConvexProvider client={convex}>{children}</ConvexProvider>;
105
+ }
106
+ `;
107
+
108
+ const ROUTER = `import { createRouter } from '@tanstack/react-router'
109
+ import { routeTree } from './routeTree.gen'
110
+
111
+ export function getRouter() {
112
+ const router = createRouter({
113
+ routeTree,
114
+ defaultPreload: 'intent',
115
+ scrollRestoration: true,
116
+ })
117
+ return router
118
+ }
119
+
120
+ declare module '@tanstack/react-router' {
121
+ interface Register {
122
+ router: ReturnType<typeof getRouter>
123
+ }
124
+ }
125
+ `;
126
+
127
+ const ROOT_ROUTE = `/// <reference types="vite/client" />
128
+ import {
129
+ HeadContent,
130
+ Outlet,
131
+ Scripts,
132
+ createRootRoute,
133
+ } from '@tanstack/react-router'
134
+ import * as React from 'react'
135
+ import { Agentation } from 'agentation'
136
+
137
+ export const Route = createRootRoute({
138
+ head: () => ({
139
+ meta: [
140
+ { charSet: 'utf-8' },
141
+ { name: 'viewport', content: 'width=device-width, initial-scale=1' },
142
+ ],
143
+ }),
144
+ component: RootDocument,
145
+ })
146
+
147
+ function RootDocument() {
148
+ return (
149
+ <html>
150
+ <head>
151
+ <HeadContent />
152
+ </head>
153
+ <body>
154
+ <Outlet />
155
+ {import.meta.env.DEV && <Agentation />}
156
+ <Scripts />
157
+ </body>
158
+ </html>
159
+ )
160
+ }
161
+ `;
162
+
163
+ const SETUP_SCRIPT = `#!/bin/bash
164
+ # .zdev/setup.sh - Runs after worktree creation
165
+ # Edit this to customize your setup (change package manager, add commands, etc.)
166
+
167
+ set -e
168
+
169
+ # Install dependencies
170
+ bun install
171
+
172
+ # Add any other setup commands below:
173
+ # bunx prisma generate
174
+ # cp ../.env.local .
175
+ `;
176
+
177
+ export async function create(
178
+ projectName: string,
179
+ options: CreateOptions = {}
180
+ ): Promise<void> {
181
+ const targetPath = resolve(projectName);
182
+
183
+ if (existsSync(targetPath)) {
184
+ console.error(`โŒ Directory already exists: ${targetPath}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ console.log(`๐Ÿ‚ Creating new project: ${projectName}`);
189
+ console.log(` Convex: ${options.convex ? 'yes' : 'no'}`);
190
+ console.log(` Structure: ${options.flat ? 'flat' : 'monorepo'}`);
191
+
192
+ // Clone start-basic template
193
+ console.log(`\n๐Ÿ“ฅ Cloning TanStack Start template...`);
194
+ const cloneResult = run("npx", [
195
+ "-y",
196
+ "gitpick",
197
+ "TanStack/router/tree/main/examples/react/start-basic",
198
+ projectName,
199
+ ]);
200
+
201
+ if (!cloneResult.success) {
202
+ console.error(`โŒ Failed to clone template: ${cloneResult.stderr}`);
203
+ process.exit(1);
204
+ }
205
+ console.log(` Template cloned`);
206
+
207
+ // Determine web directory
208
+ let webPath: string;
209
+
210
+ if (options.flat) {
211
+ webPath = targetPath;
212
+ } else {
213
+ // Monorepo: move everything into web/
214
+ console.log(`\n๐Ÿ“ Setting up monorepo structure...`);
215
+ const webDir = join(targetPath, "web");
216
+ const tempDir = join(targetPath, "_temp_web");
217
+
218
+ // Move all files to temp, then to web/
219
+ mkdirSync(tempDir, { recursive: true });
220
+
221
+ const files = readdirSync(targetPath);
222
+ for (const file of files) {
223
+ if (file !== "_temp_web") {
224
+ renameSync(join(targetPath, file), join(tempDir, file));
225
+ }
226
+ }
227
+
228
+ renameSync(tempDir, webDir);
229
+ webPath = webDir;
230
+
231
+ // Create root package.json for workspace
232
+ const rootPackageJson = {
233
+ name: projectName,
234
+ private: true,
235
+ workspaces: ["web"],
236
+ scripts: {
237
+ dev: "cd web && bun dev",
238
+ build: "cd web && bun run build",
239
+ },
240
+ };
241
+ writeFileSync(
242
+ join(targetPath, "package.json"),
243
+ JSON.stringify(rootPackageJson, null, 2)
244
+ );
245
+
246
+ console.log(` Created web/ subdirectory`);
247
+ }
248
+
249
+ // Clean up demo routes, components, and utils
250
+ console.log(`\n๐Ÿงน Cleaning up demo files...`);
251
+ const srcDir = join(webPath, "src");
252
+ const routesDir = join(srcDir, "routes");
253
+
254
+ // Clean all routes
255
+ if (existsSync(routesDir)) {
256
+ rmSync(routesDir, { recursive: true, force: true });
257
+ mkdirSync(routesDir, { recursive: true });
258
+ }
259
+
260
+ // Remove demo components, utils, and styles
261
+ const componentsDir = join(srcDir, "components");
262
+ const utilsDir = join(srcDir, "utils");
263
+ const stylesDir = join(srcDir, "styles");
264
+ if (existsSync(componentsDir)) {
265
+ rmSync(componentsDir, { recursive: true, force: true });
266
+ }
267
+ if (existsSync(utilsDir)) {
268
+ rmSync(utilsDir, { recursive: true, force: true });
269
+ }
270
+ if (existsSync(stylesDir)) {
271
+ rmSync(stylesDir, { recursive: true, force: true });
272
+ }
273
+
274
+ // Remove generated route tree (will be regenerated)
275
+ const routeTreePath = join(srcDir, "routeTree.gen.ts");
276
+ if (existsSync(routeTreePath)) {
277
+ rmSync(routeTreePath);
278
+ }
279
+
280
+ // Remove app/ directory if it exists (we use src/)
281
+ const appDir = join(webPath, "app");
282
+ if (existsSync(appDir)) {
283
+ rmSync(appDir, { recursive: true, force: true });
284
+ }
285
+
286
+ // Add clean router, root, and Zebu-themed index route
287
+ writeFileSync(join(srcDir, "router.tsx"), ROUTER);
288
+ writeFileSync(join(routesDir, "__root.tsx"), ROOT_ROUTE);
289
+ writeFileSync(join(routesDir, "index.tsx"), ZEBU_INDEX_PAGE);
290
+ console.log(` Cleaned demo files, added index route`);
291
+
292
+ // Update package.json name and add agentation
293
+ const pkgPath = join(webPath, "package.json");
294
+ if (existsSync(pkgPath)) {
295
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
296
+ pkg.name = options.flat ? projectName : `${projectName}-web`;
297
+ // Add agentation for AI agent UI feedback
298
+ pkg.dependencies = pkg.dependencies || {};
299
+ pkg.dependencies["agentation"] = "latest";
300
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
301
+ }
302
+
303
+ // Add Convex if requested
304
+ if (options.convex) {
305
+ console.log(`\n๐Ÿ”ง Setting up Convex...`);
306
+
307
+ // Add Convex dependencies
308
+ const addResult = run("bun", ["add", "convex", "convex-react"], { cwd: webPath });
309
+ if (!addResult.success) {
310
+ console.error(` Failed to add Convex deps: ${addResult.stderr}`);
311
+ } else {
312
+ console.log(` Added convex dependencies`);
313
+ }
314
+
315
+ // Initialize Convex
316
+ const initResult = run("bunx", ["convex", "init"], { cwd: webPath });
317
+ if (!initResult.success) {
318
+ console.log(` Note: Run 'bunx convex dev' to complete Convex setup`);
319
+ } else {
320
+ console.log(` Initialized Convex`);
321
+ }
322
+
323
+ // Create ConvexClientProvider
324
+ const componentsDir = join(webPath, "app", "components");
325
+ mkdirSync(componentsDir, { recursive: true });
326
+ writeFileSync(join(componentsDir, "ConvexClientProvider.tsx"), CONVEX_PROVIDER);
327
+ console.log(` Created ConvexClientProvider`);
328
+
329
+ // Create .env.local template
330
+ writeFileSync(
331
+ join(webPath, ".env.local.example"),
332
+ "VITE_CONVEX_URL=your_convex_url_here\n"
333
+ );
334
+ console.log(` Created .env.local.example`);
335
+
336
+ // Note: User needs to manually wrap their app with the provider
337
+ console.log(`\n โš ๏ธ To complete Convex setup:`);
338
+ console.log(` 1. cd ${options.flat ? projectName : projectName + '/web'}`);
339
+ console.log(` 2. bunx convex dev (select/create project)`);
340
+ console.log(` 3. Wrap your app with <ConvexClientProvider> in app/root.tsx`);
341
+ }
342
+
343
+ // Create .zdev/setup.sh for worktree setup
344
+ console.log(`\n๐Ÿ“œ Creating setup script...`);
345
+ const zdevDir = join(targetPath, ".zdev");
346
+ mkdirSync(zdevDir, { recursive: true });
347
+ const setupScriptPath = join(zdevDir, "setup.sh");
348
+ writeFileSync(setupScriptPath, SETUP_SCRIPT, { mode: 0o755 });
349
+ console.log(` Created .zdev/setup.sh`);
350
+
351
+ // Install dependencies (initial setup)
352
+ console.log(`\n๐Ÿ“ฆ Installing dependencies...`);
353
+ const installResult = run("bun", ["install"], { cwd: webPath });
354
+ if (!installResult.success) {
355
+ console.error(` Failed to install: ${installResult.stderr}`);
356
+ } else {
357
+ console.log(` Dependencies installed`);
358
+ }
359
+
360
+ // Initialize git
361
+ console.log(`\n๐Ÿ“š Initializing git...`);
362
+ run("git", ["init"], { cwd: targetPath });
363
+ run("git", ["add", "."], { cwd: targetPath });
364
+ run("git", ["commit", "-m", "Initial commit from zdev create"], { cwd: targetPath });
365
+ console.log(` Git initialized`);
366
+
367
+ // Summary
368
+ console.log(`\n${"โ”€".repeat(50)}`);
369
+ console.log(`โœ… Project "${projectName}" created!\n`);
370
+ console.log(`๐Ÿ“ Location: ${targetPath}`);
371
+ console.log(`\n๐Ÿ“ Next steps:`);
372
+ console.log(` cd ${projectName}`);
373
+ if (!options.flat) {
374
+ console.log(` cd web`);
375
+ }
376
+ if (options.convex) {
377
+ console.log(` bunx convex dev # Setup Convex project`);
378
+ }
379
+ console.log(` bun dev # Start dev server`);
380
+ console.log(`${"โ”€".repeat(50)}`);
381
+ }