@savvy-web/cli 3.5.1 → 3.6.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/cli/index.js +19 -3
- package/commands/changeset/commands/check.js +82 -21
- package/commands/changeset/commands/config-validate.js +9 -6
- package/commands/changeset/commands/deps-detect.js +2 -2
- package/commands/changeset/commands/deps-regen.js +94 -48
- package/commands/changeset/commands/init.js +56 -23
- package/commands/changeset/commands/transform.js +82 -8
- package/commands/changeset/commands/validate-file.js +12 -11
- package/commands/changeset/commands/version.js +48 -17
- package/commands/changeset/index.js +3 -3
- package/commands/changeset/utils/config-gate.js +12 -10
- package/commands/check-section.js +34 -0
- package/commands/check.js +48 -39
- package/commands/clean/progress.js +71 -0
- package/commands/clean/view.js +26 -0
- package/commands/clean.js +113 -50
- package/commands/commit/check.js +80 -77
- package/commands/commit/init.js +21 -10
- package/commands/commit/lint.js +2 -2
- package/commands/init.js +122 -34
- package/commands/lint/check.js +126 -171
- package/commands/lint/init.js +52 -33
- package/commands/lint/text.js +28 -13
- package/commands/repos/commands/add.js +14 -17
- package/commands/repos/commands/deregister.js +37 -18
- package/commands/repos/commands/note.js +78 -26
- package/commands/repos/commands/pin.js +39 -24
- package/commands/repos/commands/remove.js +30 -56
- package/commands/repos/commands/rename.js +32 -23
- package/commands/repos/commands/restore.js +42 -56
- package/commands/repos/commands/status.js +43 -47
- package/commands/repos/commands/sync.js +11 -40
- package/commands/repos/index.js +7 -10
- package/commands/repos/shared.js +129 -0
- package/index.d.ts +162 -61
- package/internal/command-error.js +60 -0
- package/internal/confirm.js +52 -0
- package/internal/failure-line.js +26 -12
- package/internal/report.js +101 -0
- package/main.js +23 -16
- package/package.json +17 -12
- package/version.js +1 -1
- package/internal/output.js +0 -52
package/cli/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { initCommand } from "../commands/init.js";
|
|
|
5
5
|
import { lintCommand } from "../commands/lint/index.js";
|
|
6
6
|
import { reposCommand } from "../commands/repos/index.js";
|
|
7
7
|
import { cleanCommand } from "../commands/clean.js";
|
|
8
|
-
import { Command } from "effect/cli";
|
|
8
|
+
import { CliConfig, Command, GlobalFlag } from "effect/cli";
|
|
9
|
+
import { CliAudience } from "@effected/cli";
|
|
9
10
|
import { BiomeSchemaSync, ChangesetConfigReader, Changesets, ConfigDiscovery, Repos, SilkPublishability } from "@savvy-web/silk-effects";
|
|
10
11
|
import { Layer } from "effect";
|
|
11
12
|
import { Git } from "@effected/git";
|
|
@@ -63,7 +64,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
|
63
64
|
/**
|
|
64
65
|
* Root `savvy` command nesting the two orchestrators and three command groups.
|
|
65
66
|
*/
|
|
66
|
-
const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
|
|
67
|
+
const rootCommand = Command.make("savvy").pipe(Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([
|
|
67
68
|
initCommand,
|
|
68
69
|
checkCommand,
|
|
69
70
|
cleanCommand,
|
|
@@ -128,9 +129,24 @@ const ToolDiscoveryGroupLive = ToolDiscovery.layer.pipe(Layer.provide(LocalExecL
|
|
|
128
129
|
* provided once, at the edge; this module never runs anything itself.
|
|
129
130
|
*/
|
|
130
131
|
const AppLive = Layer.mergeAll(ToolDiscoveryGroupLive, InspectorAndAnalyzerLive, ReposGroupLive).pipe(Layer.provideMerge(BaseLive));
|
|
132
|
+
/**
|
|
133
|
+
* Core's built-in global flags for the whole program: `--help`, `--version`,
|
|
134
|
+
* `--completions` and `--log-level`. Core's generic `--wizard` is trimmed —
|
|
135
|
+
* savvy asks its own questions through `@effected/cli/ui` screens. Provided
|
|
136
|
+
* around the program by `main()`, inside `CliRuntime.main`'s environment, so
|
|
137
|
+
* help, parsing and `CliAudience`'s interactive `--wizard` restore all read
|
|
138
|
+
* it (the kit only restores a wizard its own gate removed, never one a
|
|
139
|
+
* consumer's `builtIns` left out).
|
|
140
|
+
*/
|
|
141
|
+
const CliConfigLive = CliConfig.layer({ builtIns: [
|
|
142
|
+
GlobalFlag.Help,
|
|
143
|
+
GlobalFlag.Version,
|
|
144
|
+
GlobalFlag.Completions,
|
|
145
|
+
GlobalFlag.LogLevel
|
|
146
|
+
] });
|
|
131
147
|
/** The platform `main()` passes to `CliRuntime.main`: Node's filesystem, path, stdio, terminal and spawner. */
|
|
132
148
|
const CliPlatform = NodeServices.layer;
|
|
133
149
|
/* v8 ignore stop */
|
|
134
150
|
|
|
135
151
|
//#endregion
|
|
136
|
-
export { AppLive, CliPlatform, rootCommand };
|
|
152
|
+
export { AppLive, CliConfigLive, CliPlatform, rootCommand };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../../internal/report.js";
|
|
2
2
|
import { Argument, Command } from "effect/cli";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { CliExit } from "@effected/cli";
|
|
3
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { CliExit, Doc } from "@effected/cli";
|
|
5
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
6
|
import { Effect } from "effect";
|
|
7
7
|
|
|
@@ -16,8 +16,10 @@ import { Effect } from "effect";
|
|
|
16
16
|
* @remarks
|
|
17
17
|
* The command resolves the directory argument, delegates to
|
|
18
18
|
* {@link ChangesetLinter.validate}, groups the resulting
|
|
19
|
-
* {@link LintMessage} objects by file path, and
|
|
20
|
-
*
|
|
19
|
+
* {@link LintMessage} objects by file path, and prints one section per file
|
|
20
|
+
* (titled by the file, its findings as a position/rule/message table) plus a
|
|
21
|
+
* GitHub Actions error annotation per finding, which only the Actions log
|
|
22
|
+
* renders. Sets exit code 1 through `CliExit.set` when errors
|
|
21
23
|
* are found.
|
|
22
24
|
*
|
|
23
25
|
* @example
|
|
@@ -28,20 +30,44 @@ import { Effect } from "effect";
|
|
|
28
30
|
* @internal
|
|
29
31
|
*/
|
|
30
32
|
const { ChangesetLinter } = Changesets;
|
|
33
|
+
/** The title `savvy check` gives this section. */
|
|
34
|
+
const SECTION_TITLE = "changesets";
|
|
35
|
+
/** The columns of a file's findings table. */
|
|
36
|
+
const FINDING_COLUMNS = [
|
|
37
|
+
{ header: "position" },
|
|
38
|
+
{ header: "rule" },
|
|
39
|
+
{ header: "message" }
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* A finding's file as the report and the runner show it: relative to the
|
|
43
|
+
* working directory when it lies under it (what a GitHub Actions annotation
|
|
44
|
+
* resolves against the workspace), absolute otherwise.
|
|
45
|
+
*/
|
|
46
|
+
const displayPath = (cwd, file) => {
|
|
47
|
+
const rel = relative(cwd, file);
|
|
48
|
+
return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? file : rel;
|
|
49
|
+
};
|
|
31
50
|
/* v8 ignore next */
|
|
32
51
|
const dirArg = Argument.Directory("dir").pipe(Argument.withDefault(".changeset"));
|
|
33
52
|
/**
|
|
34
|
-
*
|
|
53
|
+
* The changeset check as a {@link CheckSection}: its blocks and verdict,
|
|
54
|
+
* unprinted, for `savvy check` to fold into its one document.
|
|
35
55
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* One titled section per file with errors (its findings as a
|
|
58
|
+
* position/rule/message table), each followed by one GitHub Actions error
|
|
59
|
+
* annotation per finding, then the summary line. The annotations sit at the
|
|
60
|
+
* top level of `blocks`, not inside the file sections: the kit draws an
|
|
61
|
+
* annotation only at the top level, as a top-level section's child, or as a
|
|
62
|
+
* direct child of a group's body, so this placement survives the caller
|
|
63
|
+
* wrapping `blocks` in a collapsible.
|
|
38
64
|
*
|
|
39
65
|
* @param dir - Path to the changeset directory (resolved relative to cwd)
|
|
40
|
-
* @returns
|
|
66
|
+
* @returns The section; `verdict` is `failure` when any file has errors
|
|
41
67
|
*
|
|
42
68
|
* @internal
|
|
43
69
|
*/
|
|
44
|
-
function
|
|
70
|
+
function changesetCheckSection(dir) {
|
|
45
71
|
return Effect.gen(function* () {
|
|
46
72
|
const resolved = resolve(dir);
|
|
47
73
|
const messages = yield* Effect.try({
|
|
@@ -54,22 +80,57 @@ function runChangesetCheck(dir) {
|
|
|
54
80
|
if (existing) existing.push(msg);
|
|
55
81
|
else byFile.set(msg.file, [msg]);
|
|
56
82
|
}
|
|
83
|
+
if (messages.length === 0) return {
|
|
84
|
+
title: SECTION_TITLE,
|
|
85
|
+
blocks: [Report.ok("All changeset files passed validation")],
|
|
86
|
+
verdict: "success",
|
|
87
|
+
fixedByInit: false
|
|
88
|
+
};
|
|
89
|
+
const cwd = resolve();
|
|
90
|
+
const blocks = [];
|
|
57
91
|
for (const [file, fileMessages] of byFile) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
92
|
+
const display = displayPath(cwd, file);
|
|
93
|
+
blocks.push(Doc.section(Doc.file(display), [Doc.table(FINDING_COLUMNS, fileMessages.map((msg) => [
|
|
94
|
+
`${msg.line}:${msg.column}`,
|
|
95
|
+
msg.rule,
|
|
96
|
+
msg.message
|
|
97
|
+
]))]), ...fileMessages.map((msg) => Doc.annotation({
|
|
98
|
+
level: "error",
|
|
99
|
+
file: display,
|
|
100
|
+
line: msg.line,
|
|
101
|
+
col: msg.column,
|
|
102
|
+
title: msg.rule
|
|
103
|
+
}, msg.message)));
|
|
61
104
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
105
|
+
blocks.push(Report.fail(`${byFile.size} file(s) with errors, ${messages.length} error(s) found`));
|
|
106
|
+
return {
|
|
107
|
+
title: SECTION_TITLE,
|
|
108
|
+
blocks,
|
|
109
|
+
verdict: "failure",
|
|
110
|
+
fixedByInit: false
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Run the check validation pipeline on all changeset files in `dir` and
|
|
116
|
+
* print {@link changesetCheckSection}'s blocks (no title heading: the
|
|
117
|
+
* standalone `savvy changeset check` output). Sets exit code 1 through
|
|
118
|
+
* `CliExit.set` when one or more errors are found.
|
|
119
|
+
*
|
|
120
|
+
* @param dir - Path to the changeset directory (resolved relative to cwd)
|
|
121
|
+
* @returns An Effect that performs validation and prints the report
|
|
122
|
+
*
|
|
123
|
+
* @internal
|
|
124
|
+
*/
|
|
125
|
+
function runChangesetCheck(dir) {
|
|
126
|
+
return Effect.gen(function* () {
|
|
127
|
+
const section = yield* changesetCheckSection(dir);
|
|
128
|
+
yield* Report.print(section.blocks);
|
|
129
|
+
if (section.verdict === "failure") yield* CliExit.set(1);
|
|
69
130
|
});
|
|
70
131
|
}
|
|
71
132
|
/* v8 ignore next 3 -- CLI registration; handler tested via runChangesetCheck */
|
|
72
133
|
const checkCommand = Command.make("check", { dir: dirArg }, ({ dir }) => runChangesetCheck(dir)).pipe(Command.withDescription("Full changeset validation with summary"));
|
|
73
134
|
|
|
74
135
|
//#endregion
|
|
75
|
-
export { checkCommand, runChangesetCheck };
|
|
136
|
+
export { changesetCheckSection, checkCommand, runChangesetCheck };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../../internal/report.js";
|
|
2
2
|
import { Argument, Command } from "effect/cli";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
4
|
import { CliExit } from "@effected/cli";
|
|
5
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
6
|
import { Effect } from "effect";
|
|
@@ -30,8 +30,11 @@ const { ConfigInspector } = Changesets;
|
|
|
30
30
|
/* v8 ignore next */
|
|
31
31
|
const dirArg = Argument.Directory("dir").pipe(Argument.withDefault("."));
|
|
32
32
|
/**
|
|
33
|
-
* Run validation
|
|
34
|
-
*
|
|
33
|
+
* Run validation and print the outcome as one stdout line: `✓ <config> —
|
|
34
|
+
* N packages declared` on success, `✗ <config> — field: reason` plus exit
|
|
35
|
+
* code 1 through `CliExit.set` on a finding. Both outcomes stay on stdout as
|
|
36
|
+
* `Report` lines (not `CliMessage`, whose failure line goes to stderr), so a
|
|
37
|
+
* gate reading the result reads one stream.
|
|
35
38
|
*
|
|
36
39
|
* @internal
|
|
37
40
|
*/
|
|
@@ -51,10 +54,10 @@ function runConfigValidate(dir) {
|
|
|
51
54
|
const { config } = result;
|
|
52
55
|
const pkgCount = config.packages.length;
|
|
53
56
|
const note = config.legacyVersionFilesUsed ? " (warning: legacy versionFiles in use)" : "";
|
|
54
|
-
yield*
|
|
57
|
+
yield* Report.print([Report.ok(`${config.configPath} — ${pkgCount} package${pkgCount === 1 ? "" : "s"} declared${note}`)]);
|
|
55
58
|
return;
|
|
56
59
|
}
|
|
57
|
-
yield*
|
|
60
|
+
yield* Report.print([Report.fail(`${join(resolved, ".changeset", "config.json")} — ${result.field}: ${result.reason}`)]);
|
|
58
61
|
yield* CliExit.set(1);
|
|
59
62
|
});
|
|
60
63
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../../internal/report.js";
|
|
2
2
|
import { Command, Flag } from "effect/cli";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { CliExit } from "@effected/cli";
|
|
@@ -69,7 +69,7 @@ function runDepsDetect(cwd, from, to, pkg, json, markdown) {
|
|
|
69
69
|
...Option.isSome(to) ? { to: to.value } : {}
|
|
70
70
|
}).pipe(Effect.tapError(() => CliExit.set(1)))).toWrite.map((entry) => entry.diff);
|
|
71
71
|
if (markdown && !json) {
|
|
72
|
-
yield*
|
|
72
|
+
yield* Report.print([Report.verbatim(renderMarkdownBlocks(diffs))]);
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
yield* Console.log(JSON.stringify(diffs, null, 2));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../../internal/report.js";
|
|
2
2
|
import { Command, Flag } from "effect/cli";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { CliExit } from "@effected/cli";
|
|
4
|
+
import { CliExit, Doc } from "@effected/cli";
|
|
5
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
6
6
|
import { Console, Effect, Option } from "effect";
|
|
7
7
|
|
|
@@ -41,9 +41,12 @@ import { Console, Effect, Option } from "effect";
|
|
|
41
41
|
*
|
|
42
42
|
* **Dry-run reporting.** With `--dry-run` nothing is written or deleted, so
|
|
43
43
|
* the human output is phrased as a plan (`Would delete N pure dependency
|
|
44
|
-
* changeset(s):` / `Would write N dependency changeset(s)
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* changeset(s):` / `Would write N dependency changeset(s):`, each over a
|
|
45
|
+
* file / package (/ rows) table) rather than the
|
|
46
|
+
* real run's `✓ Wrote …` / `✓ Deleted …`. A real run prints each write and
|
|
47
|
+
* delete as `execute()` reports it landing (its `onStep`), so a run that
|
|
48
|
+
* fails partway still shows what already reached disk; it reports what
|
|
49
|
+
* `execute()` did, not what the plan listed: deletes are tolerant, so a
|
|
47
50
|
* planned delete that found nothing or could not remove the file is reported
|
|
48
51
|
* as not removed. `--json`
|
|
49
52
|
* emits the plan's fields plus an explicit `dryRun` boolean in both modes,
|
|
@@ -79,57 +82,100 @@ function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
|
79
82
|
...Option.isSome(base) ? { base: base.value } : {},
|
|
80
83
|
...Option.isSome(pkg) ? { package: pkg.value } : {}
|
|
81
84
|
}).pipe(Effect.tapError(() => CliExit.set(1)));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
if (json) {
|
|
86
|
+
const result = dryRun ? void 0 : yield* service.execute(plan);
|
|
87
|
+
yield* Console.log(JSON.stringify({
|
|
88
|
+
...plan,
|
|
89
|
+
dryRun,
|
|
90
|
+
...result ? { result } : {}
|
|
91
|
+
}, null, 2));
|
|
92
|
+
} else if (dryRun) yield* renderDryRunPlan(plan);
|
|
93
|
+
else yield* runAndReport(plan, service);
|
|
89
94
|
});
|
|
90
95
|
}
|
|
96
|
+
/** The columns of a table of changesets to delete (or not removed). */
|
|
97
|
+
const DELETE_COLUMNS = [{ header: "file" }, { header: "package" }];
|
|
98
|
+
/** The columns of a table of changesets to write. */
|
|
99
|
+
const WRITE_COLUMNS = [
|
|
100
|
+
{ header: "file" },
|
|
101
|
+
{ header: "package" },
|
|
102
|
+
{
|
|
103
|
+
header: "rows",
|
|
104
|
+
align: "right"
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
const row = (entry) => `${entry.file} (${entry.package})`;
|
|
108
|
+
const writeRow = (entry) => `${entry.file} (${entry.package} — ${entry.diff.rows.length} row${entry.diff.rows.length === 1 ? "" : "s"})`;
|
|
91
109
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* `result.deleted` was already gone or could not be removed, and
|
|
96
|
-
* not removed, never as deleted.
|
|
110
|
+
* Apply the plan for a person, printing each write and delete as `execute`
|
|
111
|
+
* reports it landing, so a failure partway still leaves what reached disk on
|
|
112
|
+
* stdout. The closing summary covers what no step reported: a planned delete
|
|
113
|
+
* absent from `result.deleted` was already gone or could not be removed, and
|
|
114
|
+
* is reported as not removed, never as deleted. Only paths `execute` reports
|
|
115
|
+
* are ever named as written or deleted.
|
|
97
116
|
*/
|
|
98
|
-
function
|
|
117
|
+
function runAndReport(plan, service) {
|
|
99
118
|
return Effect.gen(function* () {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
const writes = new Map(plan.toWrite.map((entry) => [entry.file, entry]));
|
|
120
|
+
const deletes = new Map(plan.toDelete.map((entry) => [entry.file, entry]));
|
|
121
|
+
const reported = /* @__PURE__ */ new Set();
|
|
122
|
+
const stepBlock = (step) => {
|
|
123
|
+
if (step._tag === "Written") {
|
|
124
|
+
const entry = writes.get(step.file);
|
|
125
|
+
return Report.ok(`Wrote ${entry ? writeRow(entry) : step.file}`);
|
|
106
126
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
yield* dryRun ? Output.heading(`Would write ${writes.length} dependency changeset(s):`) : Output.ok(`Wrote ${writes.length} fresh dependency changeset(s):`);
|
|
123
|
-
for (const entry of writes) yield* Output.detail(`+ ${entry.file} (${entry.package} — ${entry.diff.rows.length} row${entry.diff.rows.length === 1 ? "" : "s"})`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (plan.skippedMixed.length > 0) {
|
|
127
|
-
yield* Output.line("");
|
|
128
|
-
yield* Output.skip(`Skipped ${plan.skippedMixed.length} mixed changeset(s) (have Dependencies but also other content):`);
|
|
129
|
-
for (const file of plan.skippedMixed) yield* Output.detail(`~ ${file}`);
|
|
130
|
-
}
|
|
127
|
+
const entry = deletes.get(step.file);
|
|
128
|
+
return Report.ok(`Deleted ${entry ? row(entry) : step.file}`);
|
|
129
|
+
};
|
|
130
|
+
const printStep = (step) => Effect.suspend(() => {
|
|
131
|
+
reported.add(`${step._tag}:${step.file}`);
|
|
132
|
+
return Report.print([stepBlock(step)]);
|
|
133
|
+
});
|
|
134
|
+
const result = yield* service.execute(plan, { onStep: printStep });
|
|
135
|
+
yield* renderRunSummary(plan, result, [...result.written.filter((file) => !reported.has(`Written:${file}`)).map((file) => stepBlock({
|
|
136
|
+
_tag: "Written",
|
|
137
|
+
file
|
|
138
|
+
})), ...result.deleted.filter((file) => !reported.has(`Deleted:${file}`)).map((file) => stepBlock({
|
|
139
|
+
_tag: "Deleted",
|
|
140
|
+
file
|
|
141
|
+
}))]);
|
|
131
142
|
});
|
|
132
143
|
}
|
|
144
|
+
/** The closing document of a real run: whatever no step printed, then the totals. */
|
|
145
|
+
function renderRunSummary(plan, result, unreported) {
|
|
146
|
+
const blocks = [...unreported];
|
|
147
|
+
if (plan.toDelete.length === 0 && plan.toWrite.length === 0) blocks.push(Report.ok("No dependency changes to regenerate"));
|
|
148
|
+
else {
|
|
149
|
+
const deleted = new Set(result.deleted);
|
|
150
|
+
const notRemoved = plan.toDelete.filter((entry) => !deleted.has(entry.file));
|
|
151
|
+
if (notRemoved.length > 0) blocks.push(Report.skip(`${notRemoved.length} planned deletion(s) not removed (already gone or undeletable):`), Doc.table(DELETE_COLUMNS, notRemoved.map((entry) => [entry.file, entry.package])));
|
|
152
|
+
if (result.written.length > 0 || result.deleted.length > 0) blocks.push(Report.ok(`Wrote ${result.written.length} fresh and deleted ${result.deleted.length} pure dependency changeset(s)`));
|
|
153
|
+
}
|
|
154
|
+
return Report.print([...blocks, ...mixedBlocks(plan)]);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Render a dry run for a person. Nothing changed, so its headings are
|
|
158
|
+
* plan-phrased and carry no `✓`.
|
|
159
|
+
*/
|
|
160
|
+
function renderDryRunPlan(plan) {
|
|
161
|
+
const blocks = [];
|
|
162
|
+
if (plan.toDelete.length === 0 && plan.toWrite.length === 0) blocks.push(Report.ok("No dependency changes to regenerate"));
|
|
163
|
+
else {
|
|
164
|
+
if (plan.toDelete.length > 0) blocks.push(Doc.section(`Would delete ${plan.toDelete.length} pure dependency changeset(s):`, [Doc.table(DELETE_COLUMNS, plan.toDelete.map((entry) => [entry.file, entry.package]))]));
|
|
165
|
+
if (plan.toWrite.length > 0) blocks.push(Doc.section(`Would write ${plan.toWrite.length} dependency changeset(s):`, [Doc.table(WRITE_COLUMNS, plan.toWrite.map((entry) => [
|
|
166
|
+
entry.file,
|
|
167
|
+
entry.package,
|
|
168
|
+
String(entry.diff.rows.length)
|
|
169
|
+
]))]));
|
|
170
|
+
}
|
|
171
|
+
return Report.print([...blocks, ...mixedBlocks(plan)]);
|
|
172
|
+
}
|
|
173
|
+
/** The skipped-mixed note both a dry and a real run close with. */
|
|
174
|
+
function mixedBlocks(plan) {
|
|
175
|
+
const blocks = [];
|
|
176
|
+
if (plan.skippedMixed.length > 0) blocks.push(Report.line(""), Report.skip(`Skipped ${plan.skippedMixed.length} mixed changeset(s) (have Dependencies but also other content):`, ...plan.skippedMixed.map((file) => `~ ${file}`)));
|
|
177
|
+
return blocks;
|
|
178
|
+
}
|
|
133
179
|
/* v8 ignore next 12 */
|
|
134
180
|
const depsRegenCommand = Command.make("regen", {
|
|
135
181
|
cwd: cwdOption,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Report } from "../../../internal/report.js";
|
|
2
|
+
import { CommandError } from "../../../internal/command-error.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
import { CliExit } from "@effected/cli";
|
|
4
|
+
import { CliDoc, CliExit, Doc } from "@effected/cli";
|
|
4
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
5
6
|
import { Data, Effect, Option, Result, Schema } from "effect";
|
|
7
|
+
import { TerminalEnv } from "@effected/env";
|
|
6
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
9
|
import { Git } from "@effected/git";
|
|
8
10
|
import { Jsonc, JsoncEdit, JsoncModifier } from "@effected/jsonc";
|
|
@@ -555,17 +557,18 @@ function checkChangesetMarkdownlint(changesetDir) {
|
|
|
555
557
|
}
|
|
556
558
|
}
|
|
557
559
|
/**
|
|
558
|
-
* Run the full init pipeline.
|
|
560
|
+
* Run the full init pipeline, failing when a step fails.
|
|
559
561
|
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
+
* Steps 3–5 each run even when an earlier one fails; the run then fails with
|
|
563
|
+
* one {@link CommandError} listing every failed step. `quiet` swallows that
|
|
564
|
+
* failure (and any other) instead.
|
|
562
565
|
*
|
|
563
566
|
* @param opts - The same options the CLI command receives
|
|
564
567
|
* @returns An Effect that performs initialization
|
|
565
568
|
*
|
|
566
569
|
* @internal
|
|
567
570
|
*/
|
|
568
|
-
function
|
|
571
|
+
function runChangesetInitOrFail(opts) {
|
|
569
572
|
const { force, quiet, skipMarkdownlint, check } = opts;
|
|
570
573
|
return Effect.gen(function* () {
|
|
571
574
|
const root = yield* resolveWorkspaceRoot(process.cwd());
|
|
@@ -581,42 +584,72 @@ function runChangesetInit(opts) {
|
|
|
581
584
|
...checkChangesetMarkdownlint(changesetDir)
|
|
582
585
|
];
|
|
583
586
|
if (issues.length === 0) {
|
|
584
|
-
yield*
|
|
587
|
+
yield* Report.print([Report.ok("All @savvy-web/changesets config files are up to date")]);
|
|
585
588
|
return;
|
|
586
589
|
}
|
|
587
|
-
|
|
588
|
-
yield* Output.detail("Run \"savvy init --force\" to fix");
|
|
590
|
+
yield* Report.print([...issues.map((issue) => Report.warn(`${issue.file}: ${issue.message}`)), Report.detail("Run \"savvy init --force\" to fix")]);
|
|
589
591
|
return;
|
|
590
592
|
}
|
|
591
593
|
const changesetDir = yield* ensureChangesetDir(root);
|
|
592
|
-
yield*
|
|
594
|
+
yield* Report.print([Report.ok("Ensured .changeset/ directory")]);
|
|
593
595
|
const errors = [];
|
|
594
596
|
const configResult = yield* handleConfig(changesetDir, repoSlug, force).pipe(Effect.result);
|
|
595
597
|
if (Result.isSuccess(configResult)) {
|
|
596
|
-
yield*
|
|
598
|
+
yield* Report.print([Report.ok(configResult.success)]);
|
|
597
599
|
if (!quiet) yield* warnIfLegacyVersionFiles(changesetDir);
|
|
598
600
|
} else errors.push(configResult.failure);
|
|
599
601
|
if (!skipMarkdownlint) {
|
|
600
602
|
const baseResult = yield* handleBaseMarkdownlint(root).pipe(Effect.result);
|
|
601
|
-
if (Result.isSuccess(baseResult)) yield*
|
|
603
|
+
if (Result.isSuccess(baseResult)) yield* Report.print([Report.ok(baseResult.success)]);
|
|
602
604
|
else errors.push(baseResult.failure);
|
|
603
605
|
}
|
|
604
606
|
const mdlintResult = yield* handleChangesetMarkdownlint(changesetDir, root, force).pipe(Effect.result);
|
|
605
|
-
if (Result.isSuccess(mdlintResult)) yield*
|
|
607
|
+
if (Result.isSuccess(mdlintResult)) yield* Report.print([Report.ok(mdlintResult.success)]);
|
|
606
608
|
else errors.push(mdlintResult.failure);
|
|
607
609
|
if (errors.length > 0) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (!quiet) {
|
|
615
|
-
yield* Effect.logError(error instanceof InitError ? error.message : `Init failed: ${String(error)}`);
|
|
616
|
-
yield* CliExit.set(1);
|
|
610
|
+
if (quiet) return;
|
|
611
|
+
return yield* Effect.fail(new CommandError({
|
|
612
|
+
message: `changeset init could not finish ${errors.length} step(s)`,
|
|
613
|
+
detail: errors.map((err) => err.message),
|
|
614
|
+
hint: "Fix the files named above, then re-run \"savvy init\"."
|
|
615
|
+
}));
|
|
617
616
|
}
|
|
617
|
+
yield* Report.print([Report.ok("Init complete")]);
|
|
618
|
+
}).pipe(Effect.catch((error) => quiet ? Effect.void : Effect.fail(error instanceof CommandError ? error : CommandError.from(error, {
|
|
619
|
+
message: "changeset init failed",
|
|
620
|
+
hint: "Fix the problem above, then re-run \"savvy init\"."
|
|
621
|
+
}))));
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Run the changeset init as one step of the orchestrated `savvy init`: a
|
|
625
|
+
* failure is reported, not propagated, so the commit and lint steps still
|
|
626
|
+
* run.
|
|
627
|
+
*
|
|
628
|
+
* @remarks
|
|
629
|
+
* Runs {@link runChangesetInitOrFail}. Its {@link CommandError} is drawn on
|
|
630
|
+
* stderr exactly as the runtime draws a failure (its `CliDoc` document,
|
|
631
|
+
* rendered for the audience; never wrapped off a terminal), exit code 1 is
|
|
632
|
+
* set through `CliExit.set`, and the step succeeds. A caller that wants the
|
|
633
|
+
* failure to end the run calls {@link runChangesetInitOrFail} instead.
|
|
634
|
+
*
|
|
635
|
+
* Exported so the unified `savvy init` orchestrator can invoke the changeset
|
|
636
|
+
* init step directly without going through the CLI command layer.
|
|
637
|
+
*
|
|
638
|
+
* @param opts - The same options the CLI command receives
|
|
639
|
+
* @returns An Effect that never fails
|
|
640
|
+
*
|
|
641
|
+
* @internal
|
|
642
|
+
*/
|
|
643
|
+
function runChangesetInit(opts) {
|
|
644
|
+
return runChangesetInitOrFail(opts).pipe(Effect.catchTag("CommandError", (error) => Effect.gen(function* () {
|
|
645
|
+
const terminal = yield* TerminalEnv;
|
|
646
|
+
yield* Doc.print(error[CliDoc](), {
|
|
647
|
+
stream: "stderr",
|
|
648
|
+
...terminal.stderr.isTerminal ? {} : { width: Number.POSITIVE_INFINITY }
|
|
649
|
+
});
|
|
650
|
+
yield* CliExit.set(1);
|
|
618
651
|
})));
|
|
619
652
|
}
|
|
620
653
|
|
|
621
654
|
//#endregion
|
|
622
|
-
export { InitError, InitErrorBase, checkBaseMarkdownlint, checkChangesetDir, checkChangesetMarkdownlint, checkConfig, detectGitHubRepo, detectLegacyVersionFiles, ensureChangesetDir, findMarkdownlintConfig, handleBaseMarkdownlint, handleChangesetMarkdownlint, handleConfig, legacyVersionFilesWarning, resolveWorkspaceRoot, runChangesetInit, warnIfLegacyVersionFiles };
|
|
655
|
+
export { InitError, InitErrorBase, checkBaseMarkdownlint, checkChangesetDir, checkChangesetMarkdownlint, checkConfig, detectGitHubRepo, detectLegacyVersionFiles, ensureChangesetDir, findMarkdownlintConfig, handleBaseMarkdownlint, handleChangesetMarkdownlint, handleConfig, legacyVersionFilesWarning, resolveWorkspaceRoot, runChangesetInit, runChangesetInitOrFail, warnIfLegacyVersionFiles };
|