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
- import { execSync } from 'node:child_process';
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
- function cli(cmd, cwd, timeout = READ_TIMEOUT) {
7
- return execSync(cmd, { cwd, timeout, stdio: 'pipe', encoding: 'utf-8' }).trim();
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 = cli(`gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base "${opts.baseBranch}" --head "${opts.branch}" --json number,url`, opts.cwd, WRITE_TIMEOUT);
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 — glab mr create doesn't support --json, parse URL from output
30
- const output = cli(`glab mr create --title "${escapedTitle}" --description "${escapedBody}" --target-branch "${opts.baseBranch}" --source-branch "${opts.branch}" --no-editor`, opts.cwd, WRITE_TIMEOUT);
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 = cli(`gh pr view "${branch}" --json number,url,state,mergeable,reviewDecision,statusCheckRollup,comments`, cwd);
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, // GitHub PR comments don't have resolved state (only review threads do)
102
+ resolved: false,
86
103
  }));
87
104
  }
88
105
  function getGitLabMRStatus(cwd, branch) {
89
- const output = cli(`glab mr view "${branch}" --output json`, cwd);
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, // Would need separate API call; scheduler handles via mergeable check
115
+ approvals: 0,
99
116
  mergeable,
100
- comments: [], // Fetched separately via getPRComments
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 = cli(`gh api repos/{owner}/{repo}/pulls/${prNumber}/reviews --jq '.[] | select(.state != "APPROVED") | {id: .id, user: .user.login, body: .body, state: .state}'`, cwd);
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 = cli(`glab api projects/:id/merge_requests/${mrIid}/notes --paginate`, cwd);
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
- cli(`gh pr merge ${prNumber} ${flag} --delete-branch`, cwd, WRITE_TIMEOUT);
185
+ run('gh', ['pr', 'merge', String(prNumber), flag, '--delete-branch'], cwd, WRITE_TIMEOUT);
164
186
  }
165
187
  else {
166
- const flag = method === 'squash' ? '--squash' : '';
167
- cli(`glab mr merge ${mrIid(prNumber)} ${flag} --remove-source-branch --yes`, cwd, WRITE_TIMEOUT);
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
- if (remove.length > 0) {
183
- for (const label of remove) {
184
- try {
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
- cli(`gh issue edit ${issueNumber} --add-label "${add.join(',')}"`, cwd);
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
- cli(`glab issue update ${issueNumber} --unlabel "${remove.join(',')}"`, cwd);
215
+ run('glab', ['issue', 'update', String(issueNumber), '--unlabel', remove.join(',')], cwd);
198
216
  }
199
217
  if (add.length > 0) {
200
- cli(`glab issue update ${issueNumber} --label "${add.join(',')}"`, cwd);
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
- cli(`gh pr close ${prNumber}`, cwd);
228
+ run('gh', ['pr', 'close', String(prNumber)], cwd);
211
229
  }
212
230
  else {
213
- cli(`glab mr close ${prNumber}`, cwd);
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 `.claude/settings.json` to `.gitignore` — it contains the API token and MUST NOT be committed.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",