@reventlessdev/reventless-spec 3.0.0-alpha.169 → 3.0.0-alpha.171
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/CHANGELOG.md +32 -0
- package/README.md +17 -3
- package/package.json +5 -4
- package/run-rename-to-decision.mjs +3 -0
- package/src/AnsiStyle.res +3 -3
- package/src/CliArgs.res +1 -1
- package/src/LogPrefix.res +1 -1
- package/src/codemod/RenameToDecision.res +207 -0
- package/src/codemod/RenameToDecision.res.mjs +334 -0
- package/src/components/Aggregate.res +1 -1
- package/src/components/CapabilityManifest.res +1 -1
- package/src/components/ComponentKind.res +2 -2
- package/src/components/ComponentKind.res.mjs +1 -1
- package/src/components/DcbScopeInference.res +2 -2
- package/src/components/DcbTag.res +5 -5
- package/src/components/OutboundTranslationSlice.res +2 -2
- package/src/components/Plugin.res +14 -14
- package/src/components/Plugin.res.mjs +8 -8
- package/src/components/Sensitive.res +1 -1
- package/src/components/Snapshot.res +5 -5
- package/src/components/StateAnnotations.res +1 -1
- package/src/components/StateChangeSlice.res +6 -5
- package/src/components/StateViewSlice.res +3 -3
- package/src/components/TaggedUnion.res +1 -1
- package/src/generator/Codegen.res +5 -5
- package/src/generator/Codegen.res.mjs +1 -1
- package/src/generator/Discovery.res +3 -3
- package/src/generator/Pairing.res +47 -18
- package/src/generator/Pairing.res.mjs +41 -10
- package/src/lifecycle/CheckLifecycleModel.res +8 -7
- package/src/lifecycle/CheckLifecycleModel.res.mjs +2 -2
- package/src/semantic/Capabilities.res +2 -2
- package/src/semantic/CapabilityNeed.res +3 -3
- package/src/semantic/Color.res +8 -8
- package/src/semantic/IdentityProvider.res +1 -1
- package/src/semantic/Messaging.res +2 -2
- package/src/types/{Behavior.res → Decision.res} +10 -6
- package/src/types/Message.res +1 -1
- package/src/types/OwnerScope.res +5 -5
- package/src/types/RoleCoverage.res +6 -6
- package/src/types/RoleCoverage.res.mjs +6 -6
- package/src/types/Trait.res +1 -1
- /package/src/types/{Behavior.res.mjs → Decision.res.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,38 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.171 (2026-10-21)
|
|
7
|
+
|
|
8
|
+
* feat!: a state view's row key is module Id ([08c9726](https://github.com/ReventlessDev/reventless-core/commit/08c972659a3d689a39fd5e2f2026597155e92210))
|
|
9
|
+
* feat!: a plugin structure's component definitions are read-side and write-side defs ([ea2ab69](https://github.com/ReventlessDev/reventless-core/commit/ea2ab69e928ba16d7e640e4c66ab0e9ba034fe5c))
|
|
10
|
+
|
|
11
|
+
### BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* a state view spec declares its row key as module Id. A
|
|
14
|
+
hand-written module Key in a spec under StateView/ or StateViewStream/
|
|
15
|
+
fails the build with "A state view's row key is `module Id`, not
|
|
16
|
+
`module Key`".
|
|
17
|
+
* Plugin.queryableDef, Plugin.writableDef, their schemas,
|
|
18
|
+
isPublicQueryable and encodeQueryableDef no longer exist; a reader names
|
|
19
|
+
readSideDef, writeSideDef, isPublicReadSide and encodeReadSideDef. There
|
|
20
|
+
are no aliases, so the compiler lists every reader.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 3.0.0-alpha.170 (2026-10-21)
|
|
25
|
+
|
|
26
|
+
* feat!: the write-side logic module is called the decision, and a codemod moves an app over ([cb573ff](https://github.com/ReventlessDev/reventless-core/commit/cb573ff5b928c7d7f3e75988dfaa65066f8c95eb))
|
|
27
|
+
|
|
28
|
+
### BREAKING CHANGES
|
|
29
|
+
|
|
30
|
+
* <Spec>_Behavior.res, @@reventless.behavior,
|
|
31
|
+
Reventless.Behavior.T, StateChangeSlice.Behavior, Behavior_GWT and
|
|
32
|
+
thenRefused no longer exist. Run `npx reventless-rename-to-decision` at an
|
|
33
|
+
app's root, then rebuild; comments are not rewritten. The PPX must be the
|
|
34
|
+
matching release.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
6
38
|
# 3.0.0-alpha.169 (2026-10-19)
|
|
7
39
|
|
|
8
40
|
### Features
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ The **specification layer** of [Reventless](https://docs.reventless.dev) — a
|
|
|
11
11
|
spec-driven, event-sourced CQRS framework written in [ReScript](https://rescript-lang.org).
|
|
12
12
|
This package holds the type specifications and module-type interfaces that the rest of the
|
|
13
13
|
framework is built on: the contracts application domain code implements (commands, events,
|
|
14
|
-
|
|
14
|
+
decisions, projections) and the component-kind vocabulary the whole framework shares. It
|
|
15
15
|
has **no** runtime, cloud, or storage dependency, so domain code can declare its shape
|
|
16
16
|
against `reventless-spec` without pulling in an implementation. Modules are exposed under
|
|
17
17
|
the `Reventless` namespace.
|
|
@@ -24,8 +24,9 @@ ReScript modules, consumed by adding the package to your `rescript.json` `depend
|
|
|
24
24
|
|
|
25
25
|
- **`Message`** — the envelope metadata carried by every event and command (`meta`,
|
|
26
26
|
`event'<>`, `command'<>`), used for audit, correlation, tracing, and routing.
|
|
27
|
-
- **`
|
|
28
|
-
logic: `initialState`, `evolve(state, event)`, and
|
|
27
|
+
- **`Decision`** — the module type application code implements to define an aggregate's
|
|
28
|
+
write-side logic (the decider pattern): `initialState`, `evolve(state, event)`, and
|
|
29
|
+
`decide(state, command)`. Its file is `<Entity>_Decision.res`.
|
|
29
30
|
- **`Projection`** — `Source` / `Target` module types describing a read model projected
|
|
30
31
|
from an aggregate's events.
|
|
31
32
|
- **`Handler`** — function signatures for handling commands, events, and errors.
|
|
@@ -62,6 +63,19 @@ generate-plugin src/ # generate src/Plugin.res from the src/ folder
|
|
|
62
63
|
Wire it into your build via a `prebuild` script so it runs before `rescript build`. The
|
|
63
64
|
generated `Plugin.res` is committed to git, so CI compiles it directly.
|
|
64
65
|
|
|
66
|
+
**`reventless-rename-to-decision` CLI**
|
|
67
|
+
|
|
68
|
+
Moves an application from the logic module's former name, *behavior*, to *decision*:
|
|
69
|
+
it renames every `*_Behavior.res` to `*_Decision.res` (with `git mv` inside a work tree)
|
|
70
|
+
and rewrites `@@reventless.behavior`, `Behavior_GWT`, `Reventless.Behavior`,
|
|
71
|
+
`thenRefused` and `<Spec>_Behavior` references in `.res` files. The framework refuses the
|
|
72
|
+
old spellings, so a bump is this command plus a build.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
reventless-rename-to-decision --dry-run # print what would change under .
|
|
76
|
+
reventless-rename-to-decision # rename; skips node_modules and lib
|
|
77
|
+
```
|
|
78
|
+
|
|
65
79
|
## Where it fits
|
|
66
80
|
|
|
67
81
|
`reventless-spec` is the foundation package in the Reventless framework:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.171",
|
|
4
4
|
"description": "Specifications for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"certify-trait": "./run-certify-trait.mjs",
|
|
11
11
|
"trait-manifest": "./run-trait-manifest.mjs",
|
|
12
12
|
"check-lifecycle": "./run-check-lifecycle.mjs",
|
|
13
|
-
"prepare-accounts": "./run-prepare-accounts.mjs"
|
|
13
|
+
"prepare-accounts": "./run-prepare-accounts.mjs",
|
|
14
|
+
"reventless-rename-to-decision": "./run-rename-to-decision.mjs"
|
|
14
15
|
},
|
|
15
16
|
"jest": {
|
|
16
17
|
"testMatch": [
|
|
@@ -26,11 +27,11 @@
|
|
|
26
27
|
"sury": "11.0.0",
|
|
27
28
|
"sury-ppx": "11.0.0",
|
|
28
29
|
"yaml": "^2.8.3",
|
|
29
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
30
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.23"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"rescript": "12.3.1",
|
|
33
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.
|
|
34
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.13"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"rescript": "^12.3.1"
|
package/src/AnsiStyle.res
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
This is the lowest layer in the logging stack — LogPrefix and Logger
|
|
7
7
|
depend on it, so the sink decision lives here as the single source of
|
|
8
|
-
truth. `useAnsi()` ⇒ TTY text mode (
|
|
8
|
+
truth. `useAnsi()` ⇒ TTY text mode (color/bold); `isJsonSink()` ⇒
|
|
9
9
|
structured JSON for any non-TTY collector (CloudWatch, Datadog, …). */
|
|
10
10
|
@val external _isTty: option<bool> = "process.stdout.isTTY"
|
|
11
11
|
@val external _logFormat: option<string> = "process.env.REVENTLESS_LOG_FORMAT"
|
|
@@ -38,7 +38,7 @@ let _format = ref(_resolveFormat())
|
|
|
38
38
|
/** Declares what this runtime is, for the pipe case the TTY probe gets wrong.
|
|
39
39
|
Call it at module init, before anything logs. An explicit
|
|
40
40
|
REVENTLESS_LOG_FORMAT still wins, so a developer can always ask for the other
|
|
41
|
-
one. A runtime that says nothing keeps the TTY
|
|
41
|
+
one. A runtime that says nothing keeps the TTY behavior. */
|
|
42
42
|
let setDefaultFormat = (format: [#text | #json]) => {
|
|
43
43
|
_default :=
|
|
44
44
|
Some(
|
|
@@ -56,7 +56,7 @@ let reload = () => _format := _resolveFormat()
|
|
|
56
56
|
/** True when output should be structured JSON (non-TTY sink). */
|
|
57
57
|
let isJsonSink = () => _format.contents == "json"
|
|
58
58
|
|
|
59
|
-
/** True when output should carry ANSI
|
|
59
|
+
/** True when output should carry ANSI color/bold (TTY text mode). */
|
|
60
60
|
let useAnsi = () => _format.contents == "text"
|
|
61
61
|
|
|
62
62
|
/** Wrap a string in ANSI bold escape codes — no-op in a JSON sink. */
|
package/src/CliArgs.res
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/***
|
|
2
2
|
How every command-line tool in this repository reads `process.argv`: through
|
|
3
3
|
Node's `parseArgs`, with mistakes worded the way these tools word them, and one
|
|
4
|
-
|
|
4
|
+
behavior on `--help`, on a usage mistake and on a bare invocation (see [run]).
|
|
5
5
|
Non-strict with tokens rather than strict mode, because strict mode names the
|
|
6
6
|
offending argument only inside English prose.
|
|
7
7
|
*/
|
package/src/LogPrefix.res
CHANGED
|
@@ -164,7 +164,7 @@ let fmtPlainPrefix = (~comp=?, ()) => {
|
|
|
164
164
|
// no trailing space between `[Plugin]` and `[Comp]`.
|
|
165
165
|
//
|
|
166
166
|
// Sink-aware: in a non-TTY/JSON sink it falls back to `fmtPlainPrefix` so no
|
|
167
|
-
// ANSI
|
|
167
|
+
// ANSI color or bold leaks into structured logs. This makes every caller that
|
|
168
168
|
// pre-formats a prefix before reaching `Logger.emit` (EffectLogger.withComp,
|
|
169
169
|
// the infra-layer `compLog`s) correct without per-call-site changes.
|
|
170
170
|
let fmtComp = (~comp=?, ()) =>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Move an application from the write-side logic module's former name, *behavior*,
|
|
2
|
+
// to *decision*: `X_Behavior.res` becomes `X_Decision.res`, and the spellings that
|
|
3
|
+
// name it in `.res` sources follow.
|
|
4
|
+
//
|
|
5
|
+
// The framework accepts none of the former spellings, so a consumer's bump is this
|
|
6
|
+
// command plus a build. Everything is planned before anything changes: a target
|
|
7
|
+
// that already exists stops the run with nothing touched.
|
|
8
|
+
|
|
9
|
+
let usage = `Usage: reventless-rename-to-decision [--dry-run] [<dir>]
|
|
10
|
+
|
|
11
|
+
<dir> where to start; the default is the current directory
|
|
12
|
+
--dry-run print what would change, change nothing
|
|
13
|
+
|
|
14
|
+
Renames every *_Behavior.res to *_Decision.res (git mv inside a git work tree,
|
|
15
|
+
with its compiled output beside it), and in every .res and .resi file:
|
|
16
|
+
@@reventless.behavior -> @@reventless.decision, Behavior_GWT -> Decision_GWT,
|
|
17
|
+
Reventless.Behavior -> Reventless.Decision, thenRefused -> thenForbidden, and
|
|
18
|
+
<Spec>_Behavior -> <Spec>_Decision. Skips node_modules and lib.`
|
|
19
|
+
|
|
20
|
+
type args = {dir: string, dryRun: bool}
|
|
21
|
+
|
|
22
|
+
let parseArgs = (argv: array<string>): result<args, string> =>
|
|
23
|
+
CliArgs.parse(~bools=["dry-run"], argv)
|
|
24
|
+
->Result.flatMap(a => a->CliArgs.atMost(1))
|
|
25
|
+
->Result.map(a => {
|
|
26
|
+
dir: a->CliArgs.positionals->Array.get(0)->Option.getOr("."),
|
|
27
|
+
dryRun: a->CliArgs.bool("dry-run"),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
let cli: CliArgs.cli<args> = {bin: "reventless-rename-to-decision", usage, parse: parseArgs}
|
|
31
|
+
|
|
32
|
+
// ── The text ─────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
// In order: the qualified forms before the bare module names they contain.
|
|
35
|
+
let replacements: array<(RegExp.t, string)> = [
|
|
36
|
+
(/@@reventless\.behavior\b/g, "@@reventless.decision"),
|
|
37
|
+
(/\bBehavior_GWT\.BehaviorSpec\b/g, "Decision_GWT.DecisionSpec"),
|
|
38
|
+
(/\bBehavior_GWT\.Behavior\b/g, "Decision_GWT.Decision"),
|
|
39
|
+
(/\bBehavior_GWT\b/g, "Decision_GWT"),
|
|
40
|
+
(/\bReventless\.Behavior\b/g, "Reventless.Decision"),
|
|
41
|
+
(/\bStateChangeSlice\.Behavior\b/g, "StateChangeSlice.Decision"),
|
|
42
|
+
(/\bMapping_GWT\.FromBehavior\b/g, "Mapping_GWT.FromDecision"),
|
|
43
|
+
(/\bthenRefused\b/g, "thenForbidden"),
|
|
44
|
+
(/\b([A-Z][A-Za-z0-9]*)_Behavior\b/g, "$1_Decision"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
// A trait's conformance binding names the decision module `Behavior`; where a
|
|
48
|
+
// file binds it, its uses in that file follow.
|
|
49
|
+
let localBinding = /^(\s*)module Behavior = /m
|
|
50
|
+
|
|
51
|
+
let rewrite = (source: string): string => {
|
|
52
|
+
let text =
|
|
53
|
+
replacements->Array.reduce(source, (text, (pattern, replacement)) =>
|
|
54
|
+
text->String.replaceRegExp(pattern, replacement)
|
|
55
|
+
)
|
|
56
|
+
if localBinding->RegExp.test(text) {
|
|
57
|
+
text
|
|
58
|
+
->String.replaceRegExp(/^(\s*)module Behavior = /gm, "$1module Decision = ")
|
|
59
|
+
->String.replaceRegExp(/\bBehavior\./g, "Decision.")
|
|
60
|
+
} else {
|
|
61
|
+
text
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── The tree ─────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
type move = {from: string, to_: string}
|
|
68
|
+
type edit = {path: string, contents: string}
|
|
69
|
+
type plan = {moves: array<move>, edits: array<edit>}
|
|
70
|
+
|
|
71
|
+
let skippedDirs = ["node_modules", "lib", ".git"]
|
|
72
|
+
|
|
73
|
+
let isSource = (name: string) => name->String.endsWith(".res") || name->String.endsWith(".resi")
|
|
74
|
+
|
|
75
|
+
// A moved source takes its compiled output and interface along.
|
|
76
|
+
let companions = [".res", ".resi", ".res.mjs", ".res.js", ".res.cjs"]
|
|
77
|
+
|
|
78
|
+
let rec walk = (dir: string, files: array<string>) =>
|
|
79
|
+
NodeFs.readdirSync(dir, {withFileTypes: true})->Array.forEach(entry => {
|
|
80
|
+
let name = entry->NodeFs.direntName
|
|
81
|
+
let path = NodePath.join([dir, name])
|
|
82
|
+
if entry->NodeFs.isDirectory {
|
|
83
|
+
if !(skippedDirs->Array.includes(name)) {
|
|
84
|
+
walk(path, files)
|
|
85
|
+
}
|
|
86
|
+
} else if entry->NodeFs.isFile {
|
|
87
|
+
files->Array.push(path)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
let byPath = (a: string, b: string) => String.compare(a, b)
|
|
92
|
+
|
|
93
|
+
/** What the run would do under `dir`, or why it cannot. */
|
|
94
|
+
let plan = (~dir: string): result<plan, string> => {
|
|
95
|
+
let files = []
|
|
96
|
+
walk(dir, files)
|
|
97
|
+
files->Array.sort(byPath)
|
|
98
|
+
let present = Set.fromArray(files)
|
|
99
|
+
|
|
100
|
+
let moves =
|
|
101
|
+
files
|
|
102
|
+
->Array.filter(path => path->String.endsWith("_Behavior.res"))
|
|
103
|
+
->Array.flatMap(source => {
|
|
104
|
+
let stem = source->String.slice(~start=0, ~end=String.length(source) - 13)
|
|
105
|
+
companions->Array.filterMap(suffix => {
|
|
106
|
+
let from = stem ++ "_Behavior" ++ suffix
|
|
107
|
+
present->Set.has(from) ? Some({from, to_: stem ++ "_Decision" ++ suffix}) : None
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
let edits = files->Array.filterMap(path =>
|
|
112
|
+
if path->NodePath.basename->isSource {
|
|
113
|
+
let contents = NodeFs.readFileSync(path)
|
|
114
|
+
let rewritten = rewrite(contents)
|
|
115
|
+
rewritten == contents ? None : Some({path, contents: rewritten})
|
|
116
|
+
} else {
|
|
117
|
+
None
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
switch moves->Array.find(({to_}) => present->Set.has(to_)) {
|
|
122
|
+
| Some({from, to_}) =>
|
|
123
|
+
Error(`${to_} already exists, so ${from} cannot take its name. Nothing was changed.`)
|
|
124
|
+
| None => Ok({moves, edits})
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Applying it ──────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
let git = (~cwd: string, args: array<string>) =>
|
|
131
|
+
NodeChildProcess.spawnSync(
|
|
132
|
+
"git",
|
|
133
|
+
args,
|
|
134
|
+
{cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"]},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
let succeeded = (result: NodeChildProcess.spawnSyncResult) =>
|
|
138
|
+
result.status->Nullable.toOption == Some(0)
|
|
139
|
+
|
|
140
|
+
let isTracked = (path: string) =>
|
|
141
|
+
git(
|
|
142
|
+
~cwd=NodePath.dirname(path),
|
|
143
|
+
["ls-files", "--error-unmatch", "--", NodePath.basename(path)],
|
|
144
|
+
)->succeeded
|
|
145
|
+
|
|
146
|
+
let moveFile = ({from, to_}: move): result<unit, string> =>
|
|
147
|
+
if isTracked(from) {
|
|
148
|
+
let result = git(
|
|
149
|
+
~cwd=NodePath.dirname(from),
|
|
150
|
+
["mv", "--", NodePath.basename(from), NodePath.basename(to_)],
|
|
151
|
+
)
|
|
152
|
+
result->succeeded
|
|
153
|
+
? Ok()
|
|
154
|
+
: Error(`git mv ${from}: ${result.stderr->Nullable.toOption->Option.getOr("")->String.trim}`)
|
|
155
|
+
} else {
|
|
156
|
+
try {
|
|
157
|
+
NodeFs.renameSync(from, to_)
|
|
158
|
+
Ok()
|
|
159
|
+
} catch {
|
|
160
|
+
| JsExn(e) => Error(`rename ${from}: ${e->JsExn.message->Option.getOr("failed")}`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Edits are written at the old paths, then the files move, so a moved file
|
|
165
|
+
keeps both its history and its new contents. */
|
|
166
|
+
let apply = ({moves, edits}: plan): result<unit, string> => {
|
|
167
|
+
edits->Array.forEach(({path, contents}) => NodeFs.writeFileSync(path, contents))
|
|
168
|
+
moves->Array.reduce(Ok(), (done, move) => done->Result.flatMap(() => moveFile(move)))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let describe = ({moves, edits}: plan, ~dryRun: bool): array<string> => {
|
|
172
|
+
let verb = dryRun ? "would " : ""
|
|
173
|
+
let lines = [
|
|
174
|
+
...moves->Array.map(({from, to_}) => `${verb}move ${from} -> ${to_}`),
|
|
175
|
+
...edits->Array.map(({path}) => `${verb}edit ${path}`),
|
|
176
|
+
]
|
|
177
|
+
let movedSources = moves->Array.filter(({from}) => from->String.endsWith(".res"))->Array.length
|
|
178
|
+
lines->Array.push(
|
|
179
|
+
`${dryRun ? "Would rename" : "Renamed"} ${movedSources->Int.toString} file(s) and ${dryRun
|
|
180
|
+
? "edit"
|
|
181
|
+
: "edited"} ${edits->Array.length->Int.toString}.`,
|
|
182
|
+
)
|
|
183
|
+
lines
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The whole run without a process: the lines it prints, or why it failed. */
|
|
187
|
+
let execute = ({dir, dryRun}: args): result<array<string>, string> =>
|
|
188
|
+
if !(dir->NodeFs.existsSync) {
|
|
189
|
+
Error(`${dir} does not exist.`)
|
|
190
|
+
} else {
|
|
191
|
+
plan(~dir)->Result.flatMap(plan =>
|
|
192
|
+
dryRun ? Ok(describe(plan, ~dryRun)) : apply(plan)->Result.map(() => describe(plan, ~dryRun))
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let main = () =>
|
|
197
|
+
CliArgs.run(cli, async args =>
|
|
198
|
+
switch execute(args) {
|
|
199
|
+
| Ok(lines) => lines->Array.forEach(line => Console.log(line))
|
|
200
|
+
| Error(message) =>
|
|
201
|
+
Console.error(`reventless-rename-to-decision: ${message}`)
|
|
202
|
+
NodeProcess.exit(1)
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
// No top-level call: `../../run-rename-to-decision.mjs` invokes [main], so a test
|
|
207
|
+
// can import this module.
|