@milehimikey/em 1.1.1 → 1.3.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 +74 -25
- package/.claude/skills/event-modeling/reference/em-dsl.md +45 -8
- package/.claude/skills/event-modeling/reference/extract.md +124 -0
- package/.claude/skills/event-modeling/reference/methodology.md +31 -0
- package/.claude/skills/event-modeling/templates/live.html +65 -13
- package/.claude/skills/event-modeling/templates/model-readme.md +11 -6
- package/.claude/skills/event-modeling/templates/slice.md +11 -2
- package/.claude/skills/event-modeling/templates/state.md +29 -9
- package/README.md +45 -172
- 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 +150 -7
- package/dist/cli.js.map +1 -1
- package/dist/emit/json.d.ts +28 -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/dist/render/serve.d.ts +20 -0
- package/dist/render/serve.js +202 -0
- package/dist/render/serve.js.map +1 -0
- package/package.json +3 -3
|
@@ -7,14 +7,30 @@ Keep it current at the end of every working session.
|
|
|
7
7
|
# Event Modeling Progress — {{Model Name}}
|
|
8
8
|
|
|
9
9
|
- **Model file:** `{{model-name}}.em`
|
|
10
|
-
- **Current phase:** {{discover | model | slice}}
|
|
11
|
-
- **Current step:** {{1–7, see methodology}}
|
|
10
|
+
- **Current phase:** {{extract | discover | model | slice}}
|
|
11
|
+
- **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
|
|
12
12
|
- **Last updated:** {{YYYY-MM-DD}}
|
|
13
13
|
|
|
14
14
|
## Session inputs
|
|
15
15
|
- **Scope line:** {{one-line description of what's in/out of bounds for this model}}
|
|
16
16
|
- **PRD / spec reference:** {{path, link, or "none"}}
|
|
17
17
|
- **Headless/API model:** {{yes | no}}
|
|
18
|
+
- **Source mode:** {{greenfield | extract-event-driven | extract-procedural}}
|
|
19
|
+
- **Existing system refs:** {{repo paths, event-schema/topic locations, docs — or "n/a"}}
|
|
20
|
+
|
|
21
|
+
## Participants
|
|
22
|
+
<!-- Populate at session start. Live workshop: one human proxy relays questions to the room;
|
|
23
|
+
attribute every answer/decision in the Decisions log to a named participant here. -->
|
|
24
|
+
- {{Name}} — {{role}} — {{domain area}}
|
|
25
|
+
|
|
26
|
+
## Extraction progress (existing-system models only — delete for greenfield)
|
|
27
|
+
- [ ] R1. Candidate events (extracted/synthesized, filtered, confirmed)
|
|
28
|
+
- [ ] R2. Timeline order (as-is narrative, actors/callers, first render)
|
|
29
|
+
- [ ] R3. Commands / inputs
|
|
30
|
+
- [ ] R4. Read models / outputs (validate from here)
|
|
31
|
+
- [ ] R5. Boundaries & reactions (two-slice splits)
|
|
32
|
+
- [ ] R6. Gap & TBD reconciliation
|
|
33
|
+
- [ ] R7. Convergence (render + validate clean, user confirmed as-is)
|
|
18
34
|
|
|
19
35
|
## Steps completed
|
|
20
36
|
- [ ] 1. Brainstorm events
|
|
@@ -26,14 +42,18 @@ Keep it current at the end of every working session.
|
|
|
26
42
|
- [ ] 7. Evaluate completeness (`em validate` clean)
|
|
27
43
|
|
|
28
44
|
## Decisions log
|
|
29
|
-
<!-- Resolved choices, with the reasoning, so they aren't re-litigated.
|
|
30
|
-
|
|
45
|
+
<!-- Resolved choices, with the reasoning, so they aren't re-litigated. In a live workshop,
|
|
46
|
+
attribute each entry to the participant who made the call (see Participants above). -->
|
|
47
|
+
- {{YYYY-MM-DD}}: {{decision}} — {{why}} — by {{participant, if a live workshop}}
|
|
31
48
|
|
|
32
49
|
## Open questions / parking lot
|
|
33
|
-
<!-- Unresolved items to bring back to the user. Never guess these.
|
|
34
|
-
|
|
50
|
+
<!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and
|
|
51
|
+
flat — add only what's known: source (ticket/conversation that spawned it), blocked on
|
|
52
|
+
(who/what), revisit (when). -->
|
|
53
|
+
- [ ] {{question}} — source: {{ticket/conversation link, optional}} — blocked on: {{who/what, optional}} — revisit: {{when, optional}}
|
|
35
54
|
|
|
36
55
|
## Slice inventory
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
<!-- Deliberately NOT a table: the canonical slice index (slice, pattern, status, doc link)
|
|
57
|
+
lives in README.md's "Slices" section — one source of truth, updated there. This file
|
|
58
|
+
only tracks resumable session state. -->
|
|
59
|
+
See `README.md` → **Slices** for the slice index and per-slice doc status.
|
package/README.md
CHANGED
|
@@ -1,187 +1,86 @@
|
|
|
1
|
-
# em —
|
|
1
|
+
# em — event modeling in plain text
|
|
2
2
|
|
|
3
3
|
`em` is a command-line tool for [Event Modeling](https://eventmodeling.org/). You write a
|
|
4
|
-
model in a small,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
is for a person to write by hand. Layout is a **strict grid**: swimlane rows and
|
|
8
|
-
time-ordered slice columns stay perfectly aligned with no manual offsets.
|
|
4
|
+
model in a small, slice-first DSL and `em` renders it as a clean, deterministic diagram.
|
|
5
|
+
Because the source is plain text — diff-able, reviewable, unambiguous — it's as easy for an
|
|
6
|
+
AI to write and edit as it is for a person, and `em validate` keeps either one honest.
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
examples/order-fulfillment.em -> em render -> order-fulfillment.svg
|
|
12
|
-
```
|
|
8
|
+

|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
rasterized in-process, so there's nothing to `apt`/`brew` install for SVG or PNG output.
|
|
10
|
+
The source for that diagram is [about 70 lines of text](examples/order-fulfillment.em).
|
|
16
11
|
|
|
17
12
|
## Install
|
|
18
13
|
|
|
19
14
|
```bash
|
|
20
|
-
npm install -g @milehimikey/em
|
|
15
|
+
npm install -g @milehimikey/em
|
|
21
16
|
```
|
|
22
17
|
|
|
23
|
-
Requires
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Run without installing:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npx @milehimikey/em init model.em
|
|
30
|
-
npx @milehimikey/em render model.em
|
|
31
|
-
```
|
|
18
|
+
Requires Node ≥ 18. SVG and PNG rendering are fully self-contained (Graphviz runs as
|
|
19
|
+
bundled WebAssembly); nothing else to install. PDF is optional — see
|
|
20
|
+
[docs/dependencies.md](docs/dependencies.md).
|
|
32
21
|
|
|
33
22
|
## Quickstart
|
|
34
23
|
|
|
35
24
|
```bash
|
|
36
25
|
em init model.em # scaffold a starter model
|
|
37
26
|
em render model.em # -> model.svg (open it in a browser)
|
|
38
|
-
em watch model.em
|
|
27
|
+
em watch model.em --serve # live browser view, re-renders on every save
|
|
39
28
|
em validate model.em # check event-modeling rules
|
|
40
29
|
```
|
|
41
30
|
|
|
42
|
-
A
|
|
43
|
-
|
|
31
|
+
A model is a list of slices — vertical time steps, read left to right — whose elements land
|
|
32
|
+
in swimlane rows:
|
|
44
33
|
|
|
45
|
-
```
|
|
34
|
+
```em
|
|
46
35
|
model "Order Fulfillment"
|
|
47
36
|
|
|
48
|
-
persona Customer
|
|
49
|
-
context Order
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
customerId
|
|
56
|
-
total: Money # optional `: Type` annotation
|
|
57
|
-
}
|
|
58
|
-
event Order Placed @Order note "notes/order-placed.md" { # note "…" links docs
|
|
59
|
-
orderId
|
|
60
|
-
total: Money
|
|
61
|
-
}
|
|
37
|
+
persona Customer
|
|
38
|
+
context Order
|
|
39
|
+
|
|
40
|
+
slice "Browse Catalog" {
|
|
41
|
+
ui Product Catalog @Customer
|
|
42
|
+
command Place Order
|
|
43
|
+
event Order Placed @Order
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
slice "View Open Orders" {
|
|
65
|
-
view Open Orders from "Order Placed"
|
|
47
|
+
view Open Orders from "Order Placed"
|
|
66
48
|
ui Order List @Customer
|
|
67
49
|
}
|
|
68
50
|
```
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Event Modeling is built from four patterns, all expressible as slices:
|
|
73
|
-
|
|
74
|
-
| Pattern | Flow | DSL elements |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| **Input** (state change) | UI → command → event | `ui`, `command`, `event` |
|
|
77
|
-
| **Output** (state view) | event → read model → UI | `event`, `view from "…"`, `ui` |
|
|
78
|
-
| **Automation** | read model (todo) → processor → command → event | `view from "…"` + `processor`, then `command` + `event` |
|
|
79
|
-
| **Translation** | external read model → translation → command → event | `view from "…"` + `translation`, then `command` + `event` |
|
|
52
|
+
The [tutorial](docs/tutorial.md) builds a complete model from an empty file in about twenty
|
|
53
|
+
minutes.
|
|
80
54
|
|
|
81
|
-
|
|
82
|
-
model (a "todo list") or a timer and *issue* a command when done. The automation slice
|
|
83
|
-
contains **only** the read model it reads and the processor/translation; the command it
|
|
84
|
-
triggers (and that command's event) is the **next** slice.
|
|
55
|
+
## Model with AI
|
|
85
56
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
### Element keywords → swimlane bands (top → bottom)
|
|
89
|
-
|
|
90
|
-
0. *(header)* — each slice name is rendered as a title cell in the top row
|
|
91
|
-
1. `automation` / `processor` / `saga` / `translation` — **Automation** band (only shown if used)
|
|
92
|
-
2. `ui` — **persona** rows (one per `persona`)
|
|
93
|
-
3. `command` + `view` — the **API** row (commands and read models share one lane)
|
|
94
|
-
4. `event` — **context/concept** rows (one per `context`)
|
|
95
|
-
|
|
96
|
-
Commands and read models share the **API** row, so a slice holds *either* a command *or* a
|
|
97
|
-
read model. The empty API lane is dropped when a model has neither; pass `--keep-empty-lanes`
|
|
98
|
-
to keep it.
|
|
99
|
-
|
|
100
|
-
`@Persona` / `@Context` choose the row within a band (undeclared tags create a new row).
|
|
101
|
-
`view <Name> again [from "Event", …]` declares a LATER INSTANCE of an already-declared read
|
|
102
|
-
model — the Event Modeling device for keeping the timeline forward-only as a view evolves.
|
|
103
|
-
Instances are one logical view: the first declaration owns the `note` doc; each instance lists
|
|
104
|
-
the events that reach it there; instances are linked left-to-right with a continuity arrow, and
|
|
105
|
-
reactions (`processor … from "View"`) read the nearest instance at-or-before their slice.
|
|
106
|
-
|
|
107
|
-
`view … from "Event"[, "Event2"]` declares which events feed a read model and draws the
|
|
108
|
-
data-flow arrow. Arrows within a slice are inferred from the pattern; use `arrow A -> B` for
|
|
109
|
-
anything extra (e.g. a read model feeding a different screen).
|
|
110
|
-
|
|
111
|
-
### Fields
|
|
112
|
-
|
|
113
|
-
Any element can declare **data fields** in a `{ … }` block — the data it accepts (command),
|
|
114
|
-
records (event), projects (read model), or shows (UI). Each field is `name` with an optional
|
|
115
|
-
`: Type`; write one per line, or inline and comma-separated:
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
command Place Order { # one per line
|
|
119
|
-
customerId
|
|
120
|
-
items: List<LineItem>
|
|
121
|
-
total: Money
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
event Payment Requested @Payment { orderId, amount: Money } # inline
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Fields render **in the box**, UML-style: the name, a divider rule, then the fields. The box
|
|
128
|
-
grows to fit them (width stays fixed, so columns stay aligned), and arrows still anchor to
|
|
129
|
-
the box edges so the lines stay stable. A field block coexists with `note`/`from` clauses on
|
|
130
|
-
the same element. Fields are the foundation of the later event-modeling phases (the slicing /
|
|
131
|
-
information-completeness process); field-level validation across slices is planned.
|
|
132
|
-
|
|
133
|
-
### Notes
|
|
134
|
-
|
|
135
|
-
Any element can carry `note "path.md"` (valid on every keyword). The prose lives in the
|
|
136
|
-
markdown file — keeping the diagram uncluttered — and the box gets a small **numbered
|
|
137
|
-
folded-corner marker** in its top-right corner. A **legend** is appended below the diagram
|
|
138
|
-
mapping each number to its element and note file, so even a static export tells you which
|
|
139
|
-
note belongs to which box.
|
|
140
|
-
|
|
141
|
-
In **SVG** output the markers and legend rows are links; clicking one opens the markdown
|
|
142
|
-
file. Links resolve relative to the **output SVG's location**, so they keep working whether
|
|
143
|
-
the SVG is rendered beside the model or into another folder (as long as the notes travel
|
|
144
|
-
with it). Open the SVG in a **web browser** to use the links — image viewers like macOS
|
|
145
|
-
Preview/Quick Look show the markers but ignore SVG hyperlinks. Raster output (PNG/PDF) can't
|
|
146
|
-
carry links — that's what the footnote numbers + legend are for.
|
|
147
|
-
|
|
148
|
-
## Commands
|
|
57
|
+
`em` ships a Claude Code skill that runs a facilitated Event Modeling session: the AI asks
|
|
58
|
+
the questions, you supply the domain, and the model renders live as it grows.
|
|
149
59
|
|
|
150
60
|
```bash
|
|
151
|
-
em
|
|
152
|
-
em render <file> [-o out.svg] # render (format from -o extension or -T)
|
|
153
|
-
em render <file> -o out.png # PNG (in-process, no system deps)
|
|
154
|
-
em render <file> --emit-dot # print the generated DOT instead of rendering
|
|
155
|
-
em render <file> --keep-empty-lanes # don't collapse the API lane when empty
|
|
156
|
-
em watch <file> [-o out.svg] # re-render on every save
|
|
157
|
-
em validate <file> # check event-modeling rules (non-zero exit on errors)
|
|
61
|
+
em skill install # copy the skill into .claude/skills/event-modeling/
|
|
158
62
|
```
|
|
159
63
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
|
|
164
|
-
- **time flows left to right** (the Two Laws): an event feeding an EARLIER view instance,
|
|
165
|
-
a reaction reading a view that only exists later, or an explicit backward `arrow` → **error**
|
|
166
|
-
(fix: add `view X again` at the point on the timeline where the event lands)
|
|
167
|
-
- `again` on a view with no earlier declaration → **error**; `again` on non-views → parse error
|
|
168
|
-
- multi-event slices always render as a FAN from the command (never event→event); fanned
|
|
169
|
-
arrows route around the column gutter so they cannot read as a chain
|
|
170
|
-
- an automation/translation whose `from "<read model>"` doesn't exist → **error**
|
|
171
|
-
- an `arrow` endpoint that matches no element → **error**
|
|
172
|
-
- an automation/translation slice that also holds the command it triggers → **warning**
|
|
173
|
-
- a command that records no event → **warning**
|
|
174
|
-
- a read model with no source event → **warning**
|
|
175
|
-
- a name defined more than once and referenced → **warning** (resolves to first)
|
|
64
|
+
Then run `/event-modeling` in Claude Code. See [docs/ai-workflow.md](docs/ai-workflow.md)
|
|
65
|
+
for the phases and what a session produces, and the
|
|
66
|
+
[em-with-ai repository](https://github.com/milehimikey/em-with-ai) for a ~50-slice model
|
|
67
|
+
built this way.
|
|
176
68
|
|
|
177
69
|
## Documentation
|
|
178
70
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
71
|
+
| Doc | What it answers |
|
|
72
|
+
|---|---|
|
|
73
|
+
| [docs/tutorial.md](docs/tutorial.md) | Learn the tool by building a model from scratch |
|
|
74
|
+
| [docs/patterns.md](docs/patterns.md) | The four Event Modeling patterns and their DSL shapes |
|
|
75
|
+
| [docs/dsl.md](docs/dsl.md) | Full DSL reference: keywords, `from`, `again`, fields, notes |
|
|
76
|
+
| [docs/cli.md](docs/cli.md) | Every command and flag |
|
|
77
|
+
| [docs/validation.md](docs/validation.md) | Every rule `em validate` checks, and the fixes |
|
|
78
|
+
| [docs/ci.md](docs/ci.md) | Copy-paste GitHub Actions recipe: validate `.em` changes in CI |
|
|
79
|
+
| [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
|
|
80
|
+
| [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
|
|
81
|
+
| [docs/dependencies.md](docs/dependencies.md) | What's bundled vs. what needs a system install |
|
|
82
|
+
| [docs/architecture.md](docs/architecture.md) | How rendering works; why Graphviz, not PlantUML |
|
|
83
|
+
| [docs/roadmap.md](docs/roadmap.md) | What's planned |
|
|
185
84
|
|
|
186
85
|
## Development
|
|
187
86
|
|
|
@@ -192,32 +91,6 @@ npm test # vitest
|
|
|
192
91
|
npx tsx src/cli.ts <command> ... # run straight from source
|
|
193
92
|
```
|
|
194
93
|
|
|
195
|
-
## AI Assistant (Claude Code)
|
|
196
|
-
|
|
197
|
-
`em` ships a **Claude Code skill** that guides you and your team through Event Modeling using the 7-step process and 4 design patterns, producing implementation-ready slice specifications. The AI drives the model — asking focused questions, never guessing domain facts — while `em validate` keeps the diagram honest.
|
|
198
|
-
|
|
199
|
-
**Install the skill into your project:**
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
em skill install # copies the skill into .claude/skills/event-modeling/
|
|
203
|
-
em skill install --force # overwrite an existing installation
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
Then, in Claude Code, run:
|
|
207
|
-
|
|
208
|
-
```
|
|
209
|
-
/event-modeling # start (or resume) a guided session
|
|
210
|
-
/event-modeling discover # steps 1–4: brainstorm events, timeline, commands, read models
|
|
211
|
-
/event-modeling model # steps 5–7: swimlanes, patterns, completeness check
|
|
212
|
-
/event-modeling slice # deep implementation specs, one per slice
|
|
213
|
-
/event-modeling watch # open a live browser view for team modeling sessions
|
|
214
|
-
/event-modeling validate # run em validate and resolve all diagnostics
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
Sessions are **resumable** — the skill checkpoints progress in `.event-modeling.md` so you can continue across conversations.
|
|
218
|
-
|
|
219
|
-
For a complete worked example (a headless CPQ system with ~50 slices), see the [em-with-ai repository](https://github.com/milehimikey/em-with-ai).
|
|
220
|
-
|
|
221
94
|
## License
|
|
222
95
|
|
|
223
96
|
[MIT](LICENSE) © milehimikey
|
|
@@ -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":"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"}
|