zdev 0.1.4 → 0.2.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/dist/index.js CHANGED
@@ -2515,10 +2515,13 @@ async function start(featureName, projectPath = ".", options = {}) {
2515
2515
  console.log(` Run: zdev stop ${featureName} --project ${fullPath}`);
2516
2516
  process.exit(1);
2517
2517
  }
2518
- console.log(`
2518
+ const hasOrigin = run("git", ["remote", "get-url", "origin"], { cwd: fullPath });
2519
+ if (hasOrigin.success) {
2520
+ console.log(`
2519
2521
  \uD83D\uDCE5 Fetching latest from origin...`);
2520
- if (!gitFetch(fullPath)) {
2521
- console.error(` Failed to fetch, continuing anyway...`);
2522
+ if (!gitFetch(fullPath)) {
2523
+ console.error(` Failed to fetch, continuing anyway...`);
2524
+ }
2522
2525
  }
2523
2526
  console.log(`
2524
2527
  \uD83C\uDF33 Creating worktree...`);
@@ -2660,6 +2663,8 @@ async function start(featureName, projectPath = ".", options = {}) {
2660
2663
  frontendPort: ports.frontend,
2661
2664
  convexPort: ports.convex,
2662
2665
  funnelPath: routePath,
2666
+ worktreePath,
2667
+ publicUrl: publicUrl || undefined,
2663
2668
  pids: {
2664
2669
  frontend: frontendPid,
2665
2670
  convex: convexPid
@@ -3066,6 +3071,155 @@ Commands:`);
3066
3071
  }
3067
3072
  }
3068
3073
 
3074
+ // src/commands/pr.ts
3075
+ import { resolve as resolve8, basename as basename4 } from "path";
3076
+ async function pr(featureName, projectPath = ".", options = {}) {
3077
+ const fullPath = resolve8(projectPath);
3078
+ let worktreePath = fullPath;
3079
+ let allocation;
3080
+ let projectName = getRepoName(fullPath) || basename4(fullPath);
3081
+ const config = loadConfig();
3082
+ if (featureName) {
3083
+ const allocKey = `${projectName}-${featureName}`;
3084
+ allocation = config.allocations[allocKey];
3085
+ if (!allocation) {
3086
+ const found = Object.entries(config.allocations).find(([key, alloc]) => key.endsWith(`-${featureName}`));
3087
+ if (found) {
3088
+ allocation = found[1];
3089
+ projectName = allocation.project;
3090
+ }
3091
+ }
3092
+ if (allocation) {
3093
+ worktreePath = allocation.worktreePath || resolve8(config.worktreesDir, `${projectName}-${featureName}`);
3094
+ }
3095
+ } else {
3096
+ const cwd = process.cwd();
3097
+ const found = Object.entries(config.allocations).find(([_, alloc]) => alloc.worktreePath === cwd || cwd.startsWith(alloc.worktreePath || ""));
3098
+ if (found) {
3099
+ allocation = found[1];
3100
+ featureName = found[0].split("-").slice(1).join("-");
3101
+ projectName = allocation.project;
3102
+ worktreePath = allocation.worktreePath || cwd;
3103
+ }
3104
+ }
3105
+ if (!isGitRepo(worktreePath)) {
3106
+ console.error(`❌ Not a git repository: ${worktreePath}`);
3107
+ process.exit(1);
3108
+ }
3109
+ const branchResult = run("git", ["branch", "--show-current"], { cwd: worktreePath });
3110
+ if (!branchResult.success || !branchResult.stdout.trim()) {
3111
+ console.error("❌ Could not determine current branch");
3112
+ process.exit(1);
3113
+ }
3114
+ const branch = branchResult.stdout.trim();
3115
+ console.log(`\uD83D\uDC02 Creating PR for: ${branch}`);
3116
+ if (allocation) {
3117
+ console.log(` Project: ${projectName}`);
3118
+ console.log(` Feature: ${featureName}`);
3119
+ }
3120
+ const ghCheck = run("which", ["gh"]);
3121
+ if (!ghCheck.success) {
3122
+ console.error("❌ GitHub CLI (gh) not found. Install: https://cli.github.com");
3123
+ process.exit(1);
3124
+ }
3125
+ const authCheck = run("gh", ["auth", "status"], { cwd: worktreePath });
3126
+ if (!authCheck.success) {
3127
+ console.error("❌ Not authenticated with GitHub. Run: gh auth login");
3128
+ process.exit(1);
3129
+ }
3130
+ console.log(`
3131
+ \uD83D\uDCE4 Pushing branch...`);
3132
+ const pushResult = run("git", ["push", "-u", "origin", branch], { cwd: worktreePath });
3133
+ if (!pushResult.success) {
3134
+ if (!pushResult.stderr.includes("Everything up-to-date")) {
3135
+ console.error(` Failed to push: ${pushResult.stderr}`);
3136
+ process.exit(1);
3137
+ }
3138
+ console.log(` Already up to date`);
3139
+ } else {
3140
+ console.log(` Pushed to origin/${branch}`);
3141
+ }
3142
+ let title = options.title;
3143
+ if (!title) {
3144
+ const featureForTitle = featureName || branch.replace(/^feature\//, "");
3145
+ title = featureForTitle.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3146
+ }
3147
+ let body = options.body || "";
3148
+ if (allocation && allocation.publicUrl) {
3149
+ const previewSection = `## Preview
3150
+ \uD83D\uDD17 ${allocation.publicUrl}
3151
+
3152
+ `;
3153
+ body = previewSection + body;
3154
+ } else if (config.devDomain && featureName && projectName) {
3155
+ const previewUrl = `https://${projectName}-${featureName}.${config.devDomain}`;
3156
+ const previewSection = `## Preview
3157
+ \uD83D\uDD17 ${previewUrl}
3158
+
3159
+ `;
3160
+ body = previewSection + body;
3161
+ }
3162
+ if (!body.includes("Created with zdev")) {
3163
+ body = body.trim() + `
3164
+
3165
+ ---
3166
+ *Created with [zdev](https://github.com/5hanth/zdev)*`;
3167
+ }
3168
+ const existingPr = run("gh", ["pr", "view", branch, "--json", "url"], { cwd: worktreePath });
3169
+ if (existingPr.success) {
3170
+ try {
3171
+ const prData = JSON.parse(existingPr.stdout);
3172
+ console.log(`
3173
+ ✅ PR already exists!`);
3174
+ console.log(`
3175
+ \uD83D\uDD17 ${prData.url}`);
3176
+ return;
3177
+ } catch {}
3178
+ }
3179
+ console.log(`
3180
+ \uD83D\uDCDD Creating pull request...`);
3181
+ const prArgs = ["pr", "create", "--title", title, "--body", body];
3182
+ if (options.draft) {
3183
+ prArgs.push("--draft");
3184
+ }
3185
+ if (options.web) {
3186
+ prArgs.push("--web");
3187
+ const webResult = run("gh", prArgs, { cwd: worktreePath });
3188
+ if (!webResult.success) {
3189
+ console.error(` Failed: ${webResult.stderr}`);
3190
+ process.exit(1);
3191
+ }
3192
+ console.log(` Opened in browser`);
3193
+ return;
3194
+ }
3195
+ const prResult = run("gh", prArgs, { cwd: worktreePath });
3196
+ if (!prResult.success) {
3197
+ if (prResult.stderr.includes("already exists")) {
3198
+ console.log(` PR already exists for this branch`);
3199
+ const viewResult = run("gh", ["pr", "view", "--json", "url"], { cwd: worktreePath });
3200
+ if (viewResult.success) {
3201
+ try {
3202
+ const prData = JSON.parse(viewResult.stdout);
3203
+ console.log(`
3204
+ \uD83D\uDD17 ${prData.url}`);
3205
+ } catch {}
3206
+ }
3207
+ return;
3208
+ }
3209
+ console.error(` Failed: ${prResult.stderr}`);
3210
+ process.exit(1);
3211
+ }
3212
+ const prUrl = prResult.stdout.trim();
3213
+ console.log(`
3214
+ ✅ Pull request created!`);
3215
+ console.log(`
3216
+ \uD83D\uDD17 ${prUrl}`);
3217
+ if (allocation?.publicUrl) {
3218
+ console.log(`
3219
+ \uD83D\uDCF1 Preview: ${allocation.publicUrl}`);
3220
+ }
3221
+ }
3222
+
3069
3223
  // src/index.ts
3070
3224
  import { readFileSync as readFileSync5 } from "fs";
3071
3225
  import { fileURLToPath } from "url";
@@ -3111,6 +3265,14 @@ seedCmd.command("import [path]").description("Import seed data into current work
3111
3265
  program2.command("config").description("View and manage zdev configuration").option("-a, --add <pattern>", "Add a file pattern to auto-copy").option("-r, --remove <pattern>", "Remove a file pattern").option("-s, --set <key=value>", "Set a config value (devDomain, dockerHostIp, traefikConfigDir)").option("-l, --list", "List current configuration").action(async (options) => {
3112
3266
  await configCmd(options);
3113
3267
  });
3268
+ program2.command("pr [feature]").description("Create a pull request for a feature branch").option("-p, --project <path>", "Project path", ".").option("-t, --title <title>", "PR title (auto-generated if not specified)").option("-b, --body <body>", "PR body (preview URL auto-added)").option("-d, --draft", "Create as draft PR").option("-w, --web", "Open in browser instead of CLI").action(async (feature, options) => {
3269
+ await pr(feature, options.project, {
3270
+ title: options.title,
3271
+ body: options.body,
3272
+ draft: options.draft,
3273
+ web: options.web
3274
+ });
3275
+ });
3114
3276
  program2.command("status").description("Show zdev status (alias for list)").action(async () => {
3115
3277
  await list({});
3116
3278
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdev",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Multi-agent worktree development environment for cloud dev with preview URLs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,201 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { resolve, basename } from "path";
3
+ import { run, isGitRepo, getRepoName } from "../utils.js";
4
+ import { loadConfig, type WorktreeAllocation } from "../config.js";
5
+
6
+ export interface PrOptions {
7
+ title?: string;
8
+ body?: string;
9
+ draft?: boolean;
10
+ web?: boolean;
11
+ }
12
+
13
+ export async function pr(
14
+ featureName: string | undefined,
15
+ projectPath: string = ".",
16
+ options: PrOptions = {}
17
+ ): Promise<void> {
18
+ const fullPath = resolve(projectPath);
19
+
20
+ // Check if we're in a worktree or need to find one
21
+ let worktreePath = fullPath;
22
+ let allocation: WorktreeAllocation | undefined;
23
+ let projectName = getRepoName(fullPath) || basename(fullPath);
24
+
25
+ const config = loadConfig();
26
+
27
+ // If featureName provided, find the allocation
28
+ if (featureName) {
29
+ const allocKey = `${projectName}-${featureName}`;
30
+ allocation = config.allocations[allocKey];
31
+
32
+ if (!allocation) {
33
+ // Try to find by just feature name
34
+ const found = Object.entries(config.allocations).find(
35
+ ([key, alloc]) => key.endsWith(`-${featureName}`)
36
+ );
37
+ if (found) {
38
+ allocation = found[1];
39
+ projectName = allocation.project;
40
+ }
41
+ }
42
+
43
+ if (allocation) {
44
+ worktreePath = allocation.worktreePath || resolve(config.worktreesDir, `${projectName}-${featureName}`);
45
+ }
46
+ } else {
47
+ // Try to detect from current directory
48
+ const cwd = process.cwd();
49
+ const found = Object.entries(config.allocations).find(
50
+ ([_, alloc]) => alloc.worktreePath === cwd || cwd.startsWith(alloc.worktreePath || "")
51
+ );
52
+ if (found) {
53
+ allocation = found[1];
54
+ featureName = found[0].split("-").slice(1).join("-");
55
+ projectName = allocation.project;
56
+ worktreePath = allocation.worktreePath || cwd;
57
+ }
58
+ }
59
+
60
+ if (!isGitRepo(worktreePath)) {
61
+ console.error(`❌ Not a git repository: ${worktreePath}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ // Get current branch
66
+ const branchResult = run("git", ["branch", "--show-current"], { cwd: worktreePath });
67
+ if (!branchResult.success || !branchResult.stdout.trim()) {
68
+ console.error("❌ Could not determine current branch");
69
+ process.exit(1);
70
+ }
71
+ const branch = branchResult.stdout.trim();
72
+
73
+ console.log(`🐂 Creating PR for: ${branch}`);
74
+ if (allocation) {
75
+ console.log(` Project: ${projectName}`);
76
+ console.log(` Feature: ${featureName}`);
77
+ }
78
+
79
+ // Check if gh CLI is available
80
+ const ghCheck = run("which", ["gh"]);
81
+ if (!ghCheck.success) {
82
+ console.error("❌ GitHub CLI (gh) not found. Install: https://cli.github.com");
83
+ process.exit(1);
84
+ }
85
+
86
+ // Check if authenticated
87
+ const authCheck = run("gh", ["auth", "status"], { cwd: worktreePath });
88
+ if (!authCheck.success) {
89
+ console.error("❌ Not authenticated with GitHub. Run: gh auth login");
90
+ process.exit(1);
91
+ }
92
+
93
+ // Push branch if not pushed
94
+ console.log(`\n📤 Pushing branch...`);
95
+ const pushResult = run("git", ["push", "-u", "origin", branch], { cwd: worktreePath });
96
+ if (!pushResult.success) {
97
+ // Check if it's just "already up to date"
98
+ if (!pushResult.stderr.includes("Everything up-to-date")) {
99
+ console.error(` Failed to push: ${pushResult.stderr}`);
100
+ process.exit(1);
101
+ }
102
+ console.log(` Already up to date`);
103
+ } else {
104
+ console.log(` Pushed to origin/${branch}`);
105
+ }
106
+
107
+ // Build PR title
108
+ let title = options.title;
109
+ if (!title) {
110
+ // Generate from feature name or branch
111
+ const featureForTitle = featureName || branch.replace(/^feature\//, "");
112
+ title = featureForTitle
113
+ .split(/[-_]/)
114
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
115
+ .join(" ");
116
+ }
117
+
118
+ // Build PR body
119
+ let body = options.body || "";
120
+
121
+ // Add preview URL if we have allocation
122
+ if (allocation && allocation.publicUrl) {
123
+ const previewSection = `## Preview\n🔗 ${allocation.publicUrl}\n\n`;
124
+ body = previewSection + body;
125
+ } else if (config.devDomain && featureName && projectName) {
126
+ // Try to construct preview URL
127
+ const previewUrl = `https://${projectName}-${featureName}.${config.devDomain}`;
128
+ const previewSection = `## Preview\n🔗 ${previewUrl}\n\n`;
129
+ body = previewSection + body;
130
+ }
131
+
132
+ // Add footer
133
+ if (!body.includes("Created with zdev")) {
134
+ body = body.trim() + "\n\n---\n*Created with [zdev](https://github.com/5hanth/zdev)*";
135
+ }
136
+
137
+ // Check if PR already exists
138
+ const existingPr = run("gh", ["pr", "view", branch, "--json", "url"], { cwd: worktreePath });
139
+ if (existingPr.success) {
140
+ try {
141
+ const prData = JSON.parse(existingPr.stdout);
142
+ console.log(`\n✅ PR already exists!`);
143
+ console.log(`\n🔗 ${prData.url}`);
144
+ return;
145
+ } catch {
146
+ // Continue to create
147
+ }
148
+ }
149
+
150
+ // Create PR
151
+ console.log(`\n📝 Creating pull request...`);
152
+
153
+ const prArgs = ["pr", "create", "--title", title, "--body", body];
154
+
155
+ if (options.draft) {
156
+ prArgs.push("--draft");
157
+ }
158
+
159
+ if (options.web) {
160
+ prArgs.push("--web");
161
+ const webResult = run("gh", prArgs, { cwd: worktreePath });
162
+ if (!webResult.success) {
163
+ console.error(` Failed: ${webResult.stderr}`);
164
+ process.exit(1);
165
+ }
166
+ console.log(` Opened in browser`);
167
+ return;
168
+ }
169
+
170
+ const prResult = run("gh", prArgs, { cwd: worktreePath });
171
+
172
+ if (!prResult.success) {
173
+ // Check if it's because PR already exists
174
+ if (prResult.stderr.includes("already exists")) {
175
+ console.log(` PR already exists for this branch`);
176
+ const viewResult = run("gh", ["pr", "view", "--json", "url"], { cwd: worktreePath });
177
+ if (viewResult.success) {
178
+ try {
179
+ const prData = JSON.parse(viewResult.stdout);
180
+ console.log(`\n🔗 ${prData.url}`);
181
+ } catch {
182
+ // Ignore
183
+ }
184
+ }
185
+ return;
186
+ }
187
+ console.error(` Failed: ${prResult.stderr}`);
188
+ process.exit(1);
189
+ }
190
+
191
+ // Extract PR URL from output
192
+ const prUrl = prResult.stdout.trim();
193
+
194
+ console.log(`\n✅ Pull request created!`);
195
+ console.log(`\n🔗 ${prUrl}`);
196
+
197
+ // Show preview URL again for easy access
198
+ if (allocation?.publicUrl) {
199
+ console.log(`\n📱 Preview: ${allocation.publicUrl}`);
200
+ }
201
+ }
@@ -105,10 +105,13 @@ export async function start(
105
105
  process.exit(1);
106
106
  }
107
107
 
108
- // Fetch latest
109
- console.log(`\n📥 Fetching latest from origin...`);
110
- if (!gitFetch(fullPath)) {
111
- console.error(` Failed to fetch, continuing anyway...`);
108
+ // Fetch latest (only if origin remote exists)
109
+ const hasOrigin = run("git", ["remote", "get-url", "origin"], { cwd: fullPath });
110
+ if (hasOrigin.success) {
111
+ console.log(`\n📥 Fetching latest from origin...`);
112
+ if (!gitFetch(fullPath)) {
113
+ console.error(` Failed to fetch, continuing anyway...`);
114
+ }
112
115
  }
113
116
 
114
117
  // Create worktree
@@ -298,6 +301,8 @@ export async function start(
298
301
  frontendPort: ports.frontend,
299
302
  convexPort: ports.convex,
300
303
  funnelPath: routePath,
304
+ worktreePath,
305
+ publicUrl: publicUrl || undefined,
301
306
  pids: {
302
307
  frontend: frontendPid,
303
308
  convex: convexPid,
package/src/config.ts CHANGED
@@ -15,6 +15,8 @@ export interface WorktreeAllocation {
15
15
  frontendPort: number;
16
16
  convexPort: number;
17
17
  funnelPath: string;
18
+ worktreePath?: string; // Full path to worktree
19
+ publicUrl?: string; // Public preview URL
18
20
  pids: {
19
21
  frontend?: number;
20
22
  convex?: number;
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import { list } from "./commands/list.js";
8
8
  import { clean } from "./commands/clean.js";
9
9
  import { seedExport, seedImport } from "./commands/seed.js";
10
10
  import { configCmd } from "./commands/config.js";
11
+ import { pr } from "./commands/pr.js";
11
12
  import { readFileSync } from "fs";
12
13
  import { fileURLToPath } from "url";
13
14
  import { dirname, join } from "path";
@@ -124,6 +125,24 @@ program
124
125
  await configCmd(options);
125
126
  });
126
127
 
128
+ // zdev pr
129
+ program
130
+ .command("pr [feature]")
131
+ .description("Create a pull request for a feature branch")
132
+ .option("-p, --project <path>", "Project path", ".")
133
+ .option("-t, --title <title>", "PR title (auto-generated if not specified)")
134
+ .option("-b, --body <body>", "PR body (preview URL auto-added)")
135
+ .option("-d, --draft", "Create as draft PR")
136
+ .option("-w, --web", "Open in browser instead of CLI")
137
+ .action(async (feature, options) => {
138
+ await pr(feature, options.project, {
139
+ title: options.title,
140
+ body: options.body,
141
+ draft: options.draft,
142
+ web: options.web,
143
+ });
144
+ });
145
+
127
146
  // zdev status (alias for list)
128
147
  program
129
148
  .command("status")