ctxman 3.1.1 → 3.2.7
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/.ctxman/llm-profiles.json +6 -6
- package/README.md +16 -10
- package/bin/cli.js +107 -8
- package/bin/cm-ask.js +64 -6
- package/bin/cm-audit.js +76 -0
- package/bin/cm-diagnose.js +199 -0
- package/bin/cm-focus.js +322 -0
- package/bin/cm-map.js +71 -18
- package/bin/cm-pack.js +36 -5
- package/bin/cm-policy.js +107 -2
- package/bin/cm-pr.js +18 -3
- package/bin/cm-search.js +84 -19
- package/bin/cm-task.js +192 -0
- package/bin/cm-who.js +119 -0
- package/bin/cm-why.js +41 -5
- package/index.d.ts +931 -1
- package/index.js +105 -1
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +618 -16
- package/lib/cache/CacheManager.js +161 -47
- package/lib/core/Analyzer.js +61 -5
- package/lib/diagnose/index.js +712 -0
- package/lib/focus/index.js +790 -0
- package/lib/map/call-graph.js +570 -0
- package/lib/map/graph.js +30 -5
- package/lib/map/index.js +12 -4
- package/lib/map/rank.js +45 -7
- package/lib/map/render.js +89 -28
- package/lib/pack/index.js +20 -1
- package/lib/pack/select.js +8 -1
- package/lib/policy/audit.js +3 -1
- package/lib/policy/index.js +5 -0
- package/lib/policy/workspace.js +254 -0
- package/lib/pr/pr-context.js +292 -31
- package/lib/pr/pr-renderer.js +25 -10
- package/lib/rag/EmbeddingProvider.js +139 -2
- package/lib/rag/VectorStoreFactory.js +10 -4
- package/lib/rag/ask.js +186 -17
- package/lib/rag/stores/SqliteVectorStore.js +595 -0
- package/lib/search/hybrid.js +324 -0
- package/lib/search/symbols.js +75 -11
- package/lib/task/index.js +630 -0
- package/lib/utils/file-utils.js +1 -0
- package/lib/utils/multi-repo-manager.js +4 -1
- package/lib/watch/FileWatcher.js +121 -31
- package/lib/watch/IncrementalAnalyzer.js +964 -16
- package/lib/who/index.js +893 -0
- package/lib/why/analyzer-bridge.js +223 -0
- package/lib/why/discussions.js +314 -0
- package/lib/why/index.js +356 -30
- package/lib/why/render.js +154 -9
- package/lib/workspace/index.js +1274 -0
- package/package.json +1 -1
package/bin/cm-task.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ctxman task - task-driven auto context pack
|
|
4
|
+
*
|
|
5
|
+
* stdout carries the context document (unless -o/--output or --copy); summary goes to stderr.
|
|
6
|
+
* Exit codes: 0 done, 1 error, 2 invalid arguments / options.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { getLogger } from '../lib/utils/logger.js';
|
|
12
|
+
|
|
13
|
+
function printHelp() {
|
|
14
|
+
process.stdout.write(`ctxman task - build task-driven context bundle around a goal
|
|
15
|
+
|
|
16
|
+
Usage: ctxman task "<goal>" [root] [options]
|
|
17
|
+
|
|
18
|
+
Discovers seed files and symbols via hybrid search, traverses 1-hop full code
|
|
19
|
+
and 2-hop compressed signatures around them (across module imports and the
|
|
20
|
+
cross-file AST symbol call graph), prepends an architectural repo map,
|
|
21
|
+
and packs everything strictly within the token budget.
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--repo DIR Additional repository root to include in workspace (repeatable)
|
|
25
|
+
--budget TOKENS Token budget (e.g. 32000, 32k, 40k; default: 32000)
|
|
26
|
+
--model MODEL Target model profile (e.g. claude-sonnet-4-5)
|
|
27
|
+
--format FORMAT Output format: markdown (default), json, xml
|
|
28
|
+
--json Shorthand for --format json
|
|
29
|
+
--search-mode MODE Search mode: hybrid (default), bm25, semantic
|
|
30
|
+
--calls Include cross-file AST symbol call graph edges (default)
|
|
31
|
+
--no-calls Disable AST symbol call graph edges (module imports only)
|
|
32
|
+
--no-redact Do not mask secrets
|
|
33
|
+
-o, --output FILE Write output to FILE instead of stdout
|
|
34
|
+
--copy Copy document to clipboard
|
|
35
|
+
-q, --quiet Do not print summary to stderr
|
|
36
|
+
-h, --help Show this help
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
ctxman task "Add SQLite vector store"
|
|
40
|
+
ctxman task "Fix OAuth token refresh" --budget 40k --format json
|
|
41
|
+
ctxman task "Add invoice checkout webhook" --repo ../billing-api --repo ../checkout-web
|
|
42
|
+
ctxman task "Refactor tokenizer" -o task-context.md
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
|
|
49
|
+
printHelp();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getLogger({ level: process.env.LOG_LEVEL || 'error', logToFile: false });
|
|
54
|
+
console.log = console.info = console.debug = console.error;
|
|
55
|
+
|
|
56
|
+
const { buildTaskContext, parseBudget } = await import('../lib/task/index.js');
|
|
57
|
+
|
|
58
|
+
let goal = null;
|
|
59
|
+
let root = process.cwd();
|
|
60
|
+
const repos = [];
|
|
61
|
+
let budget = 32000;
|
|
62
|
+
let model = null;
|
|
63
|
+
let format = 'markdown';
|
|
64
|
+
let searchMode = 'hybrid';
|
|
65
|
+
let callGraph = true;
|
|
66
|
+
let redact = true;
|
|
67
|
+
let outputFile = null;
|
|
68
|
+
let copy = false;
|
|
69
|
+
let quiet = false;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
for (let i = 0; i < argv.length; i++) {
|
|
73
|
+
const arg = argv[i];
|
|
74
|
+
|
|
75
|
+
if (arg === '--help' || arg === '-h') {
|
|
76
|
+
printHelp();
|
|
77
|
+
return;
|
|
78
|
+
} else if (arg === '--root' || arg === '-r') {
|
|
79
|
+
if (i + 1 >= argv.length) throw new Error('--root needs a directory path');
|
|
80
|
+
root = path.resolve(argv[++i]);
|
|
81
|
+
} else if (arg.startsWith('--root=')) {
|
|
82
|
+
root = path.resolve(arg.slice('--root='.length));
|
|
83
|
+
} else if (arg === '--repo') {
|
|
84
|
+
if (i + 1 >= argv.length) throw new Error('--repo needs a directory path');
|
|
85
|
+
repos.push(path.resolve(argv[++i]));
|
|
86
|
+
} else if (arg.startsWith('--repo=')) {
|
|
87
|
+
const val = arg.slice('--repo='.length);
|
|
88
|
+
if (!val) throw new Error('--repo needs a directory path');
|
|
89
|
+
repos.push(path.resolve(val));
|
|
90
|
+
} else if (arg === '--budget') {
|
|
91
|
+
budget = parseBudget(argv[++i]);
|
|
92
|
+
} else if (arg.startsWith('--budget=')) {
|
|
93
|
+
budget = parseBudget(arg.slice('--budget='.length));
|
|
94
|
+
} else if (arg === '--model' || arg === '--target-model') {
|
|
95
|
+
model = argv[++i];
|
|
96
|
+
} else if (arg.startsWith('--model=')) {
|
|
97
|
+
model = arg.slice('--model='.length);
|
|
98
|
+
} else if (arg === '--format') {
|
|
99
|
+
format = argv[++i];
|
|
100
|
+
} else if (arg.startsWith('--format=')) {
|
|
101
|
+
format = arg.slice('--format='.length);
|
|
102
|
+
} else if (arg === '--json') {
|
|
103
|
+
format = 'json';
|
|
104
|
+
} else if (arg === '--search-mode') {
|
|
105
|
+
searchMode = argv[++i];
|
|
106
|
+
} else if (arg.startsWith('--search-mode=')) {
|
|
107
|
+
searchMode = arg.slice('--search-mode='.length);
|
|
108
|
+
} else if (arg === '--calls' || arg === '--call-graph') {
|
|
109
|
+
callGraph = true;
|
|
110
|
+
} else if (arg === '--no-calls' || arg === '--no-call-graph') {
|
|
111
|
+
callGraph = false;
|
|
112
|
+
} else if (arg === '--no-redact') {
|
|
113
|
+
redact = false;
|
|
114
|
+
} else if (arg === '-o' || arg === '--output') {
|
|
115
|
+
outputFile = argv[++i];
|
|
116
|
+
} else if (arg.startsWith('-o=')) {
|
|
117
|
+
outputFile = arg.slice('-o='.length);
|
|
118
|
+
} else if (arg.startsWith('--output=')) {
|
|
119
|
+
outputFile = arg.slice('--output='.length);
|
|
120
|
+
} else if (arg === '--copy') {
|
|
121
|
+
copy = true;
|
|
122
|
+
} else if (arg === '-q' || arg === '--quiet') {
|
|
123
|
+
quiet = true;
|
|
124
|
+
} else if (!arg.startsWith('-')) {
|
|
125
|
+
if (goal === null) {
|
|
126
|
+
goal = arg;
|
|
127
|
+
} else if (fs.existsSync(arg) && fs.statSync(arg).isDirectory()) {
|
|
128
|
+
root = arg;
|
|
129
|
+
} else {
|
|
130
|
+
goal += ` ${arg}`;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
process.stderr.write(`Unknown option: ${arg}\n`);
|
|
134
|
+
process.exitCode = 2;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
140
|
+
process.exitCode = 2;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!goal) {
|
|
145
|
+
process.stderr.write('Error: A task goal is required.\n');
|
|
146
|
+
process.exitCode = 2;
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = await buildTaskContext(root, goal, {
|
|
152
|
+
repos,
|
|
153
|
+
budget,
|
|
154
|
+
model,
|
|
155
|
+
format,
|
|
156
|
+
searchMode,
|
|
157
|
+
callGraph,
|
|
158
|
+
redact,
|
|
159
|
+
output: outputFile,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (copy) {
|
|
163
|
+
const { default: ClipboardUtils } = await import('../lib/utils/clipboard-utils.js');
|
|
164
|
+
await ClipboardUtils.copy(result.text);
|
|
165
|
+
if (!quiet) {
|
|
166
|
+
process.stderr.write(`Copied ${result.totalTokens} tokens to clipboard.\n`);
|
|
167
|
+
}
|
|
168
|
+
} else if (!outputFile) {
|
|
169
|
+
process.stdout.write(result.text + (result.text.endsWith('\n') ? '' : '\n'));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!quiet) {
|
|
173
|
+
const repoLabel = result.repos?.length > 1 ? ` across ${result.repos.length} repos` : '';
|
|
174
|
+
process.stderr.write(
|
|
175
|
+
`Task Context: ${result.files.length} file(s)${repoLabel}, ${result.totalTokens}/${result.budget} tokens (${result.format}).\n`
|
|
176
|
+
);
|
|
177
|
+
if (outputFile) {
|
|
178
|
+
process.stderr.write(`Saved to: ${outputFile}\n`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
183
|
+
process.exitCode = error.exitCode ?? 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
process.stdout.on('error', (error) => {
|
|
188
|
+
if (error.code !== 'EPIPE') throw error;
|
|
189
|
+
process.exit(process.exitCode ?? 0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
main();
|
package/bin/cm-who.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ctxman who - "Who knows about this?" Expert Finder across git blame, recency-decayed commit history,
|
|
4
|
+
* CODEOWNERS stewardship, and natural-language topic search.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { getLogger } from '../lib/utils/logger.js';
|
|
11
|
+
|
|
12
|
+
getLogger({ level: 'error', logToFile: false });
|
|
13
|
+
const writeOut = (text) => process.stdout.write(text);
|
|
14
|
+
console.log = console.info = console.debug = console.error;
|
|
15
|
+
|
|
16
|
+
const { findExperts, WhoError, DEFAULT_WHO_LIMIT, DEFAULT_COMMIT_WINDOW, DEFAULT_HALF_LIFE_DAYS } =
|
|
17
|
+
await import('../lib/who/index.js');
|
|
18
|
+
|
|
19
|
+
const HELP = `Usage: ctxman who <target-or-topic> [options]
|
|
20
|
+
|
|
21
|
+
Find the code and domain experts for a file, symbol, line range, directory, or
|
|
22
|
+
natural-language topic by fusing surviving git blame ownership, recency-decayed
|
|
23
|
+
commit/churn depth, and CODEOWNERS stewardship (single-repo or multi-repo workspace).
|
|
24
|
+
|
|
25
|
+
Targets:
|
|
26
|
+
<file> A single file (e.g. lib/why/index.js or <alias>/src/index.js)
|
|
27
|
+
<file>:<symbol> A function, method, or class (e.g. lib/why/index.js:explainHistory)
|
|
28
|
+
<file>#L10-L40 A line range
|
|
29
|
+
<directory> A directory / subsystem (e.g. lib/rag or <alias>/src)
|
|
30
|
+
"<topic query>" A natural-language concept resolved via BM25 code search
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--root <dir> Primary repository root (default: cwd)
|
|
34
|
+
--repo <dir> Additional repository root for multi-repo workspace (repeatable)
|
|
35
|
+
-n, --limit <n> Maximum experts to return (default: ${DEFAULT_WHO_LIMIT})
|
|
36
|
+
--commits <n> Commit window to examine (default: ${DEFAULT_COMMIT_WINDOW})
|
|
37
|
+
--half-life <days> Recency decay half-life in days (default: ${DEFAULT_HALF_LIFE_DAYS})
|
|
38
|
+
--since <date> Only examine commits after this date
|
|
39
|
+
--style <style> markdown (default), xml, or json
|
|
40
|
+
--json Same as --style json
|
|
41
|
+
-o, --output <file> Write output to a file instead of stdout
|
|
42
|
+
-h, --help Show this help
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const OPTIONS = {
|
|
46
|
+
root: { type: 'string' },
|
|
47
|
+
repo: { type: 'string', multiple: true },
|
|
48
|
+
limit: { type: 'string', short: 'n' },
|
|
49
|
+
commits: { type: 'string' },
|
|
50
|
+
'half-life': { type: 'string' },
|
|
51
|
+
since: { type: 'string' },
|
|
52
|
+
style: { type: 'string' },
|
|
53
|
+
json: { type: 'boolean' },
|
|
54
|
+
output: { type: 'string', short: 'o' },
|
|
55
|
+
help: { type: 'boolean', short: 'h' },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function fail(message, exitCode = 1) {
|
|
59
|
+
process.stderr.write(`ctxman who: ${message}\n`);
|
|
60
|
+
process.exitCode = exitCode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function run() {
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parseArgs({ options: OPTIONS, allowPositionals: true });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
fail(err.message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { values, positionals } = parsed;
|
|
73
|
+
if (values.help) {
|
|
74
|
+
writeOut(HELP);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const target = positionals.join(' ').trim();
|
|
79
|
+
if (!target) {
|
|
80
|
+
fail('missing <target-or-topic>. Run `ctxman who --help` for usage.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const style = values.json ? 'json' : values.style || 'markdown';
|
|
85
|
+
if (!['markdown', 'xml', 'json'].includes(style)) {
|
|
86
|
+
fail(`unsupported --style "${style}" (expected markdown, xml, or json)`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rootDir = values.root ? path.resolve(process.cwd(), values.root) : process.cwd();
|
|
91
|
+
const extraRepos = Array.isArray(values.repo)
|
|
92
|
+
? values.repo.map((r) => path.resolve(process.cwd(), r))
|
|
93
|
+
: undefined;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const result = await findExperts(rootDir, target, {
|
|
97
|
+
repos: extraRepos,
|
|
98
|
+
limit: values.limit ? Number(values.limit) : undefined,
|
|
99
|
+
commitWindow: values.commits ? Number(values.commits) : undefined,
|
|
100
|
+
halfLifeDays: values['half-life'] ? Number(values['half-life']) : undefined,
|
|
101
|
+
since: values.since,
|
|
102
|
+
style,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (values.output) {
|
|
106
|
+
fs.writeFileSync(values.output, result.text, 'utf8');
|
|
107
|
+
} else {
|
|
108
|
+
writeOut(result.text);
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof WhoError) {
|
|
112
|
+
fail(err.message);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await run();
|
package/bin/cm-why.js
CHANGED
|
@@ -30,22 +30,30 @@ Explain why code looks the way it does: the commits that shaped it (with their
|
|
|
30
30
|
messages), the issues, tickets and URLs they reference, blame ownership, files
|
|
31
31
|
that change together with it, docs / ADRs / changelog entries / comments that
|
|
32
32
|
mention it, TODO-style notes inside it and the tests that reference it. Built
|
|
33
|
-
from the local git history;
|
|
33
|
+
from the local git history (offline by default; opt in to --discussions for
|
|
34
|
+
GitHub/GitLab issue, PR and MR context).
|
|
34
35
|
|
|
35
|
-
Targets (paths are relative to the current directory,
|
|
36
|
+
Targets (paths are relative to the current directory, or <alias>/<file> with --repo):
|
|
36
37
|
<file> The whole file (history follows renames)
|
|
37
38
|
<file>:<symbol> A function, method or class: total, Cart.total, Cart::total
|
|
38
39
|
<file>#L10-L40 A line range (also <file>#L10 and <file>:10-40)
|
|
39
40
|
|
|
40
41
|
Options:
|
|
42
|
+
--repo <dir> Additional repository root for multi-repo workspaces (repeatable)
|
|
41
43
|
-n, --limit <n> Commits to include (default: 20)
|
|
42
44
|
--since <date> Only commits after this date ("2024-01-01", "6 months ago")
|
|
43
45
|
--rev <ref> Explain the code as of this revision (default: working tree)
|
|
44
46
|
--patches Include each commit's diff of the target
|
|
47
|
+
--analyzer-db <path> Query @plansmithai/analyzer SQLite DB for institutional citations
|
|
48
|
+
--analyzer Enable analyzer citations from .plansmith/analyzer.sqlite
|
|
49
|
+
--discussions Fetch referenced GitHub/GitLab issue, PR and MR discussions
|
|
50
|
+
(uses GITHUB_TOKEN/GH_TOKEN or GITLAB_TOKEN when set)
|
|
51
|
+
--max-discussions <n> Maximum remote discussions to fetch (default: 10)
|
|
52
|
+
--max-comments <n> Maximum comments fetched per discussion (default: 5)
|
|
45
53
|
--coupling-window <n> Commits of the file examined for co-changes (default: 500, 0 = off)
|
|
46
54
|
--budget <tokens> Keep the output within this many tokens (8000 or 8k); drops
|
|
47
|
-
patches, related text, tests, coupling details
|
|
48
|
-
commits first
|
|
55
|
+
patches, discussions, related text, tests, coupling details
|
|
56
|
+
and older commits first
|
|
49
57
|
--style <style> markdown (default), xml or json
|
|
50
58
|
--json Same as --style json
|
|
51
59
|
--ask <question> Append a question at the end of the document
|
|
@@ -56,16 +64,24 @@ Options:
|
|
|
56
64
|
|
|
57
65
|
Examples:
|
|
58
66
|
ctxman why src/cart.js:total
|
|
67
|
+
ctxman why backend/src/cart.js:total --repo ../docs-repo --repo ../e2e-tests
|
|
68
|
+
ctxman why src/cart.js:total --discussions
|
|
59
69
|
ctxman why src/cart.js#L40-L72 --patches --budget 8000
|
|
60
70
|
ctxman why lib/server.js --since "1 year ago" --style xml -o why.xml
|
|
61
71
|
ctxman why src/cart.js:total --ask "Why do we round here instead of in applyDiscount?" --copy
|
|
62
72
|
`;
|
|
63
73
|
|
|
64
74
|
const OPTIONS = {
|
|
75
|
+
repo: { type: 'string', multiple: true },
|
|
65
76
|
limit: { type: 'string', short: 'n' },
|
|
66
77
|
since: { type: 'string' },
|
|
67
78
|
rev: { type: 'string' },
|
|
68
79
|
patches: { type: 'boolean' },
|
|
80
|
+
'analyzer-db': { type: 'string' },
|
|
81
|
+
analyzer: { type: 'boolean' },
|
|
82
|
+
discussions: { type: 'boolean' },
|
|
83
|
+
'max-discussions': { type: 'string' },
|
|
84
|
+
'max-comments': { type: 'string' },
|
|
69
85
|
'coupling-window': { type: 'string' },
|
|
70
86
|
budget: { type: 'string' },
|
|
71
87
|
style: { type: 'string' },
|
|
@@ -108,6 +124,10 @@ function summarize(result) {
|
|
|
108
124
|
const parts = [
|
|
109
125
|
plural(data.history.commits.length, 'commit'),
|
|
110
126
|
plural(references, 'reference'),
|
|
127
|
+
...(data.institutionalCitations?.length
|
|
128
|
+
? [plural(data.institutionalCitations.length, 'institutional citation')]
|
|
129
|
+
: []),
|
|
130
|
+
...(data.discussions?.length ? [plural(data.discussions.length, 'discussion')] : []),
|
|
111
131
|
plural(data.ownership?.authors.length ?? 0, 'author'),
|
|
112
132
|
plural(data.coupling?.files.length ?? 0, 'co-changed file'),
|
|
113
133
|
plural(data.related.length, 'related snippet'),
|
|
@@ -148,15 +168,31 @@ async function main() {
|
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
const cwd = process.cwd();
|
|
171
|
+
const extraRepos = values.repo?.length ? values.repo.map((r) => path.resolve(cwd, r)) : undefined;
|
|
151
172
|
let result;
|
|
152
173
|
try {
|
|
153
|
-
|
|
174
|
+
let root = cwd;
|
|
175
|
+
if (extraRepos) {
|
|
176
|
+
try {
|
|
177
|
+
root = findRepositoryRoot(cwd);
|
|
178
|
+
} catch {
|
|
179
|
+
root = cwd;
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
root = findRepositoryRoot(cwd);
|
|
183
|
+
}
|
|
154
184
|
result = await explainHistory(root, positionals[0], {
|
|
155
185
|
cwd,
|
|
186
|
+
repos: extraRepos,
|
|
156
187
|
limit: values.limit,
|
|
157
188
|
since: values.since,
|
|
158
189
|
rev: values.rev,
|
|
159
190
|
patches: values.patches,
|
|
191
|
+
discussions: values.discussions,
|
|
192
|
+
analyzerDb: values['analyzer-db'],
|
|
193
|
+
analyzer: values.analyzer,
|
|
194
|
+
maxDiscussions: values['max-discussions'],
|
|
195
|
+
maxComments: values['max-comments'],
|
|
160
196
|
couplingWindow: values['coupling-window'],
|
|
161
197
|
budget: parseTokenCount(values.budget),
|
|
162
198
|
style: values.json ? 'json' : values.style,
|