ikanban 0.3.1 → 0.3.3

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/lib/roadmap.js CHANGED
@@ -156,6 +156,9 @@ if command -v npx >/dev/null 2>&1; then
156
156
  fi
157
157
  `;
158
158
  const AGENT_COMMAND = "node .iKanban/hooks/roadmap-sync.js";
159
+ // IKA-2244: Claude Code runs a hook in the session's working folder, which is often not the
160
+ // repository root; $CLAUDE_PROJECT_DIR is the root (Claude Code hooks reference).
161
+ const CLAUDE_COMMAND = 'node "$CLAUDE_PROJECT_DIR/.iKanban/hooks/roadmap-sync.js"';
159
162
 
160
163
  function mergeJson(file, update) {
161
164
  const cur = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : {};
@@ -164,18 +167,21 @@ function mergeJson(file, update) {
164
167
  fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
165
168
  }
166
169
 
167
- /** Adds one entry to hooks[event] unless an entry already runs our command. */
170
+ /** Adds one entry to hooks[event]; an entry that already runs roadmap-sync.js is replaced
171
+ * by it (IKA-2244: so a re-install fixes an older install's command), never doubled. */
168
172
  function addHook(settings, event, entry) {
169
173
  settings.hooks = settings.hooks || {};
170
174
  const list = (settings.hooks[event] = settings.hooks[event] || []);
171
- if (!JSON.stringify(list).includes("roadmap-sync.js")) list.push(entry);
175
+ const at = list.findIndex((e) => JSON.stringify(e).includes("roadmap-sync.js"));
176
+ if (at < 0) list.push(entry);
177
+ else list[at] = entry;
172
178
  return settings;
173
179
  }
174
180
 
175
181
  const AGENTS = {
176
182
  claude: (root) => {
177
183
  mergeJson(path.join(root, ".claude", "settings.json"), (s) =>
178
- addHook(s, "PostToolUse", { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: AGENT_COMMAND, timeout: 90 }] }));
184
+ addHook(s, "PostToolUse", { matcher: "Write|Edit|MultiEdit", hooks: [{ type: "command", command: CLAUDE_COMMAND, timeout: 90 }] }));
179
185
  return ".claude/settings.json (PostToolUse)";
180
186
  },
181
187
  gemini: (root) => {
@@ -224,6 +230,9 @@ function installHooks(opts) {
224
230
  }
225
231
  for (const d of done) console.log(` installed: ${d}`);
226
232
  console.log("Plan files (JSON with a top-level \"roadmap\" block) now reach the Roadmap on every agent write and every git push.");
233
+ // IKA-2244: the sync's default server is prod; say which one this repository will use.
234
+ const base = connection({}, path.join(root, "plan.json")).base;
235
+ console.log(` server: ${base} (set api_url in .iKanban/settings-local.json, or IKANBAN_BASE_URL, to use another)`);
227
236
  return 0;
228
237
  }
229
238
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ikanban",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "iKanban CLI: AI and security scans of your own repo, with results filed into iKanban",
5
5
  "bin": { "ikanban": "bin/ikanban.js" },
6
6
  "files": ["bin", "lib", "hooks", "scanners", "README.md"],
@@ -64,7 +64,7 @@ from pathlib import Path
64
64
  # that npm no longer hosts the package. Starting at 1.0.0 rather than continuing
65
65
  # the npm package's 0.1.x line: that counter belonged to the wrapper, not to
66
66
  # this file, and reusing it would suggest a continuity that does not exist.
67
- SECSCAN_VERSION = "1.2.1"
67
+ SECSCAN_VERSION = "1.3.0"
68
68
 
69
69
  SEVERITIES = ["critical", "high", "medium", "low", "info"]
70
70
  SEVERITY_RANK = {s: i for i, s in enumerate(SEVERITIES)}
@@ -430,6 +430,50 @@ def git_ignored(root: Path, rels: list) -> set:
430
430
  return {l.strip() for l in p.stdout.splitlines() if l.strip()}
431
431
 
432
432
 
433
+ # Plan 11 G6: test code inside a normal file holds fake keys as often as a test folder
434
+ # does (a Rust #[cfg(test)] module, a #[test] fn, a Python test_ function). Its lines are
435
+ # suppressed and counted like a test path.
436
+ # ponytail: braces are counted without parsing strings or comments, so a brace inside a
437
+ # string can end a Rust block early; a real parser is the upgrade if that misses cases.
438
+ RUST_TEST_ATTR = re.compile(r"^\s*#\[(cfg\(test\)|test)\]")
439
+ PY_TEST_DEF = re.compile(r"^(\s*)(async\s+)?def test_\w*\s*\(")
440
+
441
+
442
+ def test_code_lines(rel: str, text: str) -> set:
443
+ lines = text.splitlines()
444
+ out = set()
445
+ if rel.endswith(".rs"):
446
+ i = 0
447
+ while i < len(lines):
448
+ if not RUST_TEST_ATTR.match(lines[i]):
449
+ i += 1
450
+ continue
451
+ depth, opened, j = 0, False, i
452
+ while j < len(lines):
453
+ depth += lines[j].count("{") - lines[j].count("}")
454
+ opened = opened or "{" in lines[j]
455
+ out.add(j + 1)
456
+ if opened and depth <= 0:
457
+ break
458
+ j += 1
459
+ i = j + 1
460
+ elif rel.endswith(".py"):
461
+ i = 0
462
+ while i < len(lines):
463
+ m = PY_TEST_DEF.match(lines[i])
464
+ if not m:
465
+ i += 1
466
+ continue
467
+ indent = len(m.group(1))
468
+ out.add(i + 1)
469
+ j = i + 1
470
+ while j < len(lines) and (not lines[j].strip() or len(lines[j]) - len(lines[j].lstrip()) > indent):
471
+ out.add(j + 1)
472
+ j += 1
473
+ i = j
474
+ return out
475
+
476
+
433
477
  def scan_secrets(root: Path) -> Section:
434
478
  sec = Section("Committed secrets")
435
479
  sec.ran.append(f"{len(SECRET_RULES)} high-confidence patterns")
@@ -441,13 +485,16 @@ def scan_secrets(root: Path) -> Section:
441
485
  text = path.read_text(errors="ignore")
442
486
  except OSError:
443
487
  continue
488
+ in_test = None
444
489
  for _id, severity, rx, desc in SECRET_RULES:
445
490
  for m in rx.finditer(text):
446
491
  line_no = text.count("\n", 0, m.start()) + 1
447
492
  line = text.splitlines()[line_no - 1] if line_no <= len(text.splitlines()) else ""
448
493
  if EXAMPLE_MARKERS.search(line):
449
494
  continue
450
- if is_fixture:
495
+ if in_test is None:
496
+ in_test = test_code_lines(rel, text)
497
+ if is_fixture or line_no in in_test:
451
498
  suppressed += 1
452
499
  continue
453
500
  hit = m.group(0)
@@ -477,7 +524,7 @@ def scan_secrets(root: Path) -> Section:
477
524
 
478
525
  if suppressed:
479
526
  sec.skipped.append(
480
- f"{suppressed} match(es) in test/fixture/doc paths were suppressed "
527
+ f"{suppressed} match(es) in test code, test/fixture/doc paths were suppressed "
481
528
  "as expected fake credentials — re-check by hand if a real secret "
482
529
  "could have been committed to a test file")
483
530
  return sec