ctxman 3.0.0 → 3.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/.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 +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
- 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 +243 -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 +255 -0
- package/lib/wizards/project-detector.js +571 -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,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export audit log
|
|
3
|
+
*
|
|
4
|
+
* One JSON object per line, appended for every export (and every export the policy
|
|
5
|
+
* refused) while the policy enables `audit`. A record names the files and carries a
|
|
6
|
+
* SHA-256 of the exported text, never the text itself.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
|
+
import { createRequire } from 'module';
|
|
15
|
+
import { relativePathOf } from './policy.js';
|
|
16
|
+
|
|
17
|
+
const { version: CTXMAN_VERSION } = createRequire(import.meta.url)('../../package.json');
|
|
18
|
+
|
|
19
|
+
const defaultWarn = (message) => console.warn(message);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SHA-256 of exported text; several parts (digest chunks) hash as their concatenation
|
|
23
|
+
* @param {string|Buffer|Array<string|Buffer>} text
|
|
24
|
+
* @returns {string} Hex digest
|
|
25
|
+
*/
|
|
26
|
+
export function sha256(text) {
|
|
27
|
+
const hash = crypto.createHash('sha256');
|
|
28
|
+
for (const part of Array.isArray(text) ? text : [text]) hash.update(part);
|
|
29
|
+
return hash.digest('hex');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Commit checked out in `dir`, or null outside a git repository
|
|
34
|
+
* @param {string} dir
|
|
35
|
+
* @returns {string|null}
|
|
36
|
+
*/
|
|
37
|
+
export function getGitHead(dir) {
|
|
38
|
+
try {
|
|
39
|
+
if (!dir || !fs.statSync(dir).isDirectory()) return null;
|
|
40
|
+
const head = execFileSync('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], {
|
|
41
|
+
cwd: dir,
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
+
timeout: 5000,
|
|
45
|
+
}).trim();
|
|
46
|
+
return /^[0-9a-f]{40,64}$/.test(head) ? head : null;
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function currentUser() {
|
|
53
|
+
try {
|
|
54
|
+
return os.userInfo().username;
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
// No passwd entry (some containers)
|
|
57
|
+
return process.env.USER || process.env.USERNAME || 'unknown';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function countRedactions(redactions) {
|
|
62
|
+
if (typeof redactions === 'number') return redactions;
|
|
63
|
+
if (redactions && typeof redactions === 'object') {
|
|
64
|
+
return Object.values(redactions).reduce((sum, count) => sum + (Number(count) || 0), 0);
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build the audit record for an export event
|
|
71
|
+
* @param {import('./policy.js').Policy} policy
|
|
72
|
+
* @param {AuditEvent} event
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
export function buildAuditRecord(policy, event = {}) {
|
|
76
|
+
const files = (event.files ?? []).map(relativePathOf);
|
|
77
|
+
const tokens =
|
|
78
|
+
typeof event.tokens === 'number'
|
|
79
|
+
? event.tokens
|
|
80
|
+
: (event.files ?? []).reduce((sum, file) => sum + (Number(file?.tokens) || 0), 0);
|
|
81
|
+
const refused = event.outcome === 'refused';
|
|
82
|
+
const root = event.root ?? policy.root;
|
|
83
|
+
|
|
84
|
+
let digest = null;
|
|
85
|
+
if (!refused) {
|
|
86
|
+
if (event.sha256) digest = event.sha256;
|
|
87
|
+
else if (event.text !== undefined && event.text !== null) digest = sha256(event.text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const record = {
|
|
91
|
+
id: crypto.randomUUID(),
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
outcome: refused ? 'refused' : 'exported',
|
|
94
|
+
command: event.command ?? null,
|
|
95
|
+
root,
|
|
96
|
+
user: currentUser(),
|
|
97
|
+
host: os.hostname(),
|
|
98
|
+
gitHead: event.gitHead !== undefined ? event.gitHead : getGitHead(root),
|
|
99
|
+
targetModel: event.targetModel ?? null,
|
|
100
|
+
format: event.format ?? null,
|
|
101
|
+
destination: event.destination ?? null,
|
|
102
|
+
fileCount: files.length,
|
|
103
|
+
totalTokens: tokens,
|
|
104
|
+
redactions: countRedactions(event.redactions),
|
|
105
|
+
removedByPolicy: event.removedByPolicy ?? 0,
|
|
106
|
+
files,
|
|
107
|
+
sha256: digest,
|
|
108
|
+
ctxmanVersion: CTXMAN_VERSION,
|
|
109
|
+
};
|
|
110
|
+
if (Array.isArray(event.outputFiles) && event.outputFiles.length > 0) {
|
|
111
|
+
record.outputFiles = event.outputFiles;
|
|
112
|
+
}
|
|
113
|
+
if (refused) {
|
|
114
|
+
record.violations = (event.violations ?? []).map(({ rule, message }) => ({ rule, message }));
|
|
115
|
+
}
|
|
116
|
+
return record;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @typedef {object} AuditEvent
|
|
121
|
+
* @property {string} command - What exported, e.g. "cli --context-export", "mcp generate_context"
|
|
122
|
+
* @property {string} [root] - Exported project (default: the policy root)
|
|
123
|
+
* @property {string|null} [targetModel]
|
|
124
|
+
* @property {string|null} [format] - Output format or style (json, toon, gitingest, xml, ...)
|
|
125
|
+
* @property {string} destination - "stdout", "clipboard", "mcp", "rest" or a file path
|
|
126
|
+
* @property {string[]} [outputFiles] - Every file written, when there are several (chunks)
|
|
127
|
+
* @property {Array<string|{relativePath?: string, path?: string, tokens?: number}>} [files]
|
|
128
|
+
* @property {number} [tokens] - Token total (default: sum of files[].tokens)
|
|
129
|
+
* @property {number|Object<string, number>} [redactions] - Count, or counts per secret type
|
|
130
|
+
* @property {number} [removedByPolicy] - Files dropped by the deny rules
|
|
131
|
+
* @property {string|Buffer|Array<string|Buffer>} [text] - Exported text; only its hash is kept
|
|
132
|
+
* @property {string} [sha256] - Precomputed hash instead of `text`
|
|
133
|
+
* @property {string|null} [gitHead] - Override the commit (default: HEAD of `root`)
|
|
134
|
+
* @property {'exported'|'refused'} [outcome]
|
|
135
|
+
* @property {Array<{rule: string, message: string}>} [violations] - For refused exports
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Append an export event to the policy's audit log(s). Does nothing unless the policy
|
|
140
|
+
* enables `audit`. Never throws: a log that cannot be written produces a warning.
|
|
141
|
+
* @param {import('./policy.js').Policy} policy - From loadPolicy()
|
|
142
|
+
* @param {AuditEvent} event
|
|
143
|
+
* @param {object} [options]
|
|
144
|
+
* @param {(message: string) => void} [options.warn=console.warn]
|
|
145
|
+
* @returns {object|null} The record written, or null when auditing is off
|
|
146
|
+
*/
|
|
147
|
+
export function recordExport(policy, event = {}, { warn = defaultWarn } = {}) {
|
|
148
|
+
if (!policy?.audit?.enabled) return null;
|
|
149
|
+
|
|
150
|
+
let record;
|
|
151
|
+
try {
|
|
152
|
+
record = buildAuditRecord(policy, event);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
warn(`⚠️ Could not build the ctxman audit record: ${error.message}`);
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
159
|
+
for (const file of policy.audit.paths) {
|
|
160
|
+
try {
|
|
161
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
162
|
+
fs.appendFileSync(file, line, { encoding: 'utf8', mode: 0o600 });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
warn(`⚠️ Could not write the ctxman audit log ${file}: ${error.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return record;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Read audit logs. Lines that are not JSON objects are skipped and counted; events that
|
|
172
|
+
* appear in several logs (organisation and project) are listed once.
|
|
173
|
+
* @param {string[]} files
|
|
174
|
+
* @returns {{events: object[], skipped: number, missing: string[]}} Events oldest first
|
|
175
|
+
*/
|
|
176
|
+
export function readAuditLog(files) {
|
|
177
|
+
const events = [];
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const missing = [];
|
|
180
|
+
let skipped = 0;
|
|
181
|
+
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
let text;
|
|
184
|
+
try {
|
|
185
|
+
text = fs.readFileSync(file, 'utf8');
|
|
186
|
+
} catch (_error) {
|
|
187
|
+
missing.push(file);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const line of text.split('\n')) {
|
|
192
|
+
if (!line.trim()) continue;
|
|
193
|
+
let event;
|
|
194
|
+
try {
|
|
195
|
+
event = JSON.parse(line);
|
|
196
|
+
} catch (_error) {
|
|
197
|
+
skipped++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!event || typeof event !== 'object' || Array.isArray(event)) {
|
|
201
|
+
skipped++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (event.id && seen.has(event.id)) continue;
|
|
205
|
+
if (event.id) seen.add(event.id);
|
|
206
|
+
events.push(event);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
events.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
211
|
+
return { events, skipped, missing };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parse a --since value: a date ("2026-10-01", ISO timestamp) or an age ("30m", "12h",
|
|
216
|
+
* "7d", "2w")
|
|
217
|
+
* @param {string} value
|
|
218
|
+
* @param {Date} [now]
|
|
219
|
+
* @returns {Date}
|
|
220
|
+
* @throws {Error} For anything else
|
|
221
|
+
*/
|
|
222
|
+
export function parseSince(value, now = new Date()) {
|
|
223
|
+
const relative = /^(\d+)\s*([mhdw])$/i.exec(String(value).trim());
|
|
224
|
+
if (relative) {
|
|
225
|
+
const unit = { m: 60e3, h: 3600e3, d: 86400e3, w: 7 * 86400e3 }[relative[2].toLowerCase()];
|
|
226
|
+
return new Date(now.getTime() - Number(relative[1]) * unit);
|
|
227
|
+
}
|
|
228
|
+
const time = Date.parse(value);
|
|
229
|
+
if (Number.isNaN(time)) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`Invalid --since value ${JSON.stringify(value)}: use a date (2026-10-01) or an age (7d, 12h)`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return new Date(time);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Keep events at or after `since`, then the last `limit` of them
|
|
239
|
+
* @param {object[]} events - Oldest first
|
|
240
|
+
* @param {{since?: Date|null, limit?: number|null}} [options]
|
|
241
|
+
* @returns {object[]}
|
|
242
|
+
*/
|
|
243
|
+
export function filterAuditEvents(events, { since = null, limit = null } = {}) {
|
|
244
|
+
let result = events;
|
|
245
|
+
if (since) {
|
|
246
|
+
result = result.filter((event) => Date.parse(event.timestamp) >= since.getTime());
|
|
247
|
+
}
|
|
248
|
+
if (limit && limit > 0 && result.length > limit) result = result.slice(-limit);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const fit = (value, width, { keepEnd = false } = {}) => {
|
|
253
|
+
const text = value === null || value === undefined || value === '' ? '-' : String(value);
|
|
254
|
+
if (text.length <= width) return text.padEnd(width);
|
|
255
|
+
return keepEnd ? `…${text.slice(-(width - 1))}` : `${text.slice(0, width - 1)}…`;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// A file destination inside the exported project is shown relative to it
|
|
259
|
+
const destinationOf = (event) => {
|
|
260
|
+
const destination = event.destination;
|
|
261
|
+
if (typeof destination !== 'string' || typeof event.root !== 'string') return destination;
|
|
262
|
+
const relative = path.relative(event.root, destination);
|
|
263
|
+
return path.isAbsolute(destination) && relative && !relative.startsWith('..')
|
|
264
|
+
? relative
|
|
265
|
+
: destination;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Render events as a fixed-width table
|
|
270
|
+
* @param {object[]} events
|
|
271
|
+
* @returns {string}
|
|
272
|
+
*/
|
|
273
|
+
export function formatAuditTable(events) {
|
|
274
|
+
const columns = [
|
|
275
|
+
['TIME (UTC)', 20, (e) => String(e.timestamp ?? '').replace(/\.\d+Z$/, 'Z')],
|
|
276
|
+
['RESULT', 8, (e) => e.outcome],
|
|
277
|
+
['USER', 12, (e) => e.user],
|
|
278
|
+
['COMMAND', 26, (e) => e.command],
|
|
279
|
+
['DESTINATION', 28, destinationOf, { keepEnd: true }],
|
|
280
|
+
['MODEL', 16, (e) => e.targetModel],
|
|
281
|
+
['FILES', 6, (e) => e.fileCount],
|
|
282
|
+
['TOKENS', 10, (e) => (typeof e.totalTokens === 'number' ? e.totalTokens : null)],
|
|
283
|
+
['REDACT', 6, (e) => e.redactions],
|
|
284
|
+
['SHA256', 12, (e) => (e.sha256 ? e.sha256.slice(0, 12) : null)],
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const header = columns.map(([title, width]) => fit(title, width)).join(' ');
|
|
288
|
+
const rows = events.map((event) =>
|
|
289
|
+
columns.map(([, width, get, options]) => fit(get(event), width, options)).join(' ')
|
|
290
|
+
);
|
|
291
|
+
return [header, '-'.repeat(header.length), ...rows].map((line) => line.trimEnd()).join('\n');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export default { recordExport, readAuditLog, formatAuditTable, sha256 };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export enforcement: checks an export request against the effective policy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { recordExport } from './audit.js';
|
|
6
|
+
import { relativePathOf } from './policy.js';
|
|
7
|
+
|
|
8
|
+
/** Exit code of a CLI export the policy refused */
|
|
9
|
+
export const POLICY_VIOLATION_EXIT_CODE = 3;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An export the policy does not allow
|
|
13
|
+
*/
|
|
14
|
+
export class PolicyViolationError extends Error {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Array<{rule: string, message: string, sources: string[]}>} violations
|
|
17
|
+
*/
|
|
18
|
+
constructor(violations) {
|
|
19
|
+
super(
|
|
20
|
+
`Export refused by the ctxman policy:\n${violations.map((v) => ` - ${v.message}`).join('\n')}`
|
|
21
|
+
);
|
|
22
|
+
this.name = 'PolicyViolationError';
|
|
23
|
+
this.code = 'POLICY_VIOLATION';
|
|
24
|
+
this.violations = violations;
|
|
25
|
+
this.statusCode = 403;
|
|
26
|
+
this.exitCode = POLICY_VIOLATION_EXIT_CODE;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const formatNumber = (value) => value.toLocaleString('en-US');
|
|
31
|
+
const inFiles = (files) => (files.length > 0 ? ` (set in ${files.join(', ')})` : '');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check an export against the policy. Callers remove denied files before exporting; a
|
|
35
|
+
* denied file still in `files` is reported as a violation.
|
|
36
|
+
* @param {import('./policy.js').Policy} policy - From loadPolicy()
|
|
37
|
+
* @param {object} request
|
|
38
|
+
* @param {Array<string|{relativePath?: string, path?: string, tokens?: number}>} [request.files]
|
|
39
|
+
* Files in the export
|
|
40
|
+
* @param {number} [request.tokens] - Token total of the export (default: sum of files[].tokens)
|
|
41
|
+
* @param {boolean} [request.redact=true] - False when secret redaction was turned off (--no-redact)
|
|
42
|
+
* @param {string|null} [request.targetModel] - Model named with --target-model, if any
|
|
43
|
+
* @throws {PolicyViolationError} Listing every rule the export breaks
|
|
44
|
+
*/
|
|
45
|
+
export function enforceExport(
|
|
46
|
+
policy,
|
|
47
|
+
{ files = [], tokens, redact = true, targetModel = null } = {}
|
|
48
|
+
) {
|
|
49
|
+
if (!policy?.active) return;
|
|
50
|
+
|
|
51
|
+
const violations = [];
|
|
52
|
+
|
|
53
|
+
const deniedSources = new Set();
|
|
54
|
+
let denied = 0;
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
const source = policy.deniedBy(relativePathOf(file));
|
|
57
|
+
if (source) {
|
|
58
|
+
denied++;
|
|
59
|
+
deniedSources.add(source);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (denied > 0) {
|
|
63
|
+
violations.push({
|
|
64
|
+
rule: 'deny',
|
|
65
|
+
message: `${denied} file${denied === 1 ? '' : 's'} in the export ${denied === 1 ? 'matches' : 'match'} the policy's deny rules and can never be exported${inFiles([...deniedSources])}`,
|
|
66
|
+
sources: [...deniedSources],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (redact === false && policy.requireRedaction) {
|
|
71
|
+
violations.push({
|
|
72
|
+
rule: 'requireRedaction',
|
|
73
|
+
message: `secret redaction is required, so --no-redact is refused${inFiles(policy.requireRedactionSources)}`,
|
|
74
|
+
sources: policy.requireRedactionSources,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const total =
|
|
79
|
+
typeof tokens === 'number'
|
|
80
|
+
? tokens
|
|
81
|
+
: files.reduce((sum, file) => sum + (Number(file?.tokens) || 0), 0);
|
|
82
|
+
if (policy.maxExportTokens !== null && total > policy.maxExportTokens) {
|
|
83
|
+
violations.push({
|
|
84
|
+
rule: 'maxExportTokens',
|
|
85
|
+
message: `the export has ${formatNumber(total)} tokens, over the limit of ${formatNumber(policy.maxExportTokens)}${inFiles([policy.maxExportTokensSource])}; narrow it with .contextinclude, --changed-only or a template`,
|
|
86
|
+
sources: [policy.maxExportTokensSource],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (targetModel && !policy.isModelAllowed(targetModel)) {
|
|
91
|
+
const rejecting = policy.modelRules.filter(
|
|
92
|
+
(rule) => !rule.regexes.some((regex) => regex.test(targetModel))
|
|
93
|
+
);
|
|
94
|
+
const allowed = rejecting.map((rule) => rule.patterns.join(', ')).join('; and ');
|
|
95
|
+
violations.push({
|
|
96
|
+
rule: 'allowedTargetModels',
|
|
97
|
+
message: `target model "${targetModel}" is not allowed (allowed: ${allowed})${inFiles(rejecting.map((rule) => rule.file))}`,
|
|
98
|
+
sources: rejecting.map((rule) => rule.file),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (violations.length > 0) throw new PolicyViolationError(violations);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* enforceExport() followed by recordExport(): the usual call right before an export is
|
|
107
|
+
* handed out. A refused export is logged with outcome "refused" and the error re-thrown.
|
|
108
|
+
* @param {import('./policy.js').Policy} policy
|
|
109
|
+
* @param {import('./audit.js').AuditEvent & {redact?: boolean}} event
|
|
110
|
+
* @param {object} [options] - Passed to recordExport()
|
|
111
|
+
* @returns {object|null} The audit record, or null when auditing is off
|
|
112
|
+
* @throws {PolicyViolationError}
|
|
113
|
+
*/
|
|
114
|
+
export function guardExport(policy, event, options) {
|
|
115
|
+
if (!policy?.active) return null;
|
|
116
|
+
try {
|
|
117
|
+
enforceExport(policy, event);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof PolicyViolationError) {
|
|
120
|
+
recordExport(policy, { ...event, outcome: 'refused', violations: error.violations }, options);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
return recordExport(policy, event, options);
|
|
125
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-tool ignore files
|
|
3
|
+
*
|
|
4
|
+
* Other coding assistants keep their own ignore file next to .gitignore. A team that already
|
|
5
|
+
* lists its sensitive paths there can have ctxman honour the same lists instead of copying
|
|
6
|
+
* them into .contextignore. All of them use .gitignore syntax, so the existing parser reads
|
|
7
|
+
* them; only the files in the project root are read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import GitIgnoreParser from '../parsers/gitignore-parser.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Known ignore files, with the tool that reads each one
|
|
16
|
+
* @type {ReadonlyArray<{file: string, tool: string, emptyMeansAll?: boolean}>}
|
|
17
|
+
*/
|
|
18
|
+
export const KNOWN_IGNORE_FILES = Object.freeze([
|
|
19
|
+
{ file: '.cursorignore', tool: 'Cursor' },
|
|
20
|
+
{ file: '.cursorindexingignore', tool: 'Cursor (indexing only)' },
|
|
21
|
+
// Gemini Code Assist / Android Studio: an empty .aiexclude blocks its whole directory
|
|
22
|
+
{ file: '.aiexclude', tool: 'Gemini Code Assist', emptyMeansAll: true },
|
|
23
|
+
{ file: '.geminiignore', tool: 'Gemini CLI' },
|
|
24
|
+
{ file: '.augmentignore', tool: 'Augment Code' },
|
|
25
|
+
{ file: '.tabnineignore', tool: 'Tabnine' },
|
|
26
|
+
{ file: '.devinignore', tool: 'Devin' },
|
|
27
|
+
{ file: '.codeiumignore', tool: 'Windsurf (Codeium)' },
|
|
28
|
+
{ file: '.aiderignore', tool: 'Aider' },
|
|
29
|
+
{ file: '.clineignore', tool: 'Cline' },
|
|
30
|
+
{ file: '.rooignore', tool: 'Roo Code' },
|
|
31
|
+
{ file: '.continueignore', tool: 'Continue' },
|
|
32
|
+
{ file: '.repomixignore', tool: 'Repomix' },
|
|
33
|
+
{ file: '.gitingestignore', tool: 'Gitingest' },
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const KNOWN_BY_NAME = new Map(KNOWN_IGNORE_FILES.map((entry) => [entry.file, entry]));
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether a name can be used as an ignore file: a bare file name in the project root
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
export function isValidIgnoreFileName(name) {
|
|
44
|
+
return (
|
|
45
|
+
typeof name === 'string' &&
|
|
46
|
+
name.length > 0 &&
|
|
47
|
+
name !== '.' &&
|
|
48
|
+
name !== '..' &&
|
|
49
|
+
!/[/\\\0]/.test(name)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Turn a --respect-ignore-files value or a policy `ignoreFiles` list into file names.
|
|
55
|
+
* "all" stands for every known file; names are deduplicated and keep their order.
|
|
56
|
+
* @param {string|string[]|null|undefined} spec - "all", "a,b" or ["a", "b"]
|
|
57
|
+
* @returns {string[]}
|
|
58
|
+
* @throws {Error} For a name with a path separator or `..`
|
|
59
|
+
*/
|
|
60
|
+
export function resolveIgnoreFileNames(spec) {
|
|
61
|
+
if (spec === null || spec === undefined || spec === false) return [];
|
|
62
|
+
|
|
63
|
+
const items = (Array.isArray(spec) ? spec : String(spec).split(','))
|
|
64
|
+
.map((item) => String(item).trim())
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
|
|
67
|
+
const names = [];
|
|
68
|
+
for (const item of items) {
|
|
69
|
+
const expanded = item === 'all' ? KNOWN_IGNORE_FILES.map((entry) => entry.file) : [item];
|
|
70
|
+
for (const name of expanded) {
|
|
71
|
+
if (!isValidIgnoreFileName(name)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Invalid ignore file name ${JSON.stringify(name)}: expected a file name in the project root, such as .cursorignore, or "all"`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!names.includes(name)) names.push(name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Patterns of the given ignore files in `root`. Each file is applied on its own: a path is
|
|
84
|
+
* ignored when any of the files ignores it, and a `!` line only re-includes paths matched
|
|
85
|
+
* earlier in the same file.
|
|
86
|
+
* @param {string} root - Project root
|
|
87
|
+
* @param {string[]} names - File names from resolveIgnoreFileNames()
|
|
88
|
+
* @returns {IgnoreFileRules}
|
|
89
|
+
*/
|
|
90
|
+
export function loadIgnoreFileRules(root, names) {
|
|
91
|
+
const parser = new GitIgnoreParser(null, null, null);
|
|
92
|
+
const files = names.map((name) => {
|
|
93
|
+
const filePath = path.join(root, name);
|
|
94
|
+
const known = KNOWN_BY_NAME.get(name);
|
|
95
|
+
const exists = isFile(filePath);
|
|
96
|
+
let patterns = exists ? parser.parsePatternFile(filePath) : [];
|
|
97
|
+
if (exists && patterns.length === 0 && known?.emptyMeansAll) {
|
|
98
|
+
patterns = [parser.convertToRegex('**')];
|
|
99
|
+
}
|
|
100
|
+
return { name, path: filePath, tool: known?.tool ?? null, exists, patterns };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return new IgnoreFileRules(parser, files);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isFile(filePath) {
|
|
107
|
+
try {
|
|
108
|
+
return fs.statSync(filePath).isFile();
|
|
109
|
+
} catch (_error) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Loaded ignore files and the matcher over them
|
|
116
|
+
*/
|
|
117
|
+
export class IgnoreFileRules {
|
|
118
|
+
/**
|
|
119
|
+
* @param {GitIgnoreParser} parser - Supplies the pattern matching
|
|
120
|
+
* @param {Array<{name: string, path: string, tool: string|null, exists: boolean, patterns: object[]}>} files
|
|
121
|
+
*/
|
|
122
|
+
constructor(parser, files) {
|
|
123
|
+
this.parser = parser;
|
|
124
|
+
this.files = files;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @returns {boolean} True when at least one of the files exists */
|
|
128
|
+
get active() {
|
|
129
|
+
return this.files.some((file) => file.exists);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} relativePath - Path relative to the project root (file or directory)
|
|
134
|
+
* @returns {string|null} Name of the first ignore file that ignores the path, or null
|
|
135
|
+
*/
|
|
136
|
+
match(relativePath) {
|
|
137
|
+
const rel = relativePath.split(path.sep).join('/');
|
|
138
|
+
for (const file of this.files) {
|
|
139
|
+
if (file.patterns.length > 0 && this.parser.testPatterns(file.patterns, rel, file.name)) {
|
|
140
|
+
return file.name;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} relativePath
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
isIgnored(relativePath) {
|
|
151
|
+
return this.match(relativePath) !== null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* One line per file for console output, e.g. ".cursorignore (3 patterns)"
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
describe() {
|
|
159
|
+
return this.files
|
|
160
|
+
.map((file) =>
|
|
161
|
+
file.exists
|
|
162
|
+
? `${file.name} (${file.patterns.length} pattern${file.patterns.length === 1 ? '' : 's'})`
|
|
163
|
+
: `${file.name} (not found)`
|
|
164
|
+
)
|
|
165
|
+
.join(', ');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default { KNOWN_IGNORE_FILES, resolveIgnoreFileNames, loadIgnoreFileRules };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export policy and audit log
|
|
3
|
+
*
|
|
4
|
+
* The calls an exporting command makes:
|
|
5
|
+
*
|
|
6
|
+
* const policy = loadPolicy(root); // once, before scanning
|
|
7
|
+
* const scanner = new Scanner(root, { policy }); // drops denied files
|
|
8
|
+
* ...build the export...
|
|
9
|
+
* guardExport(policy, { command, destination, format, targetModel, files, tokens,
|
|
10
|
+
* redact, redactions, text }); // throws PolicyViolationError
|
|
11
|
+
* ...write / send the export...
|
|
12
|
+
*
|
|
13
|
+
* guardExport() is enforceExport() plus recordExport(); call the two separately when the
|
|
14
|
+
* export must be checked before it is built.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
loadPolicy,
|
|
19
|
+
Policy,
|
|
20
|
+
PolicyConfigError,
|
|
21
|
+
validatePolicyDocument,
|
|
22
|
+
createStarterPolicy,
|
|
23
|
+
normalizeRelativePath,
|
|
24
|
+
relativePathOf,
|
|
25
|
+
modelGlobToRegex,
|
|
26
|
+
POLICY_FILE,
|
|
27
|
+
POLICY_ENV,
|
|
28
|
+
DEFAULT_AUDIT_PATH,
|
|
29
|
+
POLICY_DOCS_URL,
|
|
30
|
+
} from './policy.js';
|
|
31
|
+
export {
|
|
32
|
+
enforceExport,
|
|
33
|
+
guardExport,
|
|
34
|
+
PolicyViolationError,
|
|
35
|
+
POLICY_VIOLATION_EXIT_CODE,
|
|
36
|
+
} from './enforce.js';
|
|
37
|
+
export {
|
|
38
|
+
recordExport,
|
|
39
|
+
buildAuditRecord,
|
|
40
|
+
readAuditLog,
|
|
41
|
+
filterAuditEvents,
|
|
42
|
+
formatAuditTable,
|
|
43
|
+
parseSince,
|
|
44
|
+
getGitHead,
|
|
45
|
+
sha256,
|
|
46
|
+
} from './audit.js';
|
|
47
|
+
export {
|
|
48
|
+
KNOWN_IGNORE_FILES,
|
|
49
|
+
resolveIgnoreFileNames,
|
|
50
|
+
loadIgnoreFileRules,
|
|
51
|
+
IgnoreFileRules,
|
|
52
|
+
} from './ignore-files.js';
|