canary-test-cli 6.5.0 → 6.7.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.
@@ -30,10 +30,33 @@ const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
30
30
  function skippedSuffix(skipped) {
31
31
  if (!skipped || skipped.length === 0)
32
32
  return '';
33
- const names = skipped
34
- .map((s) => s.name.replace(CONTROL_CHARS, ''))
35
- .join(', ');
36
- return ` (${skipped.length} skipped: ${names})`;
33
+ // #579: `reason` used to be write-only -- every caller set one and no
34
+ // surface ever rendered it, so a reader could see THAT a path was dropped
35
+ // but never WHY. Name and cause now travel together, keeping the D7
36
+ // contract that the path stays visible rather than collapsing into a bare
37
+ // count.
38
+ //
39
+ // Grouped by reason, because producers write reasons at very different
40
+ // grain: the guardian uses short tokens (`skipGlobs`), doctor uses whole
41
+ // remedy sentences. Repeating the reason per name turned one doctor line
42
+ // into 183 characters that said the same thing twice. Groups keep
43
+ // first-appearance order, so output stays deterministic.
44
+ //
45
+ // Reasons are control-char stripped for the same line-forging reason names
46
+ // are -- rendering the field is what made it an injection surface.
47
+ const groups = new Map();
48
+ for (const entry of skipped) {
49
+ const reason = entry.reason.replace(CONTROL_CHARS, '');
50
+ const names = groups.get(reason);
51
+ if (names)
52
+ names.push(entry.name.replace(CONTROL_CHARS, ''));
53
+ else
54
+ groups.set(reason, [entry.name.replace(CONTROL_CHARS, '')]);
55
+ }
56
+ const rendered = [...groups]
57
+ .map(([reason, names]) => reason ? `${names.join(', ')} [${reason}]` : names.join(', '))
58
+ .join('; ');
59
+ return ` (${skipped.length} skipped: ${rendered})`;
37
60
  }
38
61
  /**
39
62
  * The single summary-line/exit-code path for swept commands.
@@ -109,7 +109,9 @@ function collectConsent(dest, name, confirm, out) {
109
109
  }
110
110
  const granted = confirm(`Allow 'canary doctor' to run these commands for "${name}"? [y/N] `);
111
111
  if (!granted) {
112
- out.write("Declined — 'canary doctor' will skip these command checks. Re-add the overlay to change this.\n");
112
+ out.write("Declined — 'canary doctor' will skip these command checks. Run the same " +
113
+ "'canary overlay add' again to change this; it re-asks consent and " +
114
+ 'never re-clones.\n');
113
115
  }
114
116
  return { consent: granted, consentCommandsHash: hash };
115
117
  }
@@ -184,8 +186,34 @@ function add(source, options = {}, deps = {}) {
184
186
  err.write(`canary overlay add: ${e.message}\n`);
185
187
  return 1;
186
188
  }
187
- // Idempotent: re-adding a registered overlay is a no-op with an update hint.
188
- if (registry.get(reg, parsed.name)) {
189
+ // Idempotent: re-adding a registered overlay never re-clones. It DOES
190
+ // re-run the consent prompt (#505), because `add` is the consent path every
191
+ // other surface points at -- doctor's skip remedy ("re-run 'canary overlay
192
+ // add'") and the decline message ("Re-add the overlay to change this") both
193
+ // said so while this branch returned before reaching `collectConsent`. The
194
+ // advice was a dead end: short of `overlay remove` + `add`, a user who
195
+ // declined once could never grant consent.
196
+ const existing = registry.get(reg, parsed.name);
197
+ if (existing) {
198
+ const refreshed = collectConsent(existing.path, parsed.name, deps.confirm ?? defaultConfirm, out);
199
+ if (refreshed.consentCommandsHash !== existing.consentCommandsHash ||
200
+ refreshed.consent !== existing.consent) {
201
+ const updated = {
202
+ ...existing,
203
+ consent: refreshed.consent,
204
+ consentCommandsHash: refreshed.consentCommandsHash,
205
+ };
206
+ try {
207
+ registry.write({
208
+ ...reg,
209
+ overlays: reg.overlays.map((o) => o.name === parsed.name ? updated : o),
210
+ }, homeDir);
211
+ }
212
+ catch (e) {
213
+ err.write(`canary overlay add: ${e.message}\n`);
214
+ return 1;
215
+ }
216
+ }
189
217
  out.write(`overlay "${parsed.name}" is already added — run 'canary overlay update ${parsed.name}' to refresh it.\n`);
190
218
  return 0;
191
219
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canary-test-cli",
3
- "version": "6.5.0",
3
+ "version": "6.7.0",
4
4
  "description": "Canary — AI-powered test automation agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "test": "tsc && node --test \"scripts/__tests__/*.test.js\""
35
35
  },
36
36
  "engines": {
37
- "node": ">=18"
37
+ "node": ">=22"
38
38
  },
39
39
  "files": [
40
40
  "bin/canary.js",