zdev 0.1.5 → 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
@@ -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,155 @@ 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
+ 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
+
3072
3223
  // src/index.ts
3073
3224
  import { readFileSync as readFileSync5 } from "fs";
3074
3225
  import { fileURLToPath } from "url";
@@ -3114,6 +3265,14 @@ seedCmd.command("import [path]").description("Import seed data into current work
3114
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) => {
3115
3266
  await configCmd(options);
3116
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
+ });
3117
3276
  program2.command("status").description("Show zdev status (alias for list)").action(async () => {
3118
3277
  await list({});
3119
3278
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zdev",
3
- "version": "0.1.5",
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
+ }
@@ -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")