zdev 0.1.5 → 0.2.1

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
@@ -2663,6 +2663,8 @@ async function start(featureName, projectPath = ".", options = {}) {
2663
2663
  frontendPort: ports.frontend,
2664
2664
  convexPort: ports.convex,
2665
2665
  funnelPath: routePath,
2666
+ worktreePath,
2667
+ publicUrl: publicUrl || undefined,
2666
2668
  pids: {
2667
2669
  frontend: frontendPid,
2668
2670
  convex: convexPid
@@ -3069,6 +3071,184 @@ Commands:`);
3069
3071
  }
3070
3072
  }
3071
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
+ const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath });
3143
+ const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
3144
+ const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath });
3145
+ const commits = commitsResult.success ? commitsResult.stdout.trim().split(`
3146
+ `).filter(Boolean) : [];
3147
+ const filesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--stat", "--stat-width=60"], { cwd: worktreePath });
3148
+ const filesSummary = filesResult.success ? filesResult.stdout.trim() : "";
3149
+ const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath });
3150
+ const diffStat = diffStatResult.success ? diffStatResult.stdout.trim() : "";
3151
+ let title = options.title;
3152
+ if (!title) {
3153
+ if (commits.length > 0) {
3154
+ title = commits[0];
3155
+ } else {
3156
+ const featureForTitle = featureName || branch.replace(/^feature\//, "");
3157
+ title = featureForTitle.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
3158
+ }
3159
+ }
3160
+ let body = options.body || "";
3161
+ if (allocation && allocation.publicUrl) {
3162
+ body += `## Preview
3163
+ \uD83D\uDD17 ${allocation.publicUrl}
3164
+
3165
+ `;
3166
+ } else if (config.devDomain && featureName && projectName) {
3167
+ const previewUrl = `https://${projectName}-${featureName}.${config.devDomain}`;
3168
+ body += `## Preview
3169
+ \uD83D\uDD17 ${previewUrl}
3170
+
3171
+ `;
3172
+ }
3173
+ if (commits.length > 0) {
3174
+ body += `## Changes
3175
+ `;
3176
+ commits.forEach((commit) => {
3177
+ body += `- ${commit}
3178
+ `;
3179
+ });
3180
+ body += `
3181
+ `;
3182
+ }
3183
+ if (diffStat) {
3184
+ body += `## Summary
3185
+ \`\`\`
3186
+ ${diffStat}
3187
+ \`\`\`
3188
+
3189
+ `;
3190
+ }
3191
+ if (!body.includes("Created with zdev")) {
3192
+ body = body.trim() + `
3193
+
3194
+ ---
3195
+ *Created with [zdev](https://github.com/5hanth/zdev)*`;
3196
+ }
3197
+ const existingPr = run("gh", ["pr", "view", branch, "--json", "url"], { cwd: worktreePath });
3198
+ if (existingPr.success) {
3199
+ try {
3200
+ const prData = JSON.parse(existingPr.stdout);
3201
+ console.log(`
3202
+ ✅ PR already exists!`);
3203
+ console.log(`
3204
+ \uD83D\uDD17 ${prData.url}`);
3205
+ return;
3206
+ } catch {}
3207
+ }
3208
+ console.log(`
3209
+ \uD83D\uDCDD Creating pull request...`);
3210
+ const prArgs = ["pr", "create", "--title", title, "--body", body];
3211
+ if (options.draft) {
3212
+ prArgs.push("--draft");
3213
+ }
3214
+ if (options.web) {
3215
+ prArgs.push("--web");
3216
+ const webResult = run("gh", prArgs, { cwd: worktreePath });
3217
+ if (!webResult.success) {
3218
+ console.error(` Failed: ${webResult.stderr}`);
3219
+ process.exit(1);
3220
+ }
3221
+ console.log(` Opened in browser`);
3222
+ return;
3223
+ }
3224
+ const prResult = run("gh", prArgs, { cwd: worktreePath });
3225
+ if (!prResult.success) {
3226
+ if (prResult.stderr.includes("already exists")) {
3227
+ console.log(` PR already exists for this branch`);
3228
+ const viewResult = run("gh", ["pr", "view", "--json", "url"], { cwd: worktreePath });
3229
+ if (viewResult.success) {
3230
+ try {
3231
+ const prData = JSON.parse(viewResult.stdout);
3232
+ console.log(`
3233
+ \uD83D\uDD17 ${prData.url}`);
3234
+ } catch {}
3235
+ }
3236
+ return;
3237
+ }
3238
+ console.error(` Failed: ${prResult.stderr}`);
3239
+ process.exit(1);
3240
+ }
3241
+ const prUrl = prResult.stdout.trim();
3242
+ console.log(`
3243
+ ✅ Pull request created!`);
3244
+ console.log(`
3245
+ \uD83D\uDD17 ${prUrl}`);
3246
+ if (allocation?.publicUrl) {
3247
+ console.log(`
3248
+ \uD83D\uDCF1 Preview: ${allocation.publicUrl}`);
3249
+ }
3250
+ }
3251
+
3072
3252
  // src/index.ts
3073
3253
  import { readFileSync as readFileSync5 } from "fs";
3074
3254
  import { fileURLToPath } from "url";
@@ -3114,6 +3294,14 @@ seedCmd.command("import [path]").description("Import seed data into current work
3114
3294
  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) => {
3115
3295
  await configCmd(options);
3116
3296
  });
3297
+ 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) => {
3298
+ await pr(feature, options.project, {
3299
+ title: options.title,
3300
+ body: options.body,
3301
+ draft: options.draft,
3302
+ web: options.web
3303
+ });
3304
+ });
3117
3305
  program2.command("status").description("Show zdev status (alias for list)").action(async () => {
3118
3306
  await list({});
3119
3307
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdev",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "Multi-agent worktree development environment for cloud dev with preview URLs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,234 @@
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
+ // Get base branch for comparison
108
+ const defaultBranch = run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], { cwd: worktreePath });
109
+ const baseBranch = defaultBranch.success ? defaultBranch.stdout.trim().replace("origin/", "") : "main";
110
+
111
+ // Get commits since base branch
112
+ const commitsResult = run("git", ["log", `origin/${baseBranch}..HEAD`, "--pretty=format:%s"], { cwd: worktreePath });
113
+ const commits = commitsResult.success ? commitsResult.stdout.trim().split("\n").filter(Boolean) : [];
114
+
115
+ // Get files changed
116
+ const filesResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--stat", "--stat-width=60"], { cwd: worktreePath });
117
+ const filesSummary = filesResult.success ? filesResult.stdout.trim() : "";
118
+
119
+ // Get short diff summary
120
+ const diffStatResult = run("git", ["diff", `origin/${baseBranch}..HEAD`, "--shortstat"], { cwd: worktreePath });
121
+ const diffStat = diffStatResult.success ? diffStatResult.stdout.trim() : "";
122
+
123
+ // Build PR title
124
+ let title = options.title;
125
+ if (!title) {
126
+ if (commits.length > 0) {
127
+ // Use first commit message as title
128
+ title = commits[0];
129
+ } else {
130
+ // Fallback to feature name
131
+ const featureForTitle = featureName || branch.replace(/^feature\//, "");
132
+ title = featureForTitle
133
+ .split(/[-_]/)
134
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
135
+ .join(" ");
136
+ }
137
+ }
138
+
139
+ // Build PR body
140
+ let body = options.body || "";
141
+
142
+ // Add preview URL if we have allocation
143
+ if (allocation && allocation.publicUrl) {
144
+ body += `## Preview\n🔗 ${allocation.publicUrl}\n\n`;
145
+ } else if (config.devDomain && featureName && projectName) {
146
+ // Try to construct preview URL
147
+ const previewUrl = `https://${projectName}-${featureName}.${config.devDomain}`;
148
+ body += `## Preview\n🔗 ${previewUrl}\n\n`;
149
+ }
150
+
151
+ // Add commits section if we have commits
152
+ if (commits.length > 0) {
153
+ body += `## Changes\n`;
154
+ commits.forEach((commit) => {
155
+ body += `- ${commit}\n`;
156
+ });
157
+ body += "\n";
158
+ }
159
+
160
+ // Add files changed
161
+ if (diffStat) {
162
+ body += `## Summary\n\`\`\`\n${diffStat}\n\`\`\`\n\n`;
163
+ }
164
+
165
+ // Add footer
166
+ if (!body.includes("Created with zdev")) {
167
+ body = body.trim() + "\n\n---\n*Created with [zdev](https://github.com/5hanth/zdev)*";
168
+ }
169
+
170
+ // Check if PR already exists
171
+ const existingPr = run("gh", ["pr", "view", branch, "--json", "url"], { cwd: worktreePath });
172
+ if (existingPr.success) {
173
+ try {
174
+ const prData = JSON.parse(existingPr.stdout);
175
+ console.log(`\n✅ PR already exists!`);
176
+ console.log(`\n🔗 ${prData.url}`);
177
+ return;
178
+ } catch {
179
+ // Continue to create
180
+ }
181
+ }
182
+
183
+ // Create PR
184
+ console.log(`\n📝 Creating pull request...`);
185
+
186
+ const prArgs = ["pr", "create", "--title", title, "--body", body];
187
+
188
+ if (options.draft) {
189
+ prArgs.push("--draft");
190
+ }
191
+
192
+ if (options.web) {
193
+ prArgs.push("--web");
194
+ const webResult = run("gh", prArgs, { cwd: worktreePath });
195
+ if (!webResult.success) {
196
+ console.error(` Failed: ${webResult.stderr}`);
197
+ process.exit(1);
198
+ }
199
+ console.log(` Opened in browser`);
200
+ return;
201
+ }
202
+
203
+ const prResult = run("gh", prArgs, { cwd: worktreePath });
204
+
205
+ if (!prResult.success) {
206
+ // Check if it's because PR already exists
207
+ if (prResult.stderr.includes("already exists")) {
208
+ console.log(` PR already exists for this branch`);
209
+ const viewResult = run("gh", ["pr", "view", "--json", "url"], { cwd: worktreePath });
210
+ if (viewResult.success) {
211
+ try {
212
+ const prData = JSON.parse(viewResult.stdout);
213
+ console.log(`\n🔗 ${prData.url}`);
214
+ } catch {
215
+ // Ignore
216
+ }
217
+ }
218
+ return;
219
+ }
220
+ console.error(` Failed: ${prResult.stderr}`);
221
+ process.exit(1);
222
+ }
223
+
224
+ // Extract PR URL from output
225
+ const prUrl = prResult.stdout.trim();
226
+
227
+ console.log(`\n✅ Pull request created!`);
228
+ console.log(`\n🔗 ${prUrl}`);
229
+
230
+ // Show preview URL again for easy access
231
+ if (allocation?.publicUrl) {
232
+ console.log(`\n📱 Preview: ${allocation.publicUrl}`);
233
+ }
234
+ }
@@ -301,6 +301,8 @@ export async function start(
301
301
  frontendPort: ports.frontend,
302
302
  convexPort: ports.convex,
303
303
  funnelPath: routePath,
304
+ worktreePath,
305
+ publicUrl: publicUrl || undefined,
304
306
  pids: {
305
307
  frontend: frontendPid,
306
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")