create-claude-workspace 2.3.2 → 2.3.4
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.
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
// ─── PR/MR lifecycle management ───
|
|
2
2
|
// Deterministic TS, zero AI tokens. Uses gh/glab CLI.
|
|
3
|
-
|
|
3
|
+
// All CLI calls use execFileSync (no shell) to safely pass arbitrary text
|
|
4
|
+
// containing backticks, $(), quotes, etc. without shell interpretation.
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
4
6
|
const READ_TIMEOUT = 30_000;
|
|
5
7
|
const WRITE_TIMEOUT = 60_000;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
/** Shell-safe CLI execution — no shell interpretation of arguments */
|
|
9
|
+
function run(bin, args, cwd, timeout = READ_TIMEOUT) {
|
|
10
|
+
return execFileSync(bin, args, { cwd, timeout, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
8
11
|
}
|
|
9
12
|
export function createPR(opts) {
|
|
10
13
|
const body = opts.issueNumber
|
|
11
14
|
? `${opts.body}\n\nCloses #${opts.issueNumber}`
|
|
12
15
|
: opts.body;
|
|
13
|
-
// Escape body for shell — write to temp approach avoided, use stdin-like quoting
|
|
14
|
-
const escapedBody = body.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
15
|
-
const escapedTitle = opts.title.replace(/"/g, '\\"');
|
|
16
16
|
if (opts.platform === 'github') {
|
|
17
|
-
const output =
|
|
17
|
+
const output = run('gh', [
|
|
18
|
+
'pr', 'create',
|
|
19
|
+
'--title', opts.title,
|
|
20
|
+
'--body', body,
|
|
21
|
+
'--base', opts.baseBranch,
|
|
22
|
+
'--head', opts.branch,
|
|
23
|
+
'--json', 'number,url',
|
|
24
|
+
], opts.cwd, WRITE_TIMEOUT);
|
|
18
25
|
const pr = JSON.parse(output);
|
|
19
26
|
return {
|
|
20
27
|
number: pr.number,
|
|
@@ -26,8 +33,15 @@ export function createPR(opts) {
|
|
|
26
33
|
comments: [],
|
|
27
34
|
};
|
|
28
35
|
}
|
|
29
|
-
// GitLab —
|
|
30
|
-
const output =
|
|
36
|
+
// GitLab — execFileSync passes --description value directly, no shell expansion
|
|
37
|
+
const output = run('glab', [
|
|
38
|
+
'mr', 'create',
|
|
39
|
+
'--title', opts.title,
|
|
40
|
+
'--description', body,
|
|
41
|
+
'--target-branch', opts.baseBranch,
|
|
42
|
+
'--source-branch', opts.branch,
|
|
43
|
+
'--no-editor',
|
|
44
|
+
], opts.cwd, WRITE_TIMEOUT);
|
|
31
45
|
// glab outputs the MR URL, e.g. "https://gitlab.com/group/repo/-/merge_requests/7"
|
|
32
46
|
const urlMatch = output.match(/https?:\/\/\S+merge_requests\/(\d+)/);
|
|
33
47
|
const mrIid = urlMatch ? parseInt(urlMatch[1], 10) : 0;
|
|
@@ -50,7 +64,10 @@ export function getPRStatus(cwd, platform, branch) {
|
|
|
50
64
|
return getGitLabMRStatus(cwd, branch);
|
|
51
65
|
}
|
|
52
66
|
function getGitHubPRStatus(cwd, branch) {
|
|
53
|
-
const output =
|
|
67
|
+
const output = run('gh', [
|
|
68
|
+
'pr', 'view', branch,
|
|
69
|
+
'--json', 'number,url,state,mergeable,reviewDecision,statusCheckRollup,comments',
|
|
70
|
+
], cwd);
|
|
54
71
|
const pr = JSON.parse(output);
|
|
55
72
|
const ciStatus = resolveGitHubCIStatus(pr.statusCheckRollup ?? []);
|
|
56
73
|
const approvals = pr.reviewDecision === 'APPROVED' ? 1 : 0;
|
|
@@ -82,11 +99,11 @@ function parseGitHubComments(comments) {
|
|
|
82
99
|
id: c.id,
|
|
83
100
|
author: c.author.login,
|
|
84
101
|
body: c.body,
|
|
85
|
-
resolved: false,
|
|
102
|
+
resolved: false,
|
|
86
103
|
}));
|
|
87
104
|
}
|
|
88
105
|
function getGitLabMRStatus(cwd, branch) {
|
|
89
|
-
const output =
|
|
106
|
+
const output = run('glab', ['mr', 'view', branch, '--output', 'json'], cwd);
|
|
90
107
|
const mr = JSON.parse(output);
|
|
91
108
|
const ciStatus = resolveGitLabCIStatus(mr.head_pipeline);
|
|
92
109
|
const mergeable = mr.merge_status === 'can_be_merged' && ciStatus === 'passed';
|
|
@@ -95,9 +112,9 @@ function getGitLabMRStatus(cwd, branch) {
|
|
|
95
112
|
url: mr.web_url,
|
|
96
113
|
status: mr.state === 'merged' ? 'merged' : mr.state === 'closed' ? 'closed' : 'open',
|
|
97
114
|
ciStatus,
|
|
98
|
-
approvals: 0,
|
|
115
|
+
approvals: 0,
|
|
99
116
|
mergeable,
|
|
100
|
-
comments: [],
|
|
117
|
+
comments: [],
|
|
101
118
|
};
|
|
102
119
|
}
|
|
103
120
|
function resolveGitLabCIStatus(pipeline) {
|
|
@@ -119,7 +136,10 @@ export function getPRComments(cwd, platform, prNumber) {
|
|
|
119
136
|
}
|
|
120
137
|
function getGitHubPRComments(cwd, prNumber) {
|
|
121
138
|
try {
|
|
122
|
-
const output =
|
|
139
|
+
const output = run('gh', [
|
|
140
|
+
'api', `repos/{owner}/{repo}/pulls/${prNumber}/reviews`,
|
|
141
|
+
'--jq', '.[] | select(.state != "APPROVED") | {id: .id, user: .user.login, body: .body, state: .state}',
|
|
142
|
+
], cwd);
|
|
123
143
|
if (!output)
|
|
124
144
|
return [];
|
|
125
145
|
return output.split('\n').filter(Boolean).map(line => {
|
|
@@ -138,7 +158,9 @@ function getGitHubPRComments(cwd, prNumber) {
|
|
|
138
158
|
}
|
|
139
159
|
function getGitLabMRComments(cwd, mrIid) {
|
|
140
160
|
try {
|
|
141
|
-
const output =
|
|
161
|
+
const output = run('glab', [
|
|
162
|
+
'api', `projects/:id/merge_requests/${mrIid}/notes`, '--paginate',
|
|
163
|
+
], cwd);
|
|
142
164
|
if (!output)
|
|
143
165
|
return [];
|
|
144
166
|
const notes = JSON.parse(output);
|
|
@@ -160,11 +182,13 @@ export function mergePR(cwd, platform, prNumber, method = 'merge') {
|
|
|
160
182
|
try {
|
|
161
183
|
if (platform === 'github') {
|
|
162
184
|
const flag = method === 'squash' ? '--squash' : method === 'rebase' ? '--rebase' : '--merge';
|
|
163
|
-
|
|
185
|
+
run('gh', ['pr', 'merge', String(prNumber), flag, '--delete-branch'], cwd, WRITE_TIMEOUT);
|
|
164
186
|
}
|
|
165
187
|
else {
|
|
166
|
-
const
|
|
167
|
-
|
|
188
|
+
const args = ['mr', 'merge', String(prNumber), '--remove-source-branch', '--yes'];
|
|
189
|
+
if (method === 'squash')
|
|
190
|
+
args.push('--squash');
|
|
191
|
+
run('glab', args, cwd, WRITE_TIMEOUT);
|
|
168
192
|
}
|
|
169
193
|
return true;
|
|
170
194
|
}
|
|
@@ -172,32 +196,26 @@ export function mergePR(cwd, platform, prNumber, method = 'merge') {
|
|
|
172
196
|
return false;
|
|
173
197
|
}
|
|
174
198
|
}
|
|
175
|
-
function mrIid(n) {
|
|
176
|
-
return String(n);
|
|
177
|
-
}
|
|
178
199
|
// ─── Issue label management ───
|
|
179
200
|
export function updateIssueLabels(cwd, platform, issueNumber, add, remove) {
|
|
180
201
|
try {
|
|
181
202
|
if (platform === 'github') {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
cli(`gh issue edit ${issueNumber} --remove-label "${label}"`, cwd);
|
|
186
|
-
}
|
|
187
|
-
catch { /* label may not exist */ }
|
|
203
|
+
for (const label of remove) {
|
|
204
|
+
try {
|
|
205
|
+
run('gh', ['issue', 'edit', String(issueNumber), '--remove-label', label], cwd);
|
|
188
206
|
}
|
|
207
|
+
catch { /* label may not exist */ }
|
|
189
208
|
}
|
|
190
209
|
if (add.length > 0) {
|
|
191
|
-
|
|
210
|
+
run('gh', ['issue', 'edit', String(issueNumber), '--add-label', add.join(',')], cwd);
|
|
192
211
|
}
|
|
193
212
|
}
|
|
194
213
|
else {
|
|
195
|
-
// GitLab: comma-separated labels
|
|
196
214
|
if (remove.length > 0) {
|
|
197
|
-
|
|
215
|
+
run('glab', ['issue', 'update', String(issueNumber), '--unlabel', remove.join(',')], cwd);
|
|
198
216
|
}
|
|
199
217
|
if (add.length > 0) {
|
|
200
|
-
|
|
218
|
+
run('glab', ['issue', 'update', String(issueNumber), '--label', add.join(',')], cwd);
|
|
201
219
|
}
|
|
202
220
|
}
|
|
203
221
|
}
|
|
@@ -207,10 +225,10 @@ export function updateIssueLabels(cwd, platform, issueNumber, add, remove) {
|
|
|
207
225
|
export function closePR(cwd, platform, prNumber) {
|
|
208
226
|
try {
|
|
209
227
|
if (platform === 'github') {
|
|
210
|
-
|
|
228
|
+
run('gh', ['pr', 'close', String(prNumber)], cwd);
|
|
211
229
|
}
|
|
212
230
|
else {
|
|
213
|
-
|
|
231
|
+
run('glab', ['mr', 'close', String(prNumber)], cwd);
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
234
|
catch { /* best-effort */ }
|
|
@@ -73,7 +73,10 @@ If the user provided a Figma URL:
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
|
-
- **IMPORTANT**: Add
|
|
76
|
+
- **IMPORTANT**: Add these to `.gitignore`:
|
|
77
|
+
- `.claude/settings.json` — contains API token, MUST NOT be committed
|
|
78
|
+
- `.claude/scheduler/` — runtime state (state.json, logs, inbox), session-specific
|
|
79
|
+
- `.worktrees/` — git worktrees for parallel task execution
|
|
77
80
|
- Verify the MCP server works by attempting to use a Figma tool.
|
|
78
81
|
3. **Store the Figma URL** — save it for Step 4 (CLAUDE.md generation). The URL will be written into CLAUDE.md so agents know where to find designs.
|
|
79
82
|
|
|
@@ -40,6 +40,15 @@ When running in autonomous/unattended mode (via `autonomous.mjs` or non-interact
|
|
|
40
40
|
- **Workflow**: solo / team (solo = direct merge to main, team = MR/PR review required)
|
|
41
41
|
- **Task Platform**: local | github | gitlab
|
|
42
42
|
|
|
43
|
+
### .gitignore (scheduler runtime)
|
|
44
|
+
|
|
45
|
+
Ensure these are in `.gitignore`:
|
|
46
|
+
```
|
|
47
|
+
.claude/scheduler/
|
|
48
|
+
.claude/settings.json
|
|
49
|
+
.worktrees/
|
|
50
|
+
```
|
|
51
|
+
|
|
43
52
|
[If npm publishing (question 6 = yes):]
|
|
44
53
|
- **Distribution**: npm ([registry], [public/restricted])
|
|
45
54
|
|