@milehimikey/em 1.2.0 → 1.4.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/.claude/skills/event-modeling/SKILL.md +66 -16
- package/.claude/skills/event-modeling/reference/conform.md +228 -0
- package/.claude/skills/event-modeling/reference/em-dsl.md +22 -5
- package/.claude/skills/event-modeling/reference/extract.md +5 -4
- package/.claude/skills/event-modeling/reference/methodology.md +11 -3
- package/.claude/skills/event-modeling/templates/conformance-report.md +69 -0
- package/.claude/skills/event-modeling/templates/model-readme.md +6 -4
- package/.claude/skills/event-modeling/templates/slice.md +11 -2
- package/.claude/skills/event-modeling/templates/state.md +17 -7
- package/README.md +17 -3
- package/dist/cli/changelog-git.d.ts +53 -0
- package/dist/cli/changelog-git.js +112 -0
- package/dist/cli/changelog-git.js.map +1 -0
- package/dist/cli/diff-inputs.d.ts +44 -0
- package/dist/cli/diff-inputs.js +62 -0
- package/dist/cli/diff-inputs.js.map +1 -0
- package/dist/cli.js +231 -7
- package/dist/cli.js.map +1 -1
- package/dist/emit/changelog.d.ts +57 -0
- package/dist/emit/changelog.js +143 -0
- package/dist/emit/changelog.js.map +1 -0
- package/dist/emit/diffJson.d.ts +17 -0
- package/dist/emit/diffJson.js +74 -0
- package/dist/emit/diffJson.js.map +1 -0
- package/dist/emit/json.d.ts +29 -0
- package/dist/emit/json.js +121 -0
- package/dist/emit/json.js.map +1 -0
- package/dist/model/diff.d.ts +52 -0
- package/dist/model/diff.js +351 -0
- package/dist/model/diff.js.map +1 -0
- package/dist/model/model.d.ts +2 -0
- package/dist/model/model.js +1 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/validate.js +74 -0
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +2 -0
- package/dist/parser/parser.js +8 -0
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.d.ts +8 -2
- package/dist/render/drawNotes.js +106 -42
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.js +5 -4
- package/dist/render/render.js.map +1 -1
- package/package.json +3 -2
|
@@ -10,6 +10,7 @@ Keep it current at the end of every working session.
|
|
|
10
10
|
- **Current phase:** {{extract | discover | model | slice}}
|
|
11
11
|
- **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
|
|
12
12
|
- **Last updated:** {{YYYY-MM-DD}}
|
|
13
|
+
- **Last conformance:** {{YYYY-MM-DD @ <target-repo revision> — report: conformance/<date>-report.md | never}}
|
|
13
14
|
|
|
14
15
|
## Session inputs
|
|
15
16
|
- **Scope line:** {{one-line description of what's in/out of bounds for this model}}
|
|
@@ -18,6 +19,11 @@ Keep it current at the end of every working session.
|
|
|
18
19
|
- **Source mode:** {{greenfield | extract-event-driven | extract-procedural}}
|
|
19
20
|
- **Existing system refs:** {{repo paths, event-schema/topic locations, docs — or "n/a"}}
|
|
20
21
|
|
|
22
|
+
## Participants
|
|
23
|
+
<!-- Populate at session start. Live workshop: one human proxy relays questions to the room;
|
|
24
|
+
attribute every answer/decision in the Decisions log to a named participant here. -->
|
|
25
|
+
- {{Name}} — {{role}} — {{domain area}}
|
|
26
|
+
|
|
21
27
|
## Extraction progress (existing-system models only — delete for greenfield)
|
|
22
28
|
- [ ] R1. Candidate events (extracted/synthesized, filtered, confirmed)
|
|
23
29
|
- [ ] R2. Timeline order (as-is narrative, actors/callers, first render)
|
|
@@ -37,14 +43,18 @@ Keep it current at the end of every working session.
|
|
|
37
43
|
- [ ] 7. Evaluate completeness (`em validate` clean)
|
|
38
44
|
|
|
39
45
|
## Decisions log
|
|
40
|
-
<!-- Resolved choices, with the reasoning, so they aren't re-litigated.
|
|
41
|
-
|
|
46
|
+
<!-- Resolved choices, with the reasoning, so they aren't re-litigated. In a live workshop,
|
|
47
|
+
attribute each entry to the participant who made the call (see Participants above). -->
|
|
48
|
+
- {{YYYY-MM-DD}}: {{decision}} — {{why}} — by {{participant, if a live workshop}}
|
|
42
49
|
|
|
43
50
|
## Open questions / parking lot
|
|
44
|
-
<!-- Unresolved items to bring back to the user. Never guess these.
|
|
45
|
-
|
|
51
|
+
<!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and
|
|
52
|
+
flat — add only what's known: source (ticket/conversation that spawned it), blocked on
|
|
53
|
+
(who/what), revisit (when). -->
|
|
54
|
+
- [ ] {{question}} — source: {{ticket/conversation link, optional}} — blocked on: {{who/what, optional}} — revisit: {{when, optional}}
|
|
46
55
|
|
|
47
56
|
## Slice inventory
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
<!-- Deliberately NOT a table: the canonical slice index (slice, pattern, status, doc link)
|
|
58
|
+
lives in README.md's "Slices" section — one source of truth, updated there. This file
|
|
59
|
+
only tracks resumable session state. -->
|
|
60
|
+
See `README.md` → **Slices** for the slice index and per-slice doc status.
|
package/README.md
CHANGED
|
@@ -28,6 +28,14 @@ em watch model.em --serve # live browser view, re-renders on every save
|
|
|
28
28
|
em validate model.em # check event-modeling rules
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Once a model is real and committed, `em` keeps working on it over time:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
em export model.em # versioned JSON, for agents and tooling
|
|
35
|
+
em diff model.em --from HEAD~1 # what this change did to the model
|
|
36
|
+
em changelog model.em # the model's git history as a business ledger
|
|
37
|
+
```
|
|
38
|
+
|
|
31
39
|
A model is a list of slices — vertical time steps, read left to right — whose elements land
|
|
32
40
|
in swimlane rows:
|
|
33
41
|
|
|
@@ -50,7 +58,9 @@ slice "View Open Orders" {
|
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
The [tutorial](docs/tutorial.md) builds a complete model from an empty file in about twenty
|
|
53
|
-
minutes.
|
|
61
|
+
minutes, and [docs/workflow.md](docs/workflow.md) picks up where it leaves off: how a model
|
|
62
|
+
gets specified, gated in CI, handed to implementation, and checked against the code that
|
|
63
|
+
implements it.
|
|
54
64
|
|
|
55
65
|
## Model with AI
|
|
56
66
|
|
|
@@ -61,8 +71,10 @@ the questions, you supply the domain, and the model renders live as it grows.
|
|
|
61
71
|
em skill install # copy the skill into .claude/skills/event-modeling/
|
|
62
72
|
```
|
|
63
73
|
|
|
64
|
-
Then run `/event-modeling` in Claude Code.
|
|
65
|
-
|
|
74
|
+
Then run `/event-modeling` in Claude Code. The same skill also runs the reverse direction:
|
|
75
|
+
`extract` derives a model from a system that already exists, and `conform` checks a model
|
|
76
|
+
against the code implementing it and reports where they've drifted. See
|
|
77
|
+
[docs/ai-workflow.md](docs/ai-workflow.md) for the phases and what a session produces, and the
|
|
66
78
|
[em-with-ai repository](https://github.com/milehimikey/em-with-ai) for a ~50-slice model
|
|
67
79
|
built this way.
|
|
68
80
|
|
|
@@ -71,10 +83,12 @@ built this way.
|
|
|
71
83
|
| Doc | What it answers |
|
|
72
84
|
|---|---|
|
|
73
85
|
| [docs/tutorial.md](docs/tutorial.md) | Learn the tool by building a model from scratch |
|
|
86
|
+
| [docs/workflow.md](docs/workflow.md) | The model lifecycle: specify, gate, hand off, track change, detect drift |
|
|
74
87
|
| [docs/patterns.md](docs/patterns.md) | The four Event Modeling patterns and their DSL shapes |
|
|
75
88
|
| [docs/dsl.md](docs/dsl.md) | Full DSL reference: keywords, `from`, `again`, fields, notes |
|
|
76
89
|
| [docs/cli.md](docs/cli.md) | Every command and flag |
|
|
77
90
|
| [docs/validation.md](docs/validation.md) | Every rule `em validate` checks, and the fixes |
|
|
91
|
+
| [docs/ci.md](docs/ci.md) | Copy-paste CI recipes: validate `.em` changes, run conformance on a schedule |
|
|
78
92
|
| [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
|
|
79
93
|
| [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
|
|
80
94
|
| [docs/dependencies.md](docs/dependencies.md) | What's bundled vs. what needs a system install |
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { GitRunner } from "./diff-inputs.js";
|
|
2
|
+
export interface CommitInfo {
|
|
3
|
+
/** Full commit hash — what `readFileAtCommit`/`git show` reads content at. */
|
|
4
|
+
hash: string;
|
|
5
|
+
shortHash: string;
|
|
6
|
+
/** YYYY-MM-DD author date (`--date=short`). */
|
|
7
|
+
date: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
/**
|
|
10
|
+
* Repo-relative path of the file AS OF this commit, taken from the same
|
|
11
|
+
* `git log --follow --name-only` walk — pre-rename commits carry the old
|
|
12
|
+
* path, the one `git show <hash>:<path>` can actually read.
|
|
13
|
+
*/
|
|
14
|
+
path: string;
|
|
15
|
+
}
|
|
16
|
+
export type CommitsResult = {
|
|
17
|
+
ok: true;
|
|
18
|
+
repoRoot: string;
|
|
19
|
+
commits: CommitInfo[];
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* List every commit that touched `file` (rename-following via `--follow`),
|
|
26
|
+
* oldest -> newest, each carrying the file's path as of that commit.
|
|
27
|
+
*
|
|
28
|
+
* `--from`/`--to` bound the walk **inclusive of both boundary commits**: the
|
|
29
|
+
* range compiles to `<from>^..<to>` (git's standard idiom for an
|
|
30
|
+
* inclusive-from range), so `--from` itself is the oldest commit in the
|
|
31
|
+
* output and `--to` the newest. `--to` defaults to `HEAD`. `--from` on a
|
|
32
|
+
* root commit (no parent to exclude) falls back to an unbounded walk up to
|
|
33
|
+
* `<to>`, which is equivalent — there's nothing before a root commit to
|
|
34
|
+
* exclude anyway.
|
|
35
|
+
*/
|
|
36
|
+
export declare function listModelCommits(file: string, opts?: {
|
|
37
|
+
from?: string;
|
|
38
|
+
to?: string;
|
|
39
|
+
}, runGit?: GitRunner): CommitsResult;
|
|
40
|
+
/** File content at a commit, or a note-ready `message` (no command prefix — it lands inside a changelog section). */
|
|
41
|
+
export type FileAtCommitResult = {
|
|
42
|
+
ok: true;
|
|
43
|
+
content: string;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
message: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Read the model's content at `commit`, using `commit.path` — the path the
|
|
50
|
+
* file had AT that commit — so reads keep working across renames, where the
|
|
51
|
+
* current path doesn't exist in older revisions.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readFileAtCommit(repoRoot: string, commit: CommitInfo, runGit?: GitRunner): FileAtCommitResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Git interaction for `em changelog`, isolated like diff-inputs.ts: listing
|
|
3
|
+
// the commits that touched a model file and reading a revision's content
|
|
4
|
+
// (`readFileAtCommit`) are the only impure operations here — everything
|
|
5
|
+
// formattable/parseable (markdown assembly, decisions parsing) lives in
|
|
6
|
+
// ../emit/changelog.ts, unit-tested without git.
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
import { realGit } from "./diff-inputs.js";
|
|
9
|
+
// A control character, not a valid character in a commit subject — safe as a
|
|
10
|
+
// field separator in `git log --format`.
|
|
11
|
+
const FS = "\x01";
|
|
12
|
+
/**
|
|
13
|
+
* List every commit that touched `file` (rename-following via `--follow`),
|
|
14
|
+
* oldest -> newest, each carrying the file's path as of that commit.
|
|
15
|
+
*
|
|
16
|
+
* `--from`/`--to` bound the walk **inclusive of both boundary commits**: the
|
|
17
|
+
* range compiles to `<from>^..<to>` (git's standard idiom for an
|
|
18
|
+
* inclusive-from range), so `--from` itself is the oldest commit in the
|
|
19
|
+
* output and `--to` the newest. `--to` defaults to `HEAD`. `--from` on a
|
|
20
|
+
* root commit (no parent to exclude) falls back to an unbounded walk up to
|
|
21
|
+
* `<to>`, which is equivalent — there's nothing before a root commit to
|
|
22
|
+
* exclude anyway.
|
|
23
|
+
*/
|
|
24
|
+
export function listModelCommits(file, opts = {}, runGit = realGit) {
|
|
25
|
+
const abs = resolve(file);
|
|
26
|
+
const toplevel = runGit(["-C", dirname(abs), "rev-parse", "--show-toplevel"]);
|
|
27
|
+
if (toplevel.status !== 0) {
|
|
28
|
+
return { ok: false, message: `em changelog: ${file} is not inside a git repository` };
|
|
29
|
+
}
|
|
30
|
+
const repoRoot = toplevel.stdout.trim();
|
|
31
|
+
const lsFiles = runGit(["-C", repoRoot, "ls-files", "--full-name", "--", abs]);
|
|
32
|
+
const relPath = lsFiles.stdout.trim().split("\n")[0];
|
|
33
|
+
if (!relPath) {
|
|
34
|
+
return { ok: false, message: `em changelog: ${file} is not tracked by git in ${repoRoot}` };
|
|
35
|
+
}
|
|
36
|
+
const upRev = opts.to ?? "HEAD";
|
|
37
|
+
if (opts.to) {
|
|
38
|
+
const verifyTo = runGit(["-C", repoRoot, "rev-parse", "--verify", "--end-of-options", opts.to]);
|
|
39
|
+
if (verifyTo.status !== 0) {
|
|
40
|
+
return { ok: false, message: `em changelog: unknown revision "${opts.to}" for --to` };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let range = upRev;
|
|
44
|
+
if (opts.from) {
|
|
45
|
+
const verifyFrom = runGit(["-C", repoRoot, "rev-parse", "--verify", "--end-of-options", opts.from]);
|
|
46
|
+
if (verifyFrom.status !== 0) {
|
|
47
|
+
return { ok: false, message: `em changelog: unknown revision "${opts.from}" for --from` };
|
|
48
|
+
}
|
|
49
|
+
const verifyParent = runGit(["-C", repoRoot, "rev-parse", "--verify", "--end-of-options", `${opts.from}^`]);
|
|
50
|
+
range = verifyParent.status === 0 ? `${opts.from}^..${upRev}` : upRev;
|
|
51
|
+
}
|
|
52
|
+
// --name-only prints, after each commit's format line, the file's path as
|
|
53
|
+
// of that commit (--follow makes that the historical, pre-rename path when
|
|
54
|
+
// applicable). core.quotepath=false keeps non-ASCII paths literal instead
|
|
55
|
+
// of octal-escaped, so the printed path is usable in `git show`.
|
|
56
|
+
const log = runGit([
|
|
57
|
+
"-C",
|
|
58
|
+
repoRoot,
|
|
59
|
+
"-c",
|
|
60
|
+
"core.quotepath=false",
|
|
61
|
+
"log",
|
|
62
|
+
"--follow",
|
|
63
|
+
"--name-only",
|
|
64
|
+
`--format=%H${FS}%h${FS}%ad${FS}%s`,
|
|
65
|
+
"--date=short",
|
|
66
|
+
range,
|
|
67
|
+
"--",
|
|
68
|
+
relPath,
|
|
69
|
+
]);
|
|
70
|
+
if (log.status !== 0) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
message: `em changelog: git log failed: ${(log.stderr || "").trim() || "unknown git error"}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Output alternates: a format line (contains FS), a blank line, then the
|
|
77
|
+
// path line for that commit. A commit with no path line (e.g. a merge,
|
|
78
|
+
// whose file list git omits by default) falls back to the current path.
|
|
79
|
+
const commits = [];
|
|
80
|
+
for (const line of log.stdout.split("\n")) {
|
|
81
|
+
if (line.length === 0)
|
|
82
|
+
continue;
|
|
83
|
+
if (line.includes(FS)) {
|
|
84
|
+
const [hash, shortHash, date, ...subjectParts] = line.split(FS);
|
|
85
|
+
commits.push({ hash, shortHash, date, subject: subjectParts.join(FS), path: relPath });
|
|
86
|
+
}
|
|
87
|
+
else if (commits.length > 0) {
|
|
88
|
+
commits[commits.length - 1].path = line;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
commits.reverse(); // git log is newest-first; changelog building wants oldest->newest
|
|
92
|
+
if (commits.length === 0) {
|
|
93
|
+
return { ok: false, message: `em changelog: no commits found for ${relPath} in the given range` };
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, repoRoot, commits };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read the model's content at `commit`, using `commit.path` — the path the
|
|
99
|
+
* file had AT that commit — so reads keep working across renames, where the
|
|
100
|
+
* current path doesn't exist in older revisions.
|
|
101
|
+
*/
|
|
102
|
+
export function readFileAtCommit(repoRoot, commit, runGit = realGit) {
|
|
103
|
+
const show = runGit(["-C", repoRoot, "show", `${commit.hash}:${commit.path}`]);
|
|
104
|
+
if (show.status !== 0) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
message: `cannot read ${commit.path} at ${commit.shortHash}: ${(show.stderr || "").trim() || "unknown git error"}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, content: show.stdout };
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=changelog-git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changelog-git.js","sourceRoot":"","sources":["../../src/cli/changelog-git.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Resolved `em diff` invocation, or a user-facing `error` for an invalid form. */
|
|
2
|
+
export type DiffPlan = {
|
|
3
|
+
error: string;
|
|
4
|
+
} | {
|
|
5
|
+
form: "files";
|
|
6
|
+
oldFile: string;
|
|
7
|
+
newFile: string;
|
|
8
|
+
} | {
|
|
9
|
+
form: "git";
|
|
10
|
+
file: string;
|
|
11
|
+
from: string;
|
|
12
|
+
to?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Validate the `em diff` argument/flag combination. Pure — no I/O. The two forms
|
|
16
|
+
* (two files, or one file across git revisions) are mutually exclusive.
|
|
17
|
+
*/
|
|
18
|
+
export declare function planDiffArgs(oldFile: string, newFile: string | undefined, opts: {
|
|
19
|
+
from?: string;
|
|
20
|
+
to?: string;
|
|
21
|
+
}): DiffPlan;
|
|
22
|
+
/** The slice of a `spawnSync` result `resolveRevision` reads; lets tests inject a fake git. */
|
|
23
|
+
export interface GitResult {
|
|
24
|
+
status: number | null;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
}
|
|
28
|
+
export type GitRunner = (args: string[]) => GitResult;
|
|
29
|
+
/** Default runner: really invokes `git` (array args — no shell, no injection). */
|
|
30
|
+
export declare const realGit: GitRunner;
|
|
31
|
+
/** File content at a git revision, or a user-facing `message` for each failure mode. */
|
|
32
|
+
export type RevisionResult = {
|
|
33
|
+
ok: true;
|
|
34
|
+
content: string;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `file`'s content at git revision `rev` via `git show <rev>:<repo-relative-path>`.
|
|
41
|
+
* The git runner is injectable so the not-a-repo / not-tracked / bad-rev branches are
|
|
42
|
+
* unit-testable without a real repository.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveRevision(file: string, rev: string, runGit?: GitRunner): RevisionResult;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Testable input handling for `em diff`: argument-form validation and
|
|
3
|
+
// git-revision resolution, factored out of cli.ts so the branching logic can be
|
|
4
|
+
// unit-tested without spawning a subprocess or a real git repo (cli.ts keeps the
|
|
5
|
+
// process.exit/console wiring; see test/diff-inputs.test.ts).
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { dirname, resolve } from "node:path";
|
|
8
|
+
/**
|
|
9
|
+
* Validate the `em diff` argument/flag combination. Pure — no I/O. The two forms
|
|
10
|
+
* (two files, or one file across git revisions) are mutually exclusive.
|
|
11
|
+
*/
|
|
12
|
+
export function planDiffArgs(oldFile, newFile, opts) {
|
|
13
|
+
if (opts.from) {
|
|
14
|
+
if (newFile) {
|
|
15
|
+
return {
|
|
16
|
+
error: "em diff: cannot combine two file arguments with --from/--to — use one form or the other",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { form: "git", file: oldFile, from: opts.from, to: opts.to };
|
|
20
|
+
}
|
|
21
|
+
if (opts.to) {
|
|
22
|
+
return { error: "em diff: --to requires --from" };
|
|
23
|
+
}
|
|
24
|
+
if (!newFile) {
|
|
25
|
+
return {
|
|
26
|
+
error: "em diff: provide two files (`em diff old.em new.em`) or one file with --from <rev>",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { form: "files", oldFile, newFile };
|
|
30
|
+
}
|
|
31
|
+
/** Default runner: really invokes `git` (array args — no shell, no injection). */
|
|
32
|
+
export const realGit = (args) => {
|
|
33
|
+
const r = spawnSync("git", args, { encoding: "utf8" });
|
|
34
|
+
return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolve `file`'s content at git revision `rev` via `git show <rev>:<repo-relative-path>`.
|
|
38
|
+
* The git runner is injectable so the not-a-repo / not-tracked / bad-rev branches are
|
|
39
|
+
* unit-testable without a real repository.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveRevision(file, rev, runGit = realGit) {
|
|
42
|
+
const abs = resolve(file);
|
|
43
|
+
const toplevel = runGit(["-C", dirname(abs), "rev-parse", "--show-toplevel"]);
|
|
44
|
+
if (toplevel.status !== 0) {
|
|
45
|
+
return { ok: false, message: `em diff: ${file} is not inside a git repository (needed for --from/--to)` };
|
|
46
|
+
}
|
|
47
|
+
const repoRoot = toplevel.stdout.trim();
|
|
48
|
+
const lsFiles = runGit(["-C", repoRoot, "ls-files", "--full-name", "--", abs]);
|
|
49
|
+
const relPath = lsFiles.stdout.trim().split("\n")[0];
|
|
50
|
+
if (!relPath) {
|
|
51
|
+
return { ok: false, message: `em diff: ${file} is not tracked by git in ${repoRoot}` };
|
|
52
|
+
}
|
|
53
|
+
const show = runGit(["-C", repoRoot, "show", `${rev}:${relPath}`]);
|
|
54
|
+
if (show.status !== 0) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
message: `em diff: cannot read ${relPath} at revision "${rev}": ${(show.stderr || "").trim() || "unknown git error"}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, content: show.stdout };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=diff-inputs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-inputs.js","sourceRoot":"","sources":["../../src/cli/diff-inputs.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,sEAAsE;AACtE,gFAAgF;AAChF,iFAAiF;AACjF,8DAA8D;AAE9D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQ7C;;;GAGG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,OAA2B,EAC3B,IAAoC;IAEpC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO;gBACL,KAAK,EAAE,yFAAyF;aACjG,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IACtE,CAAC;IACD,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;QACZ,OAAO,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC;IACpD,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,oFAAoF;SAC5F,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAC7C,CAAC;AAUD,kFAAkF;AAClF,MAAM,CAAC,MAAM,OAAO,GAAc,CAAC,IAAI,EAAE,EAAE;IACzC,MAAM,CAAC,GAAG,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;AAC9E,CAAC,CAAC;AAKF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,GAAW,EAAE,SAAoB,OAAO;IACpF,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAC9E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,IAAI,0DAA0D,EAAE,CAAC;IAC5G,CAAC;IACD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACxC,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACrD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,IAAI,6BAA6B,QAAQ,EAAE,EAAE,CAAC;IACzF,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC;IACnE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,wBAAwB,OAAO,iBAAiB,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,mBAAmB,EAAE;SACtH,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AAC5C,CAAC"}
|