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/LICENSE +13 -0
- package/README.md +311 -0
- package/bun.lock +29 -0
- package/dist/index.js +3098 -0
- package/package.json +36 -0
- package/src/commands/clean.ts +122 -0
- package/src/commands/config.ts +105 -0
- package/src/commands/create.ts +381 -0
- package/src/commands/init.ts +80 -0
- package/src/commands/list.ts +78 -0
- package/src/commands/seed.ts +98 -0
- package/src/commands/start.ts +306 -0
- package/src/commands/stop.ts +101 -0
- package/src/config.ts +106 -0
- package/src/index.ts +129 -0
- package/src/utils.ts +230 -0
- package/tsconfig.json +17 -0
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
|
+
}
|