@fro.bot/systematic 2.0.2 → 2.1.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.
- package/agents/design/figma-design-sync.md +1 -1
- package/agents/document-review/coherence-reviewer.md +40 -0
- package/agents/document-review/design-lens-reviewer.md +46 -0
- package/agents/document-review/feasibility-reviewer.md +42 -0
- package/agents/document-review/product-lens-reviewer.md +50 -0
- package/agents/document-review/scope-guardian-reviewer.md +54 -0
- package/agents/document-review/security-lens-reviewer.md +38 -0
- package/agents/research/best-practices-researcher.md +2 -1
- package/agents/research/git-history-analyzer.md +1 -1
- package/agents/research/learnings-researcher.md +27 -26
- package/agents/research/repo-research-analyst.md +164 -9
- package/agents/review/api-contract-reviewer.md +49 -0
- package/agents/review/correctness-reviewer.md +49 -0
- package/agents/review/data-migrations-reviewer.md +53 -0
- package/agents/review/dhh-rails-reviewer.md +31 -52
- package/agents/review/julik-frontend-races-reviewer.md +27 -200
- package/agents/review/kieran-python-reviewer.md +29 -116
- package/agents/review/kieran-rails-reviewer.md +29 -98
- package/agents/review/kieran-typescript-reviewer.md +29 -107
- package/agents/review/maintainability-reviewer.md +49 -0
- package/agents/review/pattern-recognition-specialist.md +2 -1
- package/agents/review/performance-reviewer.md +51 -0
- package/agents/review/reliability-reviewer.md +49 -0
- package/agents/review/schema-drift-detector.md +12 -10
- package/agents/review/security-reviewer.md +51 -0
- package/agents/review/testing-reviewer.md +48 -0
- package/agents/workflow/pr-comment-resolver.md +99 -50
- package/agents/workflow/spec-flow-analyzer.md +60 -89
- package/dist/index.js +9 -0
- package/dist/lib/config-handler.d.ts +2 -0
- package/package.json +1 -1
- package/skills/agent-browser/SKILL.md +69 -48
- package/skills/ce-brainstorm/SKILL.md +2 -1
- package/skills/ce-compound/SKILL.md +126 -28
- package/skills/ce-compound-refresh/SKILL.md +181 -73
- package/skills/ce-ideate/SKILL.md +2 -1
- package/skills/ce-plan/SKILL.md +424 -414
- package/skills/ce-review/SKILL.md +379 -419
- package/skills/ce-review-beta/SKILL.md +506 -0
- package/skills/ce-review-beta/references/diff-scope.md +31 -0
- package/skills/ce-review-beta/references/findings-schema.json +128 -0
- package/skills/ce-review-beta/references/persona-catalog.md +50 -0
- package/skills/ce-review-beta/references/review-output-template.md +115 -0
- package/skills/ce-review-beta/references/subagent-template.md +56 -0
- package/skills/ce-work/SKILL.md +17 -8
- package/skills/ce-work-beta/SKILL.md +16 -9
- package/skills/claude-permissions-optimizer/SKILL.md +15 -14
- package/skills/claude-permissions-optimizer/scripts/extract-commands.mjs +9 -159
- package/skills/claude-permissions-optimizer/scripts/normalize.mjs +151 -0
- package/skills/deepen-plan/SKILL.md +348 -483
- package/skills/document-review/SKILL.md +160 -52
- package/skills/feature-video/SKILL.md +209 -178
- package/skills/file-todos/SKILL.md +72 -94
- package/skills/frontend-design/SKILL.md +243 -27
- package/skills/git-worktree/SKILL.md +37 -28
- package/skills/git-worktree/scripts/worktree-manager.sh +163 -0
- package/skills/lfg/SKILL.md +7 -7
- package/skills/orchestrating-swarms/SKILL.md +1 -1
- package/skills/reproduce-bug/SKILL.md +154 -60
- package/skills/resolve-pr-parallel/SKILL.md +19 -12
- package/skills/resolve-todo-parallel/SKILL.md +9 -6
- package/skills/setup/SKILL.md +8 -160
- package/skills/slfg/SKILL.md +11 -7
- package/skills/test-browser/SKILL.md +69 -145
- package/skills/test-xcode/SKILL.md +61 -183
- package/skills/triage/SKILL.md +10 -10
- package/skills/ce-plan-beta/SKILL.md +0 -571
- package/skills/deepen-plan-beta/SKILL.md +0 -323
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Extracts, normalizes, and pre-classifies Bash commands from
|
|
3
|
+
// Extracts, normalizes, and pre-classifies Bash commands from Claude Code sessions.
|
|
4
4
|
// Filters against the current allowlist, groups by normalized pattern, and classifies
|
|
5
5
|
// each pattern as green/yellow/red so the model can review rather than classify from scratch.
|
|
6
6
|
//
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
16
16
|
import { homedir } from 'node:os'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
|
+
import { normalize } from './normalize.mjs'
|
|
18
19
|
|
|
19
20
|
const args = process.argv.slice(2)
|
|
20
21
|
|
|
@@ -42,9 +43,8 @@ const maxSessions = parseInt(flag('max-sessions', '500'), 10)
|
|
|
42
43
|
const minCount = parseInt(flag('min-count', '5'), 10)
|
|
43
44
|
const projectSlugFilter = flag('project-slug', null)
|
|
44
45
|
const settingsPaths = flagAll('settings')
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const projectsDir = join(opencodeDir, 'projects')
|
|
46
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude')
|
|
47
|
+
const projectsDir = join(claudeDir, 'projects')
|
|
48
48
|
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
|
|
49
49
|
|
|
50
50
|
// ── Allowlist loading ──────────────────────────────────────────────────────
|
|
@@ -70,9 +70,9 @@ async function loadAllowlist(filePath) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (settingsPaths.length === 0) {
|
|
73
|
-
settingsPaths.push(join(
|
|
74
|
-
settingsPaths.push(join(process.cwd(), '.
|
|
75
|
-
settingsPaths.push(join(process.cwd(), '.
|
|
73
|
+
settingsPaths.push(join(claudeDir, 'settings.json'))
|
|
74
|
+
settingsPaths.push(join(process.cwd(), '.claude', 'settings.json'))
|
|
75
|
+
settingsPaths.push(join(process.cwd(), '.claude', 'settings.local.json'))
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
for (const p of settingsPaths) {
|
|
@@ -320,7 +320,7 @@ const GREEN_COMPOUND = [
|
|
|
320
320
|
/\b--dry-run\b/,
|
|
321
321
|
/^git\s+clean\s+.*(-[a-z]*n|--dry-run)\b/, // git clean dry run
|
|
322
322
|
// NOTE: find is intentionally NOT green. Bash(find *) would also match
|
|
323
|
-
// find -delete and find -exec rm in
|
|
323
|
+
// find -delete and find -exec rm in Claude Code's allowlist glob matching.
|
|
324
324
|
// Commands with mode-switching flags: only green when the normalized pattern
|
|
325
325
|
// is narrow enough that the allowlist glob can't match the destructive form.
|
|
326
326
|
// Bash(sed -n *) is safe; Bash(sed *) would also match sed -i.
|
|
@@ -410,156 +410,7 @@ function classify(command) {
|
|
|
410
410
|
return { tier: 'unknown' }
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
// ── Normalization
|
|
414
|
-
|
|
415
|
-
// Risk-modifying flags that must NOT be collapsed into wildcards.
|
|
416
|
-
// Global flags are always preserved; context-specific flags only matter
|
|
417
|
-
// for certain base commands.
|
|
418
|
-
const GLOBAL_RISK_FLAGS = new Set([
|
|
419
|
-
'--force',
|
|
420
|
-
'--hard',
|
|
421
|
-
'-rf',
|
|
422
|
-
'--privileged',
|
|
423
|
-
'--no-verify',
|
|
424
|
-
'--system',
|
|
425
|
-
'--force-with-lease',
|
|
426
|
-
'-D',
|
|
427
|
-
'--force-if-includes',
|
|
428
|
-
'--volumes',
|
|
429
|
-
'--rmi',
|
|
430
|
-
'--rewrite',
|
|
431
|
-
'--delete',
|
|
432
|
-
])
|
|
433
|
-
|
|
434
|
-
// Flags that are only risky for specific base commands.
|
|
435
|
-
// -f means force-push in git, force-remove in docker, but pattern-file in grep.
|
|
436
|
-
// -v means remove-volumes in docker-compose, but verbose everywhere else.
|
|
437
|
-
const CONTEXTUAL_RISK_FLAGS = {
|
|
438
|
-
'-f': new Set(['git', 'docker', 'rm']),
|
|
439
|
-
'-v': new Set(['docker', 'docker-compose']),
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function isRiskFlag(token, base) {
|
|
443
|
-
if (GLOBAL_RISK_FLAGS.has(token)) return true
|
|
444
|
-
// Check context-specific flags
|
|
445
|
-
const contexts = CONTEXTUAL_RISK_FLAGS[token]
|
|
446
|
-
if (contexts && base && contexts.has(base)) return true
|
|
447
|
-
// Combined short flags containing risk chars: -rf, -fr, -fR, etc.
|
|
448
|
-
if (/^-[a-zA-Z]*[rf][a-zA-Z]*$/.test(token) && token.length <= 4) return true
|
|
449
|
-
return false
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: command normalization intentionally centralizes risk checks and pattern shaping.
|
|
453
|
-
function normalize(command) {
|
|
454
|
-
// Don't normalize shell injection patterns
|
|
455
|
-
if (/\|\s*(sh|bash|zsh)\b/.test(command)) return command
|
|
456
|
-
// Don't normalize sudo -- keep as-is
|
|
457
|
-
if (/^sudo\s/.test(command)) return 'sudo *'
|
|
458
|
-
|
|
459
|
-
// Handle pnpm --filter <pkg> <subcommand> specially
|
|
460
|
-
const pnpmFilter = command.match(/^pnpm\s+--filter\s+\S+\s+(\S+)/)
|
|
461
|
-
if (pnpmFilter) return `pnpm --filter * ${pnpmFilter[1]} *`
|
|
462
|
-
|
|
463
|
-
// Handle sed specially -- preserve the mode flag to keep safe patterns narrow.
|
|
464
|
-
// sed -i (in-place) is destructive; sed -n, sed -e, bare sed are read-only.
|
|
465
|
-
if (/^sed\s/.test(command)) {
|
|
466
|
-
if (/\s-i\b/.test(command)) return 'sed -i *'
|
|
467
|
-
const sedFlag = command.match(/^sed\s+(-[a-zA-Z])\s/)
|
|
468
|
-
return sedFlag ? `sed ${sedFlag[1]} *` : 'sed *'
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Handle ast-grep specially -- preserve --rewrite flag.
|
|
472
|
-
if (/^(ast-grep|sg)\s/.test(command)) {
|
|
473
|
-
const base = command.startsWith('sg') ? 'sg' : 'ast-grep'
|
|
474
|
-
return /\s--rewrite\b/.test(command) ? `${base} --rewrite *` : `${base} *`
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Handle find specially -- preserve key action flags.
|
|
478
|
-
// find -delete and find -exec rm are destructive; find -name/-type are safe.
|
|
479
|
-
if (/^find\s/.test(command)) {
|
|
480
|
-
if (/\s-delete\b/.test(command)) return 'find -delete *'
|
|
481
|
-
if (/\s-exec\s/.test(command)) return 'find -exec *'
|
|
482
|
-
// Extract the first predicate flag for a narrower safe pattern
|
|
483
|
-
const findFlag = command.match(/\s(-(?:name|type|path|iname))\s/)
|
|
484
|
-
return findFlag ? `find ${findFlag[1]} *` : 'find *'
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Handle git -C <dir> <subcommand> -- strip the -C <dir> and normalize the git subcommand
|
|
488
|
-
const gitC = command.match(/^git\s+-C\s+\S+\s+(.+)$/)
|
|
489
|
-
if (gitC) return normalize(`git ${gitC[1]}`)
|
|
490
|
-
|
|
491
|
-
// Split on compound operators -- normalize the first command only
|
|
492
|
-
const compoundMatch = command.match(/^(.+?)\s*(&&|\|\||;)\s*(.+)$/)
|
|
493
|
-
if (compoundMatch) {
|
|
494
|
-
return normalize(compoundMatch[1].trim())
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Strip trailing pipe chains for normalization (e.g., `cmd | tail -5`)
|
|
498
|
-
// but preserve pipe-to-shell (already handled by shell injection check above)
|
|
499
|
-
const pipeMatch = command.match(/^(.+?)\s*\|\s*(.+)$/)
|
|
500
|
-
if (pipeMatch) {
|
|
501
|
-
return normalize(pipeMatch[1].trim())
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Strip trailing redirections (2>&1, > file, >> file)
|
|
505
|
-
const cleaned = command
|
|
506
|
-
.replace(/\s*[12]?>>?\s*\S+\s*$/, '')
|
|
507
|
-
.replace(/\s*2>&1\s*$/, '')
|
|
508
|
-
.trim()
|
|
509
|
-
|
|
510
|
-
const parts = cleaned.split(/\s+/)
|
|
511
|
-
if (parts.length === 0) return command
|
|
512
|
-
|
|
513
|
-
const base = parts[0]
|
|
514
|
-
|
|
515
|
-
// For git/docker/gh/npm etc, include the subcommand
|
|
516
|
-
const multiWordBases = [
|
|
517
|
-
'git',
|
|
518
|
-
'docker',
|
|
519
|
-
'docker-compose',
|
|
520
|
-
'gh',
|
|
521
|
-
'npm',
|
|
522
|
-
'bun',
|
|
523
|
-
'pnpm',
|
|
524
|
-
'yarn',
|
|
525
|
-
'cargo',
|
|
526
|
-
'pip',
|
|
527
|
-
'pip3',
|
|
528
|
-
'bundle',
|
|
529
|
-
'systemctl',
|
|
530
|
-
'kubectl',
|
|
531
|
-
]
|
|
532
|
-
|
|
533
|
-
let prefix = base
|
|
534
|
-
let argStart = 1
|
|
535
|
-
|
|
536
|
-
if (multiWordBases.includes(base) && parts.length > 1) {
|
|
537
|
-
prefix = `${base} ${parts[1]}`
|
|
538
|
-
argStart = 2
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Preserve risk-modifying flags in the remaining args
|
|
542
|
-
const preservedFlags = []
|
|
543
|
-
for (let i = argStart; i < parts.length; i++) {
|
|
544
|
-
if (isRiskFlag(parts[i], base)) {
|
|
545
|
-
preservedFlags.push(parts[i])
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Build the normalized pattern
|
|
550
|
-
if (parts.length <= argStart && preservedFlags.length === 0) {
|
|
551
|
-
return prefix // no args, no flags: e.g., "git status"
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const flagStr =
|
|
555
|
-
preservedFlags.length > 0 ? ` ${preservedFlags.join(' ')}` : ''
|
|
556
|
-
const hasVaryingArgs = parts.length > argStart + preservedFlags.length
|
|
557
|
-
|
|
558
|
-
if (hasVaryingArgs) {
|
|
559
|
-
return `${prefix + flagStr} *`
|
|
560
|
-
}
|
|
561
|
-
return prefix + flagStr
|
|
562
|
-
}
|
|
413
|
+
// ── Normalization (see ./normalize.mjs) ────────────────────────────────────
|
|
563
414
|
|
|
564
415
|
// ── Session file scanning ──────────────────────────────────────────────────
|
|
565
416
|
|
|
@@ -587,7 +438,6 @@ async function listJsonlFiles(dir) {
|
|
|
587
438
|
}
|
|
588
439
|
}
|
|
589
440
|
|
|
590
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: transcript parsing requires defensive guards for heterogeneous session data.
|
|
591
441
|
async function processFile(filePath, sessionId) {
|
|
592
442
|
try {
|
|
593
443
|
filesScanned++
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Normalization helpers extracted from extract-commands.mjs for testability.
|
|
2
|
+
|
|
3
|
+
// Risk-modifying flags that must NOT be collapsed into wildcards.
|
|
4
|
+
// Global flags are always preserved; context-specific flags only matter
|
|
5
|
+
// for certain base commands.
|
|
6
|
+
const GLOBAL_RISK_FLAGS = new Set([
|
|
7
|
+
'--force',
|
|
8
|
+
'--hard',
|
|
9
|
+
'-rf',
|
|
10
|
+
'--privileged',
|
|
11
|
+
'--no-verify',
|
|
12
|
+
'--system',
|
|
13
|
+
'--force-with-lease',
|
|
14
|
+
'-D',
|
|
15
|
+
'--force-if-includes',
|
|
16
|
+
'--volumes',
|
|
17
|
+
'--rmi',
|
|
18
|
+
'--rewrite',
|
|
19
|
+
'--delete',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
// Flags that are only risky for specific base commands.
|
|
23
|
+
// -f means force-push in git, force-remove in docker, but pattern-file in grep.
|
|
24
|
+
// -v means remove-volumes in docker-compose, but verbose everywhere else.
|
|
25
|
+
const CONTEXTUAL_RISK_FLAGS = {
|
|
26
|
+
'-f': new Set(['git', 'docker', 'rm']),
|
|
27
|
+
'-v': new Set(['docker', 'docker-compose']),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isRiskFlag(token, base) {
|
|
31
|
+
if (GLOBAL_RISK_FLAGS.has(token)) return true
|
|
32
|
+
// Check context-specific flags
|
|
33
|
+
const contexts = Object.hasOwn(CONTEXTUAL_RISK_FLAGS, token)
|
|
34
|
+
? CONTEXTUAL_RISK_FLAGS[token]
|
|
35
|
+
: undefined
|
|
36
|
+
if (contexts && base && contexts.has(base)) return true
|
|
37
|
+
// Combined short flags containing risk chars: -rf, -fr, -fR, etc.
|
|
38
|
+
if (/^-[a-zA-Z]*[rf][a-zA-Z]*$/.test(token) && token.length <= 4) return true
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function normalize(command) {
|
|
43
|
+
// Don't normalize shell injection patterns
|
|
44
|
+
if (/\|\s*(sh|bash|zsh)\b/.test(command)) return command
|
|
45
|
+
// Don't normalize sudo -- keep as-is
|
|
46
|
+
if (/^sudo\s/.test(command)) return 'sudo *'
|
|
47
|
+
|
|
48
|
+
// Handle pnpm --filter <pkg> <subcommand> specially
|
|
49
|
+
const pnpmFilter = command.match(/^pnpm\s+--filter\s+\S+\s+(\S+)/)
|
|
50
|
+
if (pnpmFilter) return `pnpm --filter * ${pnpmFilter[1]} *`
|
|
51
|
+
|
|
52
|
+
// Handle sed specially -- preserve the mode flag to keep safe patterns narrow.
|
|
53
|
+
// sed -i (in-place) is destructive; sed -n, sed -e, bare sed are read-only.
|
|
54
|
+
if (/^sed\s/.test(command)) {
|
|
55
|
+
if (/\s-i\b/.test(command)) return 'sed -i *'
|
|
56
|
+
const sedFlag = command.match(/^sed\s+(-[a-zA-Z])\s/)
|
|
57
|
+
return sedFlag ? `sed ${sedFlag[1]} *` : 'sed *'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle ast-grep specially -- preserve --rewrite flag.
|
|
61
|
+
if (/^(ast-grep|sg)\s/.test(command)) {
|
|
62
|
+
const base = command.startsWith('sg') ? 'sg' : 'ast-grep'
|
|
63
|
+
return /\s--rewrite\b/.test(command) ? `${base} --rewrite *` : `${base} *`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle find specially -- preserve key action flags.
|
|
67
|
+
// find -delete and find -exec rm are destructive; find -name/-type are safe.
|
|
68
|
+
if (/^find\s/.test(command)) {
|
|
69
|
+
if (/\s-delete\b/.test(command)) return 'find -delete *'
|
|
70
|
+
if (/\s-exec\s/.test(command)) return 'find -exec *'
|
|
71
|
+
// Extract the first predicate flag for a narrower safe pattern
|
|
72
|
+
const findFlag = command.match(/\s(-(?:name|type|path|iname))\s/)
|
|
73
|
+
return findFlag ? `find ${findFlag[1]} *` : 'find *'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle git -C <dir> <subcommand> -- strip the -C <dir> and normalize the git subcommand
|
|
77
|
+
const gitC = command.match(/^git\s+-C\s+\S+\s+(.+)$/)
|
|
78
|
+
if (gitC) return normalize(`git ${gitC[1]}`)
|
|
79
|
+
|
|
80
|
+
// Split on compound operators -- normalize the first command only
|
|
81
|
+
const compoundMatch = command.match(/^(.+?)\s*(&&|\|\||;)\s*(.+)$/)
|
|
82
|
+
if (compoundMatch) {
|
|
83
|
+
return normalize(compoundMatch[1].trim())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Strip trailing pipe chains for normalization (e.g., `cmd | tail -5`)
|
|
87
|
+
// but preserve pipe-to-shell (already handled by shell injection check above)
|
|
88
|
+
const pipeMatch = command.match(/^(.+?)\s*\|\s*(.+)$/)
|
|
89
|
+
if (pipeMatch) {
|
|
90
|
+
return normalize(pipeMatch[1].trim())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Strip trailing redirections (2>&1, > file, >> file)
|
|
94
|
+
const cleaned = command
|
|
95
|
+
.replace(/\s*[12]?>>?\s*\S+\s*$/, '')
|
|
96
|
+
.replace(/\s*2>&1\s*$/, '')
|
|
97
|
+
.trim()
|
|
98
|
+
|
|
99
|
+
const parts = cleaned.split(/\s+/)
|
|
100
|
+
if (parts.length === 0) return command
|
|
101
|
+
|
|
102
|
+
const base = parts[0]
|
|
103
|
+
|
|
104
|
+
// For git/docker/gh/npm etc, include the subcommand
|
|
105
|
+
const multiWordBases = [
|
|
106
|
+
'git',
|
|
107
|
+
'docker',
|
|
108
|
+
'docker-compose',
|
|
109
|
+
'gh',
|
|
110
|
+
'npm',
|
|
111
|
+
'bun',
|
|
112
|
+
'pnpm',
|
|
113
|
+
'yarn',
|
|
114
|
+
'cargo',
|
|
115
|
+
'pip',
|
|
116
|
+
'pip3',
|
|
117
|
+
'bundle',
|
|
118
|
+
'systemctl',
|
|
119
|
+
'kubectl',
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
let prefix = base
|
|
123
|
+
let argStart = 1
|
|
124
|
+
|
|
125
|
+
if (multiWordBases.includes(base) && parts.length > 1) {
|
|
126
|
+
prefix = `${base} ${parts[1]}`
|
|
127
|
+
argStart = 2
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Preserve risk-modifying flags in the remaining args
|
|
131
|
+
const preservedFlags = []
|
|
132
|
+
for (let i = argStart; i < parts.length; i++) {
|
|
133
|
+
if (isRiskFlag(parts[i], base)) {
|
|
134
|
+
preservedFlags.push(parts[i])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Build the normalized pattern
|
|
139
|
+
if (parts.length <= argStart && preservedFlags.length === 0) {
|
|
140
|
+
return prefix // no args, no flags: e.g., "git status"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const flagStr =
|
|
144
|
+
preservedFlags.length > 0 ? ` ${preservedFlags.join(' ')}` : ''
|
|
145
|
+
const hasVaryingArgs = parts.length > argStart + preservedFlags.length
|
|
146
|
+
|
|
147
|
+
if (hasVaryingArgs) {
|
|
148
|
+
return `${prefix + flagStr} *`
|
|
149
|
+
}
|
|
150
|
+
return prefix + flagStr
|
|
151
|
+
}
|