@trazum/cli 1.8.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/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/git.d.ts +81 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +225 -0
- package/dist/git.js.map +1 -0
- package/dist/i18n/en.d.ts +10 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +694 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/es.d.ts +4 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +694 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/index.d.ts +34 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +46 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +363 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +2 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2824 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +246 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +492 -0
- package/dist/markdown.js.map +1 -0
- package/dist/suggest-cache.d.ts +119 -0
- package/dist/suggest-cache.d.ts.map +1 -0
- package/dist/suggest-cache.js +225 -0
- package/dist/suggest-cache.js.map +1 -0
- package/package.json +49 -0
- package/src/git.ts +294 -0
- package/src/i18n/en.ts +825 -0
- package/src/i18n/es.ts +838 -0
- package/src/i18n/index.ts +57 -0
- package/src/i18n/types.ts +372 -0
- package/src/index.ts +3873 -0
- package/src/markdown.ts +717 -0
- package/src/suggest-cache.ts +268 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Muñoz Rey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @trazum/cli
|
|
2
|
+
|
|
3
|
+
Optimise prompts, enforce token budgets in CI, and price the saving — from your
|
|
4
|
+
terminal.
|
|
5
|
+
|
|
6
|
+
**Zero runtime dependencies**, asserted in CI. This tool reads your prompts;
|
|
7
|
+
every dependency would be someone else's code reading them too.
|
|
8
|
+
|
|
9
|
+
Part of [Trazum](https://github.com/Davmunrey/Trazum). For the library, see
|
|
10
|
+
[`@trazum/core`](https://www.npmjs.com/package/@trazum/core).
|
|
11
|
+
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> **Not published yet.** The command below will not work today — there is no tag
|
|
14
|
+
> and `npm view` returns 404. Run it from source meanwhile:
|
|
15
|
+
> [github.com/Davmunrey/Trazum](https://github.com/Davmunrey/Trazum#getting-started).
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g @trazum/cli
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| | |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `trazum optimize <file>` | shorten it, and price what that is worth |
|
|
26
|
+
| `trazum check <file\|dir>` | does it fit its budget, and has the repo drifted past its baseline — exits 1 when either fails |
|
|
27
|
+
| `trazum baseline [dir]` | what the prompts cost now, recorded to a file you commit |
|
|
28
|
+
| `trazum doctor [dir]` | the whole workspace: what nothing is watching, and what fixing would be worth |
|
|
29
|
+
| `trazum rank <dir>` | of these forty prompts, which is worth an afternoon |
|
|
30
|
+
| `trazum prune <file> --cases <file>` | which few-shot examples earn their tokens — measured, and it asks before spending |
|
|
31
|
+
| `trazum diff <before> <after>` | somebody edited this; did it get worse? |
|
|
32
|
+
| `trazum blame <file>` | who made this prompt expensive, and when |
|
|
33
|
+
| `trazum eval <file> --cases <file>` | does the shorter prompt still work |
|
|
34
|
+
| `trazum where [file]` | which prompts hide in my source files, and which model they call |
|
|
35
|
+
| `trazum models` · `trazum rules` | prices; what each rule does |
|
|
36
|
+
|
|
37
|
+
`trazum --help` documents every flag. Reports are available in English and
|
|
38
|
+
Spanish (`--locale es`); the locale changes the report, never the optimisation.
|
|
39
|
+
|
|
40
|
+
## Optimise
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
trazum optimize prompt.txt --calls 50000 --diff
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Input tokens
|
|
48
|
+
190 → 137 -27.9% (estimated, ±15%)
|
|
49
|
+
|
|
50
|
+
Cost with Claude Opus 5
|
|
51
|
+
50,000 calls/month · 300 output tokens per call
|
|
52
|
+
$422.50 → $409.25 saving $13.25/month (3.1%)
|
|
53
|
+
|
|
54
|
+
Beyond shortening the prompt
|
|
55
|
+
→ This task may not need Claude Opus 5 ~$327.40/month
|
|
56
|
+
→ If the work tolerates latency, use the Batch API ~$204.62/month
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Read the last block first.** Trimming is usually not where the money is.
|
|
60
|
+
|
|
61
|
+
Redirected output is only the optimised prompt, so it pipes cleanly:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cat prompt.md | trazum optimize - --level aggressive > prompt.optimised.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Budgets in CI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
trazum check prompts/system.txt --max-tokens 2000
|
|
71
|
+
# FAILED 2,481 tokens busts the budget of 2,000.
|
|
72
|
+
# Optimised with "trazum optimize --level safe" it would land at ~1,913 tokens and fit.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Point it at a **directory** to govern a whole repository in one step, using the
|
|
76
|
+
per-pattern budgets in `trazum.config.json`. A file no pattern covers is listed
|
|
77
|
+
as unbudgeted rather than skipped quietly, and a run where nothing at all was
|
|
78
|
+
budgeted is an error — "0 failures" from a check that measured nothing is the
|
|
79
|
+
most misleading thing this tool could tell you.
|
|
80
|
+
|
|
81
|
+
`--markdown-out <file>` writes the report as GitHub-flavoured markdown, for a
|
|
82
|
+
step summary or a pull request comment. There is also a
|
|
83
|
+
[packaged GitHub Action](https://github.com/Davmunrey/Trazum#cli).
|
|
84
|
+
|
|
85
|
+
## Did this edit make it worse?
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
trazum diff old.txt new.txt --calls 50000 --max-growth 10
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Every figure is `after - before`, so positive means worse** — the opposite of
|
|
92
|
+
every other Trazum output. The gate is opt-in: growth alone exits 0, and
|
|
93
|
+
`--max-growth` is what makes it exit 1. A tool that fails a build nobody armed
|
|
94
|
+
gets removed from the pipeline rather than fixed.
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
`trazum.config.json`, found by walking up from the working directory:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"level": "safe",
|
|
103
|
+
"usage": { "model": "claude-opus-5", "callsPerMonth": 50000 },
|
|
104
|
+
"budgets": { "prompts/**": 2000, "prompts/system.txt": 4000 },
|
|
105
|
+
"maxGrowth": 100,
|
|
106
|
+
"pricing": "./prices.json"
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Flags beat the config; the config beats the defaults. A boolean the config
|
|
111
|
+
switched on comes back off with `--no-batch`.
|
|
112
|
+
|
|
113
|
+
**An invalid config is a hard error, including an unknown key.** A lenient parser
|
|
114
|
+
would restore defaults silently, and for a budget the default is *no budget* — a
|
|
115
|
+
green build for a prompt nobody measured.
|
|
116
|
+
|
|
117
|
+
`pricing` points at a local price overlay, so correcting a published price does
|
|
118
|
+
not require upgrading Trazum. Every report says when overlaid prices were used.
|
|
119
|
+
|
|
120
|
+
## Does the shorter prompt still work?
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
trazum eval prompt.txt --cases cases.txt --level aggressive
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Everything else Trazum reports is arithmetic; this is not. It runs the original
|
|
127
|
+
**twice** per case to measure the model's own run-to-run variance, and judges the
|
|
128
|
+
rewrite against that rather than against a determinism it never had. Costs three
|
|
129
|
+
provider calls per case, prints the count before spending anything, and says
|
|
130
|
+
`inconclusive` rather than inventing a verdict when the baseline is too noisy.
|
|
131
|
+
|
|
132
|
+
Needs `TRAZUM_LLM_*` configured. Exits 1 on `diverges`.
|
|
133
|
+
|
|
134
|
+
MIT.
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
export interface Revision {
|
|
3
|
+
sha: string;
|
|
4
|
+
shortSha: string;
|
|
5
|
+
author: string;
|
|
6
|
+
/** ISO 8601, author date. */
|
|
7
|
+
date: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The spawn, injectable — the same seam the LLM providers use for `fetch`.
|
|
12
|
+
*
|
|
13
|
+
* Exported because the retry below is otherwise untestable: provoking a real
|
|
14
|
+
* `EAGAIN` means exhausting the process table, which is not something a test
|
|
15
|
+
* suite should do to the machine running it. A retry nothing checks is a retry
|
|
16
|
+
* somebody deletes in a refactor, and mutation testing said exactly that.
|
|
17
|
+
*
|
|
18
|
+
* It widens nothing in practice: the CLI has no library entry, so this module
|
|
19
|
+
* is reachable only from inside this package and from its tests.
|
|
20
|
+
*/
|
|
21
|
+
export type SpawnLike = typeof spawnSync;
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when git could not be run, as distinct from git having nothing to say.
|
|
24
|
+
*
|
|
25
|
+
* A distinct type rather than a message, so a caller cannot accidentally treat
|
|
26
|
+
* it as an empty result — which is the whole bug.
|
|
27
|
+
*/
|
|
28
|
+
export declare class GitUnavailableError extends Error {
|
|
29
|
+
constructor(detail: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether `git` can be run at all.
|
|
33
|
+
*
|
|
34
|
+
* Separate from `repositoryRoot` because the two failures need different
|
|
35
|
+
* advice: "install git" and "run this inside a repository" have nothing to do
|
|
36
|
+
* with each other, and a single "could not read history" would send half the
|
|
37
|
+
* readers looking in the wrong place.
|
|
38
|
+
*/
|
|
39
|
+
export declare function gitAvailable(cwd: string): boolean;
|
|
40
|
+
/** The repository root containing `cwd`, or `null` if there is not one. */
|
|
41
|
+
export declare function repositoryRoot(cwd: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* The path as git knows it — relative to the repository root, forward slashes —
|
|
44
|
+
* or `null` when it falls outside the repository.
|
|
45
|
+
*/
|
|
46
|
+
export declare function pathInRepository(root: string, target: string): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Commits that touched `repoPath`, newest first.
|
|
49
|
+
*
|
|
50
|
+
* `--follow` so a renamed prompt keeps its history: a file moved from
|
|
51
|
+
* `prompt.txt` to `prompts/support.txt` is the same prompt, and a cost history
|
|
52
|
+
* that restarts at the rename is telling you the growth began the day somebody
|
|
53
|
+
* tidied the directory.
|
|
54
|
+
*/
|
|
55
|
+
export declare function revisionsFor(repoPath: string, options: {
|
|
56
|
+
cwd: string;
|
|
57
|
+
max: number;
|
|
58
|
+
spawn?: SpawnLike;
|
|
59
|
+
}): Revision[];
|
|
60
|
+
/**
|
|
61
|
+
* The file's content at a commit, or `null` if it did not exist there.
|
|
62
|
+
*
|
|
63
|
+
* `--follow` above means the path can differ from the one at that commit, so
|
|
64
|
+
* this asks git for the name it had rather than assuming today's.
|
|
65
|
+
*/
|
|
66
|
+
export declare function contentAt(sha: string, repoPath: string, cwd: string): string | null;
|
|
67
|
+
/**
|
|
68
|
+
* The name the file had at each commit, keyed by sha.
|
|
69
|
+
*
|
|
70
|
+
* One `git log` rather than one per revision, and it replaces a version that
|
|
71
|
+
* asked per commit and got nothing back: `git log --follow --max-count=1 <sha>
|
|
72
|
+
* -- <today's name>` returns an empty list for every commit before a rename,
|
|
73
|
+
* because at those commits that name did not exist. The effect was that a
|
|
74
|
+
* renamed prompt showed "not present" for its entire history before the move —
|
|
75
|
+
* the data was there, under the old name, and the report said there was none.
|
|
76
|
+
*
|
|
77
|
+
* Asking once, without a starting commit, lets `--follow` do the mapping it
|
|
78
|
+
* exists for: the output pairs each sha with the path it touched.
|
|
79
|
+
*/
|
|
80
|
+
export declare function namesByRevision(repoPath: string, cwd: string, max: number): Map<string, string>;
|
|
81
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAwC/C,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AA8BD;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,SAAS,CAAC;AAuDzC;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,MAAM,EAAE,MAAM,EAGzB;CACF;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5E;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,SAAS,CAAA;CAAE,GACvD,QAAQ,EAAE,CAyCZ;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKnF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAwB/F"}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* The only place in this repository that runs another program.
|
|
5
|
+
*
|
|
6
|
+
* `trazum blame` needs a file's history, and the history lives in git. Nothing
|
|
7
|
+
* else here has ever shelled out, so this module is written as if it were the
|
|
8
|
+
* whole attack surface — because it is.
|
|
9
|
+
*
|
|
10
|
+
* The rules, and why each one is here rather than in a comment on the call site:
|
|
11
|
+
*
|
|
12
|
+
* - **No shell, ever.** `spawnSync` with an argv array and `shell: false`, which
|
|
13
|
+
* is the default and is stated anyway. The moment a path reaches a shell
|
|
14
|
+
* string, a file called `; rm -rf ~` is a command.
|
|
15
|
+
* - **Paths go after `--`.** Without it, a file named `--upload-pack=curl…` or
|
|
16
|
+
* `--output=…` is read by git as an option, and git has options that run
|
|
17
|
+
* programs. This is the specific reason `trazum blame -- <path>` is not
|
|
18
|
+
* enough on its own: the separator has to be in *our* argv, not the user's.
|
|
19
|
+
* - **Object names are validated before they are used.** `git show <sha>:<path>`
|
|
20
|
+
* glues two values into one argument, so the sha is checked against
|
|
21
|
+
* `/^[0-9a-f]{40}$/` before it can contribute anything to it.
|
|
22
|
+
* - **Everything is bounded.** A timeout so a hung git does not wedge the CLI, a
|
|
23
|
+
* `maxBuffer` so a large blob cannot exhaust memory, and a revision cap so
|
|
24
|
+
* `blame` on a file with 40,000 commits terminates.
|
|
25
|
+
* - **The path must be inside the repository.** Reading history for
|
|
26
|
+
* `../../elsewhere/secrets.txt` is not a thing this command exists to do.
|
|
27
|
+
*
|
|
28
|
+
* Failures are `null` or an empty array rather than exceptions with git's own
|
|
29
|
+
* wording. The caller turns them into a sentence in the reader's language.
|
|
30
|
+
*/
|
|
31
|
+
/** Ten seconds is a long time for `git log`; a hung one should not be forever. */
|
|
32
|
+
const TIMEOUT_MS = 10_000;
|
|
33
|
+
/** 32 MB. A prompt file this big is not a prompt file. */
|
|
34
|
+
const MAX_BUFFER = 32 * 1024 * 1024;
|
|
35
|
+
const SHA = /^[0-9a-f]{40}$/;
|
|
36
|
+
/**
|
|
37
|
+
* Errors that mean *the process could not be started*, not that git said no.
|
|
38
|
+
*
|
|
39
|
+
* `EAGAIN` is the kernel refusing a fork because the process or thread limit is
|
|
40
|
+
* momentarily full; `ENOMEM` is the same story with memory. Both are properties
|
|
41
|
+
* of the machine at that instant and both are gone a moment later, which is
|
|
42
|
+
* exactly why they surface on a loaded CI runner and never on a laptop.
|
|
43
|
+
*/
|
|
44
|
+
const TRANSIENT = new Set(['EAGAIN', 'ENOMEM']);
|
|
45
|
+
function runGit(args, cwd, spawn = spawnSync) {
|
|
46
|
+
/**
|
|
47
|
+
* Bounded by the loop, not by a condition inside it.
|
|
48
|
+
*
|
|
49
|
+
* Written as `for (;;)` with a `continue` guarded by `attempt === 0` first,
|
|
50
|
+
* which is one edit away from retrying for ever — and mutation testing does
|
|
51
|
+
* not report that as a surviving mutant, it reports it as the suite hanging
|
|
52
|
+
* until the runner is killed. In CI that is a job that burns its whole
|
|
53
|
+
* timeout instead of failing in a second.
|
|
54
|
+
*
|
|
55
|
+
* Two attempts, and only for a failure to *start* the process. A git that ran
|
|
56
|
+
* and exited non-zero is answering, and asking it twice would just re-run a
|
|
57
|
+
* command that already failed for a reason.
|
|
58
|
+
*/
|
|
59
|
+
const ATTEMPTS = 2;
|
|
60
|
+
let last = { ran: false, stdout: null, detail: 'not attempted' };
|
|
61
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
62
|
+
const result = spawn('git', args, {
|
|
63
|
+
cwd,
|
|
64
|
+
// Stated rather than left to the default: this is the line that matters.
|
|
65
|
+
shell: false,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
timeout: TIMEOUT_MS,
|
|
68
|
+
maxBuffer: MAX_BUFFER,
|
|
69
|
+
// No prompting for credentials, no pager waiting on a TTY that is not there.
|
|
70
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_PAGER: 'cat', PAGER: 'cat' },
|
|
71
|
+
});
|
|
72
|
+
if (result.error !== undefined) {
|
|
73
|
+
const code = result.error.code ?? '';
|
|
74
|
+
last = { ran: false, stdout: null, detail: code || result.error.message };
|
|
75
|
+
if (TRANSIENT.has(code))
|
|
76
|
+
continue;
|
|
77
|
+
return last;
|
|
78
|
+
}
|
|
79
|
+
if (result.status !== 0)
|
|
80
|
+
return { ran: true, stdout: null };
|
|
81
|
+
return { ran: true, stdout: result.stdout };
|
|
82
|
+
}
|
|
83
|
+
// Every attempt was refused before git started. The machine is out of
|
|
84
|
+
// whatever it ran out of, and saying so beats a third try.
|
|
85
|
+
return last;
|
|
86
|
+
}
|
|
87
|
+
function git(args, cwd) {
|
|
88
|
+
return runGit(args, cwd).stdout;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Thrown when git could not be run, as distinct from git having nothing to say.
|
|
92
|
+
*
|
|
93
|
+
* A distinct type rather than a message, so a caller cannot accidentally treat
|
|
94
|
+
* it as an empty result — which is the whole bug.
|
|
95
|
+
*/
|
|
96
|
+
export class GitUnavailableError extends Error {
|
|
97
|
+
constructor(detail) {
|
|
98
|
+
super(`could not run git (${detail})`);
|
|
99
|
+
this.name = 'GitUnavailableError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether `git` can be run at all.
|
|
104
|
+
*
|
|
105
|
+
* Separate from `repositoryRoot` because the two failures need different
|
|
106
|
+
* advice: "install git" and "run this inside a repository" have nothing to do
|
|
107
|
+
* with each other, and a single "could not read history" would send half the
|
|
108
|
+
* readers looking in the wrong place.
|
|
109
|
+
*/
|
|
110
|
+
export function gitAvailable(cwd) {
|
|
111
|
+
return git(['--version'], cwd) !== null;
|
|
112
|
+
}
|
|
113
|
+
/** The repository root containing `cwd`, or `null` if there is not one. */
|
|
114
|
+
export function repositoryRoot(cwd) {
|
|
115
|
+
const out = git(['rev-parse', '--show-toplevel'], cwd);
|
|
116
|
+
return out === null ? null : out.trim() || null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The path as git knows it — relative to the repository root, forward slashes —
|
|
120
|
+
* or `null` when it falls outside the repository.
|
|
121
|
+
*/
|
|
122
|
+
export function pathInRepository(root, target) {
|
|
123
|
+
const rel = relative(resolve(root), resolve(target));
|
|
124
|
+
// Empty means the target *is* the root, `..` at the front means it escaped,
|
|
125
|
+
// and an absolute result means the two are on different Windows drives. All
|
|
126
|
+
// three are "outside the repository".
|
|
127
|
+
if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return rel.split(sep).join('/');
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Commits that touched `repoPath`, newest first.
|
|
134
|
+
*
|
|
135
|
+
* `--follow` so a renamed prompt keeps its history: a file moved from
|
|
136
|
+
* `prompt.txt` to `prompts/support.txt` is the same prompt, and a cost history
|
|
137
|
+
* that restarts at the rename is telling you the growth began the day somebody
|
|
138
|
+
* tidied the directory.
|
|
139
|
+
*/
|
|
140
|
+
export function revisionsFor(repoPath, options) {
|
|
141
|
+
// A unit separator between fields and a record separator between commits:
|
|
142
|
+
// both are characters git will not emit inside a name or a subject, unlike
|
|
143
|
+
// the tab and newline that a commit subject can absolutely contain.
|
|
144
|
+
// Written as escapes, not typed in. A raw control byte in a source file is
|
|
145
|
+
// how `scripts/measure-token-band.mjs` ended up with no reviewable diff for
|
|
146
|
+
// three commits, one of which was a security fix.
|
|
147
|
+
const FIELD = '\u001f';
|
|
148
|
+
const RECORD = '\u001e';
|
|
149
|
+
const format = ['%H', '%h', '%an', '%aI', '%s'].join(FIELD) + RECORD;
|
|
150
|
+
const outcome = runGit([
|
|
151
|
+
'log',
|
|
152
|
+
'--follow',
|
|
153
|
+
`--max-count=${Math.max(1, Math.floor(options.max))}`,
|
|
154
|
+
`--format=${format}`,
|
|
155
|
+
// Everything after this is a path, whatever it looks like.
|
|
156
|
+
'--',
|
|
157
|
+
repoPath,
|
|
158
|
+
], options.cwd, options.spawn);
|
|
159
|
+
// The distinction this function used to lose. An empty list now means git
|
|
160
|
+
// looked and found nothing; being unable to look throws instead, so it can
|
|
161
|
+
// never be reported to somebody as a fact about their repository.
|
|
162
|
+
if (!outcome.ran)
|
|
163
|
+
throw new GitUnavailableError(outcome.detail);
|
|
164
|
+
const out = outcome.stdout;
|
|
165
|
+
if (out === null)
|
|
166
|
+
return [];
|
|
167
|
+
return out
|
|
168
|
+
.split(RECORD)
|
|
169
|
+
.map((record) => record.replace(/^\n/, ''))
|
|
170
|
+
.filter((record) => record.trim() !== '')
|
|
171
|
+
.map((record) => {
|
|
172
|
+
const [sha = '', shortSha = '', author = '', date = '', subject = ''] = record.split(FIELD);
|
|
173
|
+
return { sha, shortSha, author, date, subject };
|
|
174
|
+
})
|
|
175
|
+
.filter((revision) => SHA.test(revision.sha));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The file's content at a commit, or `null` if it did not exist there.
|
|
179
|
+
*
|
|
180
|
+
* `--follow` above means the path can differ from the one at that commit, so
|
|
181
|
+
* this asks git for the name it had rather than assuming today's.
|
|
182
|
+
*/
|
|
183
|
+
export function contentAt(sha, repoPath, cwd) {
|
|
184
|
+
// The sha becomes part of a single `sha:path` argument, so it is checked
|
|
185
|
+
// before it can contribute anything to one.
|
|
186
|
+
if (!SHA.test(sha))
|
|
187
|
+
return null;
|
|
188
|
+
return git(['show', `${sha}:${repoPath}`], cwd);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The name the file had at each commit, keyed by sha.
|
|
192
|
+
*
|
|
193
|
+
* One `git log` rather than one per revision, and it replaces a version that
|
|
194
|
+
* asked per commit and got nothing back: `git log --follow --max-count=1 <sha>
|
|
195
|
+
* -- <today's name>` returns an empty list for every commit before a rename,
|
|
196
|
+
* because at those commits that name did not exist. The effect was that a
|
|
197
|
+
* renamed prompt showed "not present" for its entire history before the move —
|
|
198
|
+
* the data was there, under the old name, and the report said there was none.
|
|
199
|
+
*
|
|
200
|
+
* Asking once, without a starting commit, lets `--follow` do the mapping it
|
|
201
|
+
* exists for: the output pairs each sha with the path it touched.
|
|
202
|
+
*/
|
|
203
|
+
export function namesByRevision(repoPath, cwd, max) {
|
|
204
|
+
const MARK = '\u001e';
|
|
205
|
+
const out = git([
|
|
206
|
+
'log',
|
|
207
|
+
'--follow',
|
|
208
|
+
'--name-only',
|
|
209
|
+
`--max-count=${Math.max(1, Math.floor(max))}`,
|
|
210
|
+
`--format=${MARK}%H`,
|
|
211
|
+
'--',
|
|
212
|
+
repoPath,
|
|
213
|
+
], cwd);
|
|
214
|
+
const names = new Map();
|
|
215
|
+
if (out === null)
|
|
216
|
+
return names;
|
|
217
|
+
for (const record of out.split(MARK)) {
|
|
218
|
+
const lines = record.split('\n').map((line) => line.trim()).filter((line) => line !== '');
|
|
219
|
+
const [sha, name] = lines;
|
|
220
|
+
if (sha !== undefined && name !== undefined && SHA.test(sha))
|
|
221
|
+
names.set(sha, name);
|
|
222
|
+
}
|
|
223
|
+
return names;
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=git.js.map
|
package/dist/git.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CliMessages } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* English catalogue — the source of truth.
|
|
4
|
+
*
|
|
5
|
+
* When a message changes here, the other catalogues need the same change.
|
|
6
|
+
* `test/i18n.test.js` in the core enforces the equivalent guarantee for the
|
|
7
|
+
* library's catalogues.
|
|
8
|
+
*/
|
|
9
|
+
export declare const en: CliMessages;
|
|
10
|
+
//# sourceMappingURL=en.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C;;;;;;GAMG;AACH,eAAO,MAAM,EAAE,EAAE,WA0yBhB,CAAC"}
|