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 +12 -3
- package/package.json +1 -1
- package/scanners/ikanban-secscan.py +50 -3
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]
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
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
|