worclaude 2.10.0 → 2.10.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/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to worclaude are documented in this file. Format loosely fol
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.10.1] — 2026-04-29
8
+
9
+ Adds opt-in scaffolding for Claude Code 2.1.113's `sandbox.network` deny/allow lists. Worclaude is a scaffolder, so the new `templates/settings/base.json` ships empty `deniedDomains` and `allowedDomains` stubs rather than an opinionated default list — project owners decide their own network policy. The merge paths for both fresh init (Scenario A) and existing-project init/upgrade (Scenarios B/C) union-merge the new arrays preserving any user-added domains, and a new `worclaude doctor` check warns when the block is missing or malformed (with `worclaude upgrade` as the remediation hint). Also bundles a Dependabot major bump to `commander` 14, which is now Node-20+-only and was unblocked by the v2.10.0 Node 18 drop.
10
+
11
+ ### Added
12
+
13
+ - **Sandbox network scaffolding** (PR #172) — `templates/settings/base.json` now scaffolds `sandbox.network.deniedDomains: []` and `allowedDomains: []` between the existing `permissions` and `hooks` blocks. New `mergeSettings` helper `unionStringList(inputs, accessor)` in `src/core/scaffolder.js` handles both `permissions.allow` and the new sandbox arrays uniformly. Backward compatible: a base without `sandbox` produces output without `sandbox`, so legacy callers in tests or downstream consumers don't surface the key spuriously.
14
+ - **`appendUnique(target, key, source)` helper** in `src/core/merger.js` (PR #172) — folds three previously-duplicated union-merge call sites in `mergeSettingsPermissionsAndHooks` (allow / deny / sandbox-arrays) into one-liners. Extracted during a `/simplify` pass after three parallel review agents flagged the duplication.
15
+ - **`checkSandboxBlock` doctor check** (PR #172) — warns when `settings.json` is missing the `sandbox` block (with a `worclaude upgrade` remediation pointer for legacy installs), when `sandbox.network` is malformed, or when either array isn't actually an array.
16
+
17
+ ### Changed
18
+
19
+ - **`commander` 13.1.0 → 14.0.3** (PR #171) — Dependabot major bump. Commander 14 requires Node 20+ (already satisfied after v2.10.0's Node 18 drop) and adds `helpGroup`/`optionsGroup`/`commandsGroup` APIs plus unescaped negative-number support. Worclaude's CLI surface is unaffected.
20
+ - ⚠ **PR #171 shipped without a `Version bump:` declaration** — Dependabot-generated body, no manual annotation. Treated as `none` per `/sync`'s "missing → none" rule and surfaced here permanently. PR #172's `patch` declaration drove the release.
21
+
22
+ ### Tests
23
+
24
+ - 967 → 992 (+25 net). Per-stack sandbox-array assertions across all 16 supported language templates (replaced one all-stacks loop test for individual failure attribution); 3 scaffolder unit tests covering union-merge, dedup, and legacy-passthrough; 2 Scenario B regressions for legacy-install upgrade and user-domain preservation through subsequent merges; 2 doctor checks for missing-block (legacy install) and malformed-block scenarios.
25
+
26
+ Release group: 2 PRs (1 patch, 1 missing-declaration treated as none). v2.10.0 → v2.10.1.
27
+
7
28
  ## [2.10.0] — 2026-04-29
8
29
 
9
30
  Drops support for Node 18, which reached LTS end-of-life on 2025-04-30 (12 months before this release). The drop unblocks two Dependabot PRs stuck on Node-20-only features (`inquirer 13`'s `util.styleText` and `ora 9`'s regex `v` flag) and ships those bumps in the same release. Also recovers from a Dependabot routing misconfiguration: `.github/dependabot.yml` now declares `target-branch: develop` for both ecosystems, fixing a config gap that caused 5 PRs in the v2.9.3 → v2.10.0 window to be opened against main instead of develop. Their content is preserved across both branches via a recovery sync.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worclaude",
3
- "version": "2.10.0",
3
+ "version": "2.10.1",
4
4
  "description": "The Workflow Layer for Claude Code — scaffold agents, commands, skills, hooks, and memory into any project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -66,7 +66,7 @@
66
66
  "dependencies": {
67
67
  "@sefaertunc/anthropic-watch-client": "^1.0.2",
68
68
  "chalk": "^5.4.1",
69
- "commander": "^13.1.0",
69
+ "commander": "^14.0.3",
70
70
  "fs-extra": "^11.3.0",
71
71
  "inquirer": "^13.4.2",
72
72
  "ora": "^9.4.0",
@@ -286,6 +286,36 @@ async function readSettingsJson(projectRoot) {
286
286
  }
287
287
  }
288
288
 
289
+ async function checkSandboxBlock(projectRoot) {
290
+ const settings = await readSettingsJson(projectRoot);
291
+ if (!settings) return null;
292
+
293
+ if (!settings.sandbox) {
294
+ return result(
295
+ WARN,
296
+ 'Sandbox block',
297
+ 'settings.json missing `sandbox` block. Run `worclaude upgrade` to scaffold network deny/allow lists.'
298
+ );
299
+ }
300
+
301
+ const network = settings.sandbox.network;
302
+ if (!network || typeof network !== 'object') {
303
+ return result(WARN, 'Sandbox block', '`sandbox.network` block missing or malformed');
304
+ }
305
+
306
+ const issues = [];
307
+ if (!Array.isArray(network.deniedDomains)) {
308
+ issues.push('`deniedDomains` not an array');
309
+ }
310
+ if (!Array.isArray(network.allowedDomains)) {
311
+ issues.push('`allowedDomains` not an array');
312
+ }
313
+ if (issues.length > 0) {
314
+ return result(WARN, 'Sandbox block', issues.join('; '));
315
+ }
316
+ return result(PASS, 'Sandbox block', null);
317
+ }
318
+
289
319
  async function checkHookEventNames(projectRoot) {
290
320
  const settings = await readSettingsJson(projectRoot);
291
321
  if (!settings) {
@@ -1047,6 +1077,7 @@ export async function doctorCommand(options = {}) {
1047
1077
  record('core', await checkClaudeMdMemoryGuidance(projectRoot));
1048
1078
  record('core', await checkAgentsMd(projectRoot));
1049
1079
  record('core', await checkSettingsJson(projectRoot));
1080
+ record('core', await checkSandboxBlock(projectRoot));
1050
1081
  record('core', await checkSessions(projectRoot));
1051
1082
  spacer();
1052
1083
 
@@ -241,20 +241,28 @@ export async function mergeSettingsPermissionsAndHooks(
241
241
  const existing = parseUserJson(existingRaw, '.claude/settings.json');
242
242
 
243
243
  // Merge permissions (Tier 1) — union-merge both allow and deny
244
- const existingAllow = existing.permissions?.allow || [];
245
- const workflowAllow = workflowSettings.permissions?.allow || [];
246
- const newAllow = workflowAllow.filter((p) => !existingAllow.includes(p));
247
244
  if (!existing.permissions) existing.permissions = {};
248
- existing.permissions.allow = [...existingAllow, ...newAllow];
245
+ const newAllow = appendUnique(existing.permissions, 'allow', workflowSettings.permissions?.allow);
249
246
 
250
- const existingDeny = existing.permissions?.deny || [];
251
- const workflowDeny = workflowSettings.permissions?.deny || [];
252
- const newDeny = workflowDeny.filter((p) => !existingDeny.includes(p));
253
- if (newDeny.length > 0 || existingDeny.length > 0) {
254
- existing.permissions.deny = [...existingDeny, ...newDeny];
247
+ const existingDenyLen = (existing.permissions.deny ?? []).length;
248
+ const newDeny = appendUnique(existing.permissions, 'deny', workflowSettings.permissions?.deny);
249
+ if (existingDenyLen === 0 && newDeny === 0) {
250
+ delete existing.permissions.deny;
255
251
  }
256
252
 
257
- report.added.permissions = newAllow.length + newDeny.length;
253
+ report.added.permissions = newAllow + newDeny;
254
+
255
+ // Merge sandbox block (Tier 1 — additive, preserves user customizations)
256
+ const workflowSandbox = workflowSettings.sandbox?.network;
257
+ if (workflowSandbox) {
258
+ if (!existing.sandbox) existing.sandbox = {};
259
+ if (!existing.sandbox.network || typeof existing.sandbox.network !== 'object') {
260
+ existing.sandbox.network = {};
261
+ }
262
+ for (const key of ['deniedDomains', 'allowedDomains']) {
263
+ appendUnique(existing.sandbox.network, key, workflowSandbox[key]);
264
+ }
265
+ }
258
266
 
259
267
  // Merge hooks (Tier 1 + Tier 3)
260
268
  if (!existing.hooks) existing.hooks = {};
@@ -360,6 +368,13 @@ async function mergeSettingsJson(projectRoot, existingScan, selections, report)
360
368
  }
361
369
  }
362
370
 
371
+ function appendUnique(target, key, source) {
372
+ const existing = Array.isArray(target[key]) ? target[key] : [];
373
+ const additions = Array.isArray(source) ? source.filter((d) => !existing.includes(d)) : [];
374
+ target[key] = [...existing, ...additions];
375
+ return additions.length;
376
+ }
377
+
363
378
  function countHooks(hooks) {
364
379
  if (!hooks) return 0;
365
380
  return Object.values(hooks).reduce((sum, entries) => sum + entries.length, 0);
@@ -204,16 +204,31 @@ export async function scaffoldMemoryDocs(projectRoot) {
204
204
 
205
205
  export function mergeSettings(base, ...stacks) {
206
206
  const merged = JSON.parse(JSON.stringify(base));
207
- const baseAllow = merged.permissions?.allow || [];
207
+ const inputs = [base, ...stacks].filter(Boolean);
208
208
 
209
- for (const stack of stacks) {
210
- if (!stack) continue;
211
- const stackAllow = stack.permissions?.allow || [];
212
- if (stackAllow.length > 0) {
213
- baseAllow.push(...stackAllow);
214
- }
209
+ merged.permissions.allow = unionStringList(inputs, (i) => i.permissions?.allow);
210
+
211
+ if (merged.sandbox?.network) {
212
+ merged.sandbox.network.deniedDomains = unionStringList(
213
+ inputs,
214
+ (i) => i.sandbox?.network?.deniedDomains
215
+ );
216
+ merged.sandbox.network.allowedDomains = unionStringList(
217
+ inputs,
218
+ (i) => i.sandbox?.network?.allowedDomains
219
+ );
215
220
  }
216
221
 
217
- merged.permissions.allow = [...new Set(baseAllow)];
218
222
  return merged;
219
223
  }
224
+
225
+ function unionStringList(inputs, accessor) {
226
+ const set = new Set();
227
+ for (const input of inputs) {
228
+ const list = accessor(input);
229
+ if (Array.isArray(list)) {
230
+ for (const item of list) set.add(item);
231
+ }
232
+ }
233
+ return [...set];
234
+ }
@@ -81,6 +81,12 @@
81
81
  "Bash(rm -rf $HOME)", "Bash(rm -rf $HOME/*)"
82
82
  ]
83
83
  },
84
+ "sandbox": {
85
+ "network": {
86
+ "deniedDomains": [],
87
+ "allowedDomains": []
88
+ }
89
+ },
84
90
  "hooks": {
85
91
  "PostToolUse": [
86
92
  {