ctxman 3.0.0 → 3.1.1
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 +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML style: Anthropic's long-context document format
|
|
3
|
+
*
|
|
4
|
+
* <documents>
|
|
5
|
+
* <document index="1">
|
|
6
|
+
* <source>path</source>
|
|
7
|
+
* <document_content>
|
|
8
|
+
* ...raw file content...
|
|
9
|
+
* </document_content>
|
|
10
|
+
* </document>
|
|
11
|
+
* </documents>
|
|
12
|
+
*
|
|
13
|
+
* File content is not XML-escaped (Claude reads raw code, and escaping every `<` and `&`
|
|
14
|
+
* costs tokens). Only a closing tag of the wrapper itself (`</document_content>`,
|
|
15
|
+
* `</document>`, `</documents>`) inside a file is neutralised by writing its `<` as `<`,
|
|
16
|
+
* so a file cannot end its own document early. Paths and the tree are escaped.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
joinSections,
|
|
21
|
+
omittedLines,
|
|
22
|
+
omittedSummary,
|
|
23
|
+
sourceLabel,
|
|
24
|
+
withoutFinalNewline,
|
|
25
|
+
} from './common.js';
|
|
26
|
+
|
|
27
|
+
const escapeXml = (text) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
28
|
+
|
|
29
|
+
// `</document_content>`, `</document>` and `</documents>`, in any case, spaces before `>`
|
|
30
|
+
const WRAPPER_CLOSING_TAG = /<(?=\/(?:document_content|documents?)\s*>)/gi;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Neutralise wrapper closing tags inside file content
|
|
34
|
+
* @param {string} content
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function protectContent(content) {
|
|
38
|
+
return content.replace(WRAPPER_CLOSING_TAG, '<');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One `<document>` element
|
|
43
|
+
* @param {object} file - Document model file
|
|
44
|
+
* @param {number} index - Document number
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function renderFile(file, index) {
|
|
48
|
+
return [
|
|
49
|
+
`<document index="${index}">`,
|
|
50
|
+
`<source>${escapeXml(sourceLabel(file))}</source>`,
|
|
51
|
+
'<document_content>',
|
|
52
|
+
protectContent(withoutFinalNewline(file.content)),
|
|
53
|
+
'</document_content>',
|
|
54
|
+
'</document>',
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {object} doc - Document model (see common.js)
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function render(doc) {
|
|
63
|
+
const documents = [
|
|
64
|
+
'<documents>',
|
|
65
|
+
...doc.files.map((file, i) => renderFile(file, doc.firstIndex + i)),
|
|
66
|
+
'</documents>',
|
|
67
|
+
].join('\n');
|
|
68
|
+
|
|
69
|
+
return joinSections([
|
|
70
|
+
doc.header,
|
|
71
|
+
doc.partLabel,
|
|
72
|
+
doc.tree !== null && `<directory_structure>\n${escapeXml(doc.tree)}\n</directory_structure>`,
|
|
73
|
+
documents,
|
|
74
|
+
doc.omitted &&
|
|
75
|
+
[
|
|
76
|
+
`<omitted_files count="${doc.omitted.count}" tokens="${doc.omitted.tokens}">`,
|
|
77
|
+
`${omittedSummary(doc.omitted)}:`,
|
|
78
|
+
...omittedLines(doc.omitted).map(escapeXml),
|
|
79
|
+
'</omitted_files>',
|
|
80
|
+
].join('\n'),
|
|
81
|
+
doc.instructions && `<instructions>\n${doc.instructions}\n</instructions>`,
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default { name: 'xml', extension: '.xml', render, renderFile };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File selection for `pack`: the project scan (with .gitignore, .contextignore and
|
|
3
|
+
* .contextinclude) or an explicit list, narrowed by --include/--exclude globs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { Scanner } from '../core/Scanner.js';
|
|
9
|
+
import GitIgnoreParser from '../parsers/gitignore-parser.js';
|
|
10
|
+
import FileUtils from '../utils/file-utils.js';
|
|
11
|
+
|
|
12
|
+
/** Error with a machine-readable code (PATH_OUTSIDE_ROOT, ROOT_NOT_FOUND, ...) */
|
|
13
|
+
export class PackError extends Error {
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'PackError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const toPosix = (relativePath) => relativePath.split(path.sep).join('/');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Path of `target` relative to `root` when it lies strictly below it, else null
|
|
25
|
+
* (`..config` is a file name, `../config` is outside)
|
|
26
|
+
* @param {string} root - Absolute directory
|
|
27
|
+
* @param {string} target - Absolute path
|
|
28
|
+
* @returns {string|null}
|
|
29
|
+
*/
|
|
30
|
+
function relativeInside(root, target) {
|
|
31
|
+
const relative = path.relative(root, target);
|
|
32
|
+
if (relative === '' || path.isAbsolute(relative)) return null;
|
|
33
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`)) return null;
|
|
34
|
+
return relative;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compile gitignore-style globs with the same rules as .contextinclude/.contextignore
|
|
39
|
+
* @param {string[]} globs
|
|
40
|
+
* @param {boolean} includeMode - true: a `!glob` excludes (as in .contextinclude)
|
|
41
|
+
* @returns {{test: function(string): boolean}|null} null when there are no globs
|
|
42
|
+
*/
|
|
43
|
+
function globMatcher(globs, includeMode) {
|
|
44
|
+
const patterns = globs.map((glob) => glob.trim()).filter(Boolean);
|
|
45
|
+
if (!patterns.length) return null;
|
|
46
|
+
// Only the pattern helpers are used: missing ignore files load nothing
|
|
47
|
+
const parser = new GitIgnoreParser('', '', '');
|
|
48
|
+
const compiled = patterns.map((pattern) => parser.convertToRegex(pattern));
|
|
49
|
+
return {
|
|
50
|
+
test: (relativePath) =>
|
|
51
|
+
includeMode
|
|
52
|
+
? parser.testPatternsWithNegation(compiled, relativePath)
|
|
53
|
+
: parser.testPatterns(compiled, relativePath, 'exclude'),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Pick the files to pack
|
|
59
|
+
* @param {string} root - Project root
|
|
60
|
+
* @param {object} options
|
|
61
|
+
* @param {string[]} [options.files] - Explicit list (replaces the scan); relative entries
|
|
62
|
+
* are resolved against `options.cwd`. Every entry must stay inside the root, and the
|
|
63
|
+
* project's ignore files still apply to it.
|
|
64
|
+
* @param {string} [options.cwd] - Base for relative `files` entries (default: root)
|
|
65
|
+
* @param {string[]} [options.include] - Keep only matching files
|
|
66
|
+
* @param {string[]} [options.exclude] - Leave matching files out
|
|
67
|
+
* @param {string[]} [options.excludeFiles] - Absolute paths to leave out (the output file)
|
|
68
|
+
* @param {number} [options.maxFileSize] - Larger files are skipped
|
|
69
|
+
* @returns {{root: string, files: Array<{absolutePath: string, path: string, size: number,
|
|
70
|
+
* modified: Date}>, skipped: Array<{path: string, reason: string}>}}
|
|
71
|
+
*/
|
|
72
|
+
export function selectFiles(root, options = {}) {
|
|
73
|
+
const rootPath = path.resolve(root);
|
|
74
|
+
if (!fs.statSync(rootPath, { throwIfNoEntry: false })?.isDirectory()) {
|
|
75
|
+
throw new PackError('ROOT_NOT_FOUND', `Not a directory: ${root}`);
|
|
76
|
+
}
|
|
77
|
+
const skipped = [];
|
|
78
|
+
const candidates = options.files
|
|
79
|
+
? listedFiles(rootPath, options.files, options.cwd ?? rootPath, skipped)
|
|
80
|
+
: scannedFiles(rootPath);
|
|
81
|
+
|
|
82
|
+
const include = globMatcher(options.include ?? [], true);
|
|
83
|
+
const exclude = globMatcher(options.exclude ?? [], false);
|
|
84
|
+
const excluded = new Set((options.excludeFiles ?? []).map((file) => path.resolve(file)));
|
|
85
|
+
const maxFileSize = options.maxFileSize ?? Infinity;
|
|
86
|
+
|
|
87
|
+
const files = [];
|
|
88
|
+
for (const file of candidates) {
|
|
89
|
+
if (include && !include.test(file.path)) continue;
|
|
90
|
+
if (exclude?.test(file.path)) continue;
|
|
91
|
+
if (excluded.has(file.absolutePath)) continue;
|
|
92
|
+
if (file.size > maxFileSize) {
|
|
93
|
+
skipped.push({ path: file.path, reason: 'too-large' });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
files.push(file);
|
|
97
|
+
}
|
|
98
|
+
return { root: rootPath, files, skipped };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Files found by the Scanner, in path order */
|
|
102
|
+
function scannedFiles(rootPath) {
|
|
103
|
+
return new Scanner(rootPath)
|
|
104
|
+
.scan()
|
|
105
|
+
.map((file) => ({
|
|
106
|
+
absolutePath: file.path,
|
|
107
|
+
path: toPosix(file.relativePath),
|
|
108
|
+
size: file.size,
|
|
109
|
+
modified: file.modified,
|
|
110
|
+
}))
|
|
111
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether the scan would leave a listed file out: a skipped directory (node_modules, .git,
|
|
116
|
+
* ...) on its path, or a .gitignore/.contextignore/.contextinclude rule on the file or one
|
|
117
|
+
* of its directories, checked the way the Scanner walks down to it
|
|
118
|
+
*/
|
|
119
|
+
function isIgnored(scanner, rootPath, relativePath) {
|
|
120
|
+
const segments = relativePath.split(path.sep);
|
|
121
|
+
for (let depth = 1; depth < segments.length; depth++) {
|
|
122
|
+
const directory = segments.slice(0, depth).join(path.sep);
|
|
123
|
+
if (FileUtils.isSkippedDirectory(segments[depth - 1])) return true;
|
|
124
|
+
if (scanner.shouldIgnore(directory, path.join(rootPath, directory))) return true;
|
|
125
|
+
}
|
|
126
|
+
return scanner.shouldIgnore(relativePath, path.join(rootPath, relativePath));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* An explicit list, in the given order. A path that leaves the root (`..`, an absolute path
|
|
131
|
+
* elsewhere, a symlink pointing out) is an error; files the ignore rules exclude are skipped.
|
|
132
|
+
*/
|
|
133
|
+
function listedFiles(rootPath, entries, cwd, skipped) {
|
|
134
|
+
const realRoot = fs.realpathSync(rootPath);
|
|
135
|
+
const scanner = new Scanner(rootPath);
|
|
136
|
+
const files = [];
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
if (typeof entry !== 'string' || entry.trim() === '') continue;
|
|
140
|
+
const absolute = path.resolve(cwd, entry);
|
|
141
|
+
// The root as given, or its real path (`find "$(pwd -P)"` under a symlinked root)
|
|
142
|
+
let relative = relativeInside(rootPath, absolute);
|
|
143
|
+
if (relative === null && realRoot !== rootPath) relative = relativeInside(realRoot, absolute);
|
|
144
|
+
if (relative === null) {
|
|
145
|
+
throw new PackError('PATH_OUTSIDE_ROOT', `Path is outside the project root: ${entry}`);
|
|
146
|
+
}
|
|
147
|
+
const absolutePath = path.join(rootPath, relative);
|
|
148
|
+
const relativePath = toPosix(relative);
|
|
149
|
+
if (seen.has(relativePath)) continue;
|
|
150
|
+
seen.add(relativePath);
|
|
151
|
+
|
|
152
|
+
let stats;
|
|
153
|
+
try {
|
|
154
|
+
// Symlinks are followed, but must not lead out of the root
|
|
155
|
+
if (relativeInside(realRoot, fs.realpathSync(absolutePath)) === null) {
|
|
156
|
+
throw new PackError('PATH_OUTSIDE_ROOT', `Path is outside the project root: ${entry}`);
|
|
157
|
+
}
|
|
158
|
+
stats = fs.statSync(absolutePath);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error instanceof PackError) throw error;
|
|
161
|
+
skipped.push({ path: relativePath, reason: 'missing' });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!stats.isFile()) {
|
|
165
|
+
skipped.push({ path: relativePath, reason: 'not-a-file' });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (isIgnored(scanner, rootPath, relative)) {
|
|
169
|
+
skipped.push({ path: relativePath, reason: 'ignored' });
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
files.push({ absolutePath, path: relativePath, size: stats.size, modified: stats.mtime });
|
|
173
|
+
}
|
|
174
|
+
return files;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default { selectFiles, PackError };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pack --split`: cut the packed files into numbered parts of at most `limit` tokens
|
|
3
|
+
*
|
|
4
|
+
* Every part is a complete document in the chosen style. Part 1 carries the header and the
|
|
5
|
+
* directory tree, the last part the omitted-files list and the instructions, and document
|
|
6
|
+
* numbers continue across parts. Files are never cut, except a file that does not fit in a
|
|
7
|
+
* part on its own: it is split by lines into pieces labelled "(lines a-b of n)".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { PackError } from './select.js';
|
|
11
|
+
|
|
12
|
+
// Label of a middle part when estimating: as long as any real label can be
|
|
13
|
+
const PLACEHOLDER_PART = { index: 9999, count: 9999 };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {Array} entries - Packed entries in output order
|
|
17
|
+
* @param {number} limit - Token limit per part
|
|
18
|
+
* @param {object} context
|
|
19
|
+
* @param {object} context.renderer - Style renderer
|
|
20
|
+
* @param {function(string): number} context.count - Token counter
|
|
21
|
+
* @param {object} context.doc - Whole-document fields: header, tree, omitted, instructions
|
|
22
|
+
* @returns {{parts: Array<{index: number, text: string, tokens: number, files: string[]}>,
|
|
23
|
+
* splitFiles: Array<{path: string, pieces: number}>}}
|
|
24
|
+
*/
|
|
25
|
+
export function splitIntoParts(entries, limit, { renderer, count, doc }) {
|
|
26
|
+
const hasFirstExtras = Boolean(doc.header) || doc.tree !== null;
|
|
27
|
+
const hasLastExtras = Boolean(doc.omitted) || Boolean(doc.instructions);
|
|
28
|
+
|
|
29
|
+
const partDoc = (units, index, total, firstIndex) => ({
|
|
30
|
+
header: index === 0 ? doc.header : null,
|
|
31
|
+
partLabel: `Part ${index + 1} of ${total}`,
|
|
32
|
+
part: { index: index + 1, count: total },
|
|
33
|
+
tree: index === 0 ? doc.tree : null,
|
|
34
|
+
files: units,
|
|
35
|
+
firstIndex,
|
|
36
|
+
omitted: index === total - 1 ? doc.omitted : null,
|
|
37
|
+
instructions: index === total - 1 ? doc.instructions : null,
|
|
38
|
+
});
|
|
39
|
+
const middleDoc = (units) => ({
|
|
40
|
+
...partDoc(units, 1, 3, 99999),
|
|
41
|
+
part: PLACEHOLDER_PART,
|
|
42
|
+
partLabel: 'Part 9999 of 9999',
|
|
43
|
+
});
|
|
44
|
+
const middleBase = count(renderer.render(middleDoc([])));
|
|
45
|
+
const unitCost = (unit) => count(renderer.renderFile(unit, 1)) + 1;
|
|
46
|
+
|
|
47
|
+
// Files too large for a part of their own are cut into pieces by lines
|
|
48
|
+
const units = [];
|
|
49
|
+
const splitFiles = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
// Estimates near the limit are checked exactly
|
|
52
|
+
const estimate = middleBase + unitCost(entry);
|
|
53
|
+
if (estimate <= limit * 0.9 || count(renderer.render(middleDoc([entry]))) <= limit) {
|
|
54
|
+
units.push(entry);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const pieces = splitByLines(entry, limit, { renderer, count, middleDoc });
|
|
58
|
+
splitFiles.push({ path: entry.path, pieces: pieces.length });
|
|
59
|
+
units.push(...pieces);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fill parts greedily with the estimates
|
|
63
|
+
let parts = [[]];
|
|
64
|
+
let used = count(renderer.render({ ...middleDoc([]), header: doc.header, tree: doc.tree }));
|
|
65
|
+
for (const unit of units) {
|
|
66
|
+
const cost = unitCost(unit);
|
|
67
|
+
if (parts[parts.length - 1].length && used + cost > limit) {
|
|
68
|
+
parts.push([]);
|
|
69
|
+
used = middleBase;
|
|
70
|
+
}
|
|
71
|
+
parts[parts.length - 1].push(unit);
|
|
72
|
+
used += cost;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Then count each part exactly and move files forward until every part fits. A move
|
|
76
|
+
// changes two parts (all of them when the number of parts changes): the others are
|
|
77
|
+
// counted once
|
|
78
|
+
const counted = new Map();
|
|
79
|
+
const countOnce = (text) => {
|
|
80
|
+
if (!counted.has(text)) counted.set(text, count(text));
|
|
81
|
+
return counted.get(text);
|
|
82
|
+
};
|
|
83
|
+
const maxIterations = (units.length + 5) * 4;
|
|
84
|
+
for (let iteration = 0; ; iteration++) {
|
|
85
|
+
if (iteration > maxIterations) {
|
|
86
|
+
throw new PackError('SPLIT_TOO_SMALL', `Could not split into parts of ${limit} tokens`);
|
|
87
|
+
}
|
|
88
|
+
const rendered = renderParts(parts, partDoc, renderer, countOnce);
|
|
89
|
+
const over = rendered.findIndex((part) => part.tokens > limit);
|
|
90
|
+
if (over === -1) {
|
|
91
|
+
return {
|
|
92
|
+
parts: rendered.map((part, i) => ({ ...part, files: parts[i].map(labelOf) })),
|
|
93
|
+
splitFiles,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isLast = over === parts.length - 1;
|
|
98
|
+
if (isLast && hasLastExtras && parts[over].length) {
|
|
99
|
+
parts.push([]); // the omitted list and instructions get a part of their own
|
|
100
|
+
} else if (parts[over].length > 1 || (over === 0 && hasFirstExtras && parts[over].length)) {
|
|
101
|
+
if (isLast) parts.push([]);
|
|
102
|
+
parts[over + 1].unshift(parts[over].pop());
|
|
103
|
+
} else {
|
|
104
|
+
throw new PackError(
|
|
105
|
+
'SPLIT_TOO_SMALL',
|
|
106
|
+
`Part ${over + 1} needs ${rendered[over].tokens} tokens; raise --split` +
|
|
107
|
+
(over === 0 && doc.tree !== null ? ' or use --no-tree' : '')
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
parts = parts.filter(
|
|
111
|
+
(units, i) =>
|
|
112
|
+
units.length ||
|
|
113
|
+
(i === 0 && hasFirstExtras) ||
|
|
114
|
+
(i === parts.length - 1 && hasLastExtras && parts.length > 1)
|
|
115
|
+
);
|
|
116
|
+
if (!parts.length) parts = [[]];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderParts(parts, partDoc, renderer, count) {
|
|
121
|
+
let firstIndex = 1;
|
|
122
|
+
return parts.map((units, i) => {
|
|
123
|
+
const text = renderer.render(partDoc(units, i, parts.length, firstIndex));
|
|
124
|
+
firstIndex += units.length;
|
|
125
|
+
return { index: i + 1, text, tokens: count(text) };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const labelOf = (unit) =>
|
|
130
|
+
unit.lines ? `${unit.path} (lines ${unit.lines[0]}-${unit.lines[1]})` : unit.path;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Cut one entry into pieces of whole lines, each fitting in a part on its own
|
|
134
|
+
* @returns {Array} Entries with `lines: [first, last]` and `totalLines`
|
|
135
|
+
*/
|
|
136
|
+
function splitByLines(entry, limit, { renderer, count, middleDoc }) {
|
|
137
|
+
const endsWithNewline = entry.content.endsWith('\n');
|
|
138
|
+
const lines = (endsWithNewline ? entry.content.slice(0, -1) : entry.content).split('\n');
|
|
139
|
+
const numberOf = (i) => (entry.lineNumbers ? entry.lineNumbers[i] : i + 1);
|
|
140
|
+
const lineTokens = lines.map((line) => count(`${line}\n`));
|
|
141
|
+
const fits = (piece) => count(renderer.render(middleDoc([piece]))) <= limit;
|
|
142
|
+
|
|
143
|
+
const piece = (from, to) => {
|
|
144
|
+
const content = lines.slice(from, to).join('\n') + (endsWithNewline ? '\n' : '');
|
|
145
|
+
return {
|
|
146
|
+
...entry,
|
|
147
|
+
content,
|
|
148
|
+
tokens: count(content),
|
|
149
|
+
lines: [numberOf(from), numberOf(to - 1)],
|
|
150
|
+
totalLines: entry.totalLines,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const empty = count(renderer.render(middleDoc([piece(0, 1)]))) - lineTokens[0];
|
|
155
|
+
let margin = Math.max(16, Math.ceil(limit * 0.02));
|
|
156
|
+
for (let attempt = 0; attempt < 6; attempt++, margin *= 2) {
|
|
157
|
+
const room = limit - empty - margin;
|
|
158
|
+
const pieces = [];
|
|
159
|
+
let start = 0;
|
|
160
|
+
let used = 0;
|
|
161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
162
|
+
if (lineTokens[i] > room) {
|
|
163
|
+
throw new PackError(
|
|
164
|
+
'SPLIT_TOO_SMALL',
|
|
165
|
+
`${entry.path}: line ${numberOf(i)} alone has ${lineTokens[i]} tokens, more than ` +
|
|
166
|
+
`a ${limit}-token part can hold`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (used + lineTokens[i] > room && i > start) {
|
|
170
|
+
pieces.push(piece(start, i));
|
|
171
|
+
start = i;
|
|
172
|
+
used = 0;
|
|
173
|
+
}
|
|
174
|
+
used += lineTokens[i];
|
|
175
|
+
}
|
|
176
|
+
pieces.push(piece(start, lines.length));
|
|
177
|
+
if (pieces.every(fits)) return pieces;
|
|
178
|
+
}
|
|
179
|
+
throw new PackError('SPLIT_TOO_SMALL', `Could not split ${entry.path} into ${limit}-token parts`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export default { splitIntoParts };
|
package/lib/pack/tree.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory tree for the top of a packed document
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Render project-relative paths as an indented tree, directories first
|
|
7
|
+
* @param {string[]} paths - Paths with `/` separators
|
|
8
|
+
* @returns {string} One entry per line, two spaces per level, directories end with `/`
|
|
9
|
+
*/
|
|
10
|
+
export function buildTree(paths) {
|
|
11
|
+
const root = { dirs: new Map(), files: [] };
|
|
12
|
+
for (const filePath of paths) {
|
|
13
|
+
const parts = filePath.split('/');
|
|
14
|
+
let node = root;
|
|
15
|
+
for (const dir of parts.slice(0, -1)) {
|
|
16
|
+
if (!node.dirs.has(dir)) node.dirs.set(dir, { dirs: new Map(), files: [] });
|
|
17
|
+
node = node.dirs.get(dir);
|
|
18
|
+
}
|
|
19
|
+
node.files.push(parts[parts.length - 1]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = [];
|
|
23
|
+
const walk = (node, depth) => {
|
|
24
|
+
const indent = ' '.repeat(depth);
|
|
25
|
+
for (const name of [...node.dirs.keys()].sort()) {
|
|
26
|
+
lines.push(`${indent}${name}/`);
|
|
27
|
+
walk(node.dirs.get(name), depth + 1);
|
|
28
|
+
}
|
|
29
|
+
for (const name of [...node.files].sort()) lines.push(`${indent}${name}`);
|
|
30
|
+
};
|
|
31
|
+
walk(root, 0);
|
|
32
|
+
return lines.join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default buildTree;
|