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
|
@@ -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
|
+
}
|