@savvy-web/cli 3.2.7 → 3.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/cli/index.js +8 -5
- package/commands/changeset/commands/check.js +11 -7
- package/commands/changeset/commands/config-validate.js +6 -4
- package/commands/changeset/commands/deps-detect.js +4 -4
- package/commands/changeset/commands/deps-regen.js +11 -10
- package/commands/changeset/commands/init.js +13 -11
- package/commands/changeset/commands/lint.js +4 -3
- package/commands/changeset/commands/transform.js +7 -5
- package/commands/changeset/commands/validate-file.js +8 -6
- package/commands/changeset/commands/version.js +5 -4
- package/commands/changeset/utils/config-gate.js +3 -3
- package/commands/check.js +3 -2
- package/commands/clean.js +7 -8
- package/commands/commit/check.js +33 -35
- package/commands/commit/constants.js +5 -0
- package/commands/commit/init.js +8 -6
- package/commands/commit/lint.js +2 -1
- package/commands/lint/check.js +43 -47
- package/commands/lint/init.js +20 -22
- package/commands/repos/commands/add.js +8 -9
- package/commands/repos/commands/deregister.js +7 -7
- package/commands/repos/commands/note.js +7 -8
- package/commands/repos/commands/pin.js +11 -13
- package/commands/repos/commands/remove.js +13 -15
- package/commands/repos/commands/rename.js +10 -12
- package/commands/repos/commands/restore.js +12 -14
- package/commands/repos/commands/status.js +16 -8
- package/commands/repos/commands/sync.js +12 -13
- package/index.d.ts +16 -15
- package/internal/failure-line.js +39 -0
- package/internal/output.js +52 -0
- package/internal/version-line.js +19 -0
- package/main.d.ts +18 -2
- package/main.js +26 -16
- package/package.json +5 -3
- package/version.js +13 -0
package/cli/index.js
CHANGED
|
@@ -122,12 +122,15 @@ const LocalExecLive = Workspaces.localExecLayer();
|
|
|
122
122
|
const ToolDiscoveryGroupLive = ToolDiscovery.layer.pipe(Layer.provide(LocalExecLive), Layer.provide(WorkspaceLive));
|
|
123
123
|
/**
|
|
124
124
|
* The merged runtime Layer stack satisfying every command's service
|
|
125
|
-
* requirements
|
|
126
|
-
* `main()` in `../main.js
|
|
127
|
-
*
|
|
125
|
+
* requirements, still open on the platform services. Provided to
|
|
126
|
+
* `rootCommand`'s assembled `Command.run` Effect by `main()` in `../main.js`,
|
|
127
|
+
* which hands {@link CliPlatform} to `CliRuntime.main` so the platform is
|
|
128
|
+
* provided once, at the edge; this module never runs anything itself.
|
|
128
129
|
*/
|
|
129
|
-
const AppLive = Layer.mergeAll(ToolDiscoveryGroupLive, InspectorAndAnalyzerLive, ReposGroupLive).pipe(Layer.provideMerge(BaseLive)
|
|
130
|
+
const AppLive = Layer.mergeAll(ToolDiscoveryGroupLive, InspectorAndAnalyzerLive, ReposGroupLive).pipe(Layer.provideMerge(BaseLive));
|
|
131
|
+
/** The platform `main()` passes to `CliRuntime.main`: Node's filesystem, path, stdio, terminal and spawner. */
|
|
132
|
+
const CliPlatform = NodeServices.layer;
|
|
130
133
|
/* v8 ignore stop */
|
|
131
134
|
|
|
132
135
|
//#endregion
|
|
133
|
-
export { AppLive, rootCommand };
|
|
136
|
+
export { AppLive, CliPlatform, rootCommand };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { Argument, Command } from "effect/unstable/cli";
|
|
2
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { CliExit } from "@effected/cli";
|
|
3
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
6
|
import { Effect } from "effect";
|
|
5
7
|
|
|
@@ -15,7 +17,7 @@ import { Effect } from "effect";
|
|
|
15
17
|
* The command resolves the directory argument, delegates to
|
|
16
18
|
* {@link ChangesetLinter.validate}, groups the resulting
|
|
17
19
|
* {@link LintMessage} objects by file path, and logs each file's errors
|
|
18
|
-
* indented under the file name. Sets
|
|
20
|
+
* indented under the file name. Sets exit code 1 through `CliExit.set` when errors
|
|
19
21
|
* are found.
|
|
20
22
|
*
|
|
21
23
|
* @example
|
|
@@ -32,7 +34,7 @@ const dirArg = Argument.Directory("dir").pipe(Argument.withDefault(".changeset")
|
|
|
32
34
|
* Run the check validation pipeline on all changeset files in `dir`.
|
|
33
35
|
*
|
|
34
36
|
* Groups lint messages by file and logs a human-readable summary. Sets
|
|
35
|
-
*
|
|
37
|
+
* exit code 1 through `CliExit.set` when one or more errors are found.
|
|
36
38
|
*
|
|
37
39
|
* @param dir - Path to the changeset directory (resolved relative to cwd)
|
|
38
40
|
* @returns An Effect that performs validation and logs results
|
|
@@ -53,15 +55,17 @@ function runChangesetCheck(dir) {
|
|
|
53
55
|
else byFile.set(msg.file, [msg]);
|
|
54
56
|
}
|
|
55
57
|
for (const [file, fileMessages] of byFile) {
|
|
56
|
-
yield*
|
|
57
|
-
|
|
58
|
+
yield* Output.line("");
|
|
59
|
+
yield* Output.heading(file);
|
|
60
|
+
for (const msg of fileMessages) yield* Output.detail(`${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
58
61
|
}
|
|
59
62
|
const errorCount = messages.length;
|
|
60
63
|
const filesWithErrors = byFile.size;
|
|
61
64
|
if (errorCount > 0) {
|
|
62
|
-
yield*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
yield* Output.line("");
|
|
66
|
+
yield* Output.fail(`${filesWithErrors} file(s) with errors, ${errorCount} error(s) found`);
|
|
67
|
+
yield* CliExit.set(1);
|
|
68
|
+
} else yield* Output.ok("All changeset files passed validation");
|
|
65
69
|
});
|
|
66
70
|
}
|
|
67
71
|
/* v8 ignore next 3 -- CLI registration; handler tested via runChangesetCheck */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { Argument, Command } from "effect/unstable/cli";
|
|
2
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { CliExit } from "@effected/cli";
|
|
3
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
6
|
import { Effect } from "effect";
|
|
5
7
|
|
|
@@ -29,7 +31,7 @@ const { ConfigInspector } = Changesets;
|
|
|
29
31
|
const dirArg = Argument.Directory("dir").pipe(Argument.withDefault("."));
|
|
30
32
|
/**
|
|
31
33
|
* Run validation. Logs a one-line OK on success; logs the error and sets
|
|
32
|
-
*
|
|
34
|
+
* exit code 1 through `CliExit.set` on failure.
|
|
33
35
|
*
|
|
34
36
|
* @internal
|
|
35
37
|
*/
|
|
@@ -49,11 +51,11 @@ function runConfigValidate(dir) {
|
|
|
49
51
|
const { config } = result;
|
|
50
52
|
const pkgCount = config.packages.length;
|
|
51
53
|
const note = config.legacyVersionFilesUsed ? " (warning: legacy versionFiles in use)" : "";
|
|
52
|
-
yield*
|
|
54
|
+
yield* Output.ok(`${config.configPath} — ${pkgCount} package${pkgCount === 1 ? "" : "s"} declared${note}`);
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
|
-
yield*
|
|
56
|
-
|
|
57
|
+
yield* Output.fail(`${result.field}: ${result.reason}`);
|
|
58
|
+
yield* CliExit.set(1);
|
|
57
59
|
});
|
|
58
60
|
}
|
|
59
61
|
/* v8 ignore next 5 -- CLI registration */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { CliExit } from "@effected/cli";
|
|
3
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
6
|
import { Console, Effect, Option } from "effect";
|
|
5
7
|
|
|
@@ -65,11 +67,9 @@ function runDepsDetect(cwd, from, to, pkg, json, markdown) {
|
|
|
65
67
|
...Option.isSome(pkg) ? { package: pkg.value } : {},
|
|
66
68
|
...Option.isSome(from) ? { from: from.value } : {},
|
|
67
69
|
...Option.isSome(to) ? { to: to.value } : {}
|
|
68
|
-
}).pipe(Effect.tapError(() =>
|
|
69
|
-
process.exitCode = 1;
|
|
70
|
-
})))).toWrite.map((entry) => entry.diff);
|
|
70
|
+
}).pipe(Effect.tapError(() => CliExit.set(1)))).toWrite.map((entry) => entry.diff);
|
|
71
71
|
if (markdown && !json) {
|
|
72
|
-
yield*
|
|
72
|
+
yield* Output.line(renderMarkdownBlocks(diffs));
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
yield* Console.log(JSON.stringify(diffs, null, 2));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { CliExit } from "@effected/cli";
|
|
3
5
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
6
|
import { Console, Effect, Option } from "effect";
|
|
5
7
|
|
|
@@ -66,9 +68,7 @@ function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
|
66
68
|
cwd,
|
|
67
69
|
...Option.isSome(base) ? { base: base.value } : {},
|
|
68
70
|
...Option.isSome(pkg) ? { package: pkg.value } : {}
|
|
69
|
-
}).pipe(Effect.tapError(() =>
|
|
70
|
-
process.exitCode = 1;
|
|
71
|
-
})));
|
|
71
|
+
}).pipe(Effect.tapError(() => CliExit.set(1)));
|
|
72
72
|
if (!dryRun) yield* service.execute(plan);
|
|
73
73
|
if (json) yield* Console.log(JSON.stringify(plan, null, 2));
|
|
74
74
|
else yield* renderHumanPlan(plan);
|
|
@@ -76,20 +76,21 @@ function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
|
76
76
|
}
|
|
77
77
|
function renderHumanPlan(plan) {
|
|
78
78
|
return Effect.gen(function* () {
|
|
79
|
-
if (plan.toDelete.length === 0 && plan.toWrite.length === 0) yield*
|
|
79
|
+
if (plan.toDelete.length === 0 && plan.toWrite.length === 0) yield* Output.ok("No dependency changes to regenerate");
|
|
80
80
|
else {
|
|
81
81
|
if (plan.toDelete.length > 0) {
|
|
82
|
-
yield*
|
|
83
|
-
for (const entry of plan.toDelete) yield*
|
|
82
|
+
yield* Output.ok(`Deleted ${plan.toDelete.length} pure dependency changeset(s):`);
|
|
83
|
+
for (const entry of plan.toDelete) yield* Output.detail(`${entry.file} (${entry.package})`);
|
|
84
84
|
}
|
|
85
85
|
if (plan.toWrite.length > 0) {
|
|
86
|
-
yield*
|
|
87
|
-
for (const entry of plan.toWrite) yield*
|
|
86
|
+
yield* Output.ok(`Wrote ${plan.toWrite.length} fresh dependency changeset(s):`);
|
|
87
|
+
for (const entry of plan.toWrite) yield* Output.detail(`+ ${entry.file} (${entry.package} — ${entry.diff.rows.length} row${entry.diff.rows.length === 1 ? "" : "s"})`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
if (plan.skippedMixed.length > 0) {
|
|
91
|
-
yield*
|
|
92
|
-
|
|
91
|
+
yield* Output.line("");
|
|
92
|
+
yield* Output.skip(`Skipped ${plan.skippedMixed.length} mixed changeset(s) (have Dependencies but also other content):`);
|
|
93
|
+
for (const file of plan.skippedMixed) yield* Output.detail(`~ ${file}`);
|
|
93
94
|
}
|
|
94
95
|
});
|
|
95
96
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { join } from "node:path";
|
|
3
|
+
import { CliExit } from "@effected/cli";
|
|
2
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
5
|
import { Data, Effect, Option, Result, Schema } from "effect";
|
|
4
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -568,7 +570,7 @@ function runChangesetInit(opts) {
|
|
|
568
570
|
return Effect.gen(function* () {
|
|
569
571
|
const root = yield* resolveWorkspaceRoot(process.cwd());
|
|
570
572
|
const repo = yield* detectGitHubRepo(root);
|
|
571
|
-
if (!repo && !quiet) yield* Effect.
|
|
573
|
+
if (!repo && !quiet) yield* Effect.logWarning("could not detect GitHub repo from git remote, using placeholder");
|
|
572
574
|
const repoSlug = repo ?? "owner/repo";
|
|
573
575
|
if (check) {
|
|
574
576
|
const changesetDir = join(root, ".changeset");
|
|
@@ -579,39 +581,39 @@ function runChangesetInit(opts) {
|
|
|
579
581
|
...checkChangesetMarkdownlint(changesetDir)
|
|
580
582
|
];
|
|
581
583
|
if (issues.length === 0) {
|
|
582
|
-
yield*
|
|
584
|
+
yield* Output.ok("All @savvy-web/changesets config files are up to date");
|
|
583
585
|
return;
|
|
584
586
|
}
|
|
585
|
-
for (const issue of issues) yield*
|
|
586
|
-
yield*
|
|
587
|
+
for (const issue of issues) yield* Output.warn(`${issue.file}: ${issue.message}`);
|
|
588
|
+
yield* Output.detail("Run \"savvy init --force\" to fix");
|
|
587
589
|
return;
|
|
588
590
|
}
|
|
589
591
|
const changesetDir = yield* ensureChangesetDir(root);
|
|
590
|
-
yield*
|
|
592
|
+
yield* Output.ok("Ensured .changeset/ directory");
|
|
591
593
|
const errors = [];
|
|
592
594
|
const configResult = yield* handleConfig(changesetDir, repoSlug, force).pipe(Effect.result);
|
|
593
595
|
if (Result.isSuccess(configResult)) {
|
|
594
|
-
yield*
|
|
596
|
+
yield* Output.ok(configResult.success);
|
|
595
597
|
if (!quiet) yield* warnIfLegacyVersionFiles(changesetDir);
|
|
596
598
|
} else errors.push(configResult.failure);
|
|
597
599
|
if (!skipMarkdownlint) {
|
|
598
600
|
const baseResult = yield* handleBaseMarkdownlint(root).pipe(Effect.result);
|
|
599
|
-
if (Result.isSuccess(baseResult)) yield*
|
|
601
|
+
if (Result.isSuccess(baseResult)) yield* Output.ok(baseResult.success);
|
|
600
602
|
else errors.push(baseResult.failure);
|
|
601
603
|
}
|
|
602
604
|
const mdlintResult = yield* handleChangesetMarkdownlint(changesetDir, root, force).pipe(Effect.result);
|
|
603
|
-
if (Result.isSuccess(mdlintResult)) yield*
|
|
605
|
+
if (Result.isSuccess(mdlintResult)) yield* Output.ok(mdlintResult.success);
|
|
604
606
|
else errors.push(mdlintResult.failure);
|
|
605
607
|
if (errors.length > 0) {
|
|
606
608
|
for (const err of errors) yield* Effect.logError(err.message);
|
|
607
|
-
if (!quiet)
|
|
609
|
+
if (!quiet) yield* CliExit.set(1);
|
|
608
610
|
return;
|
|
609
611
|
}
|
|
610
|
-
yield*
|
|
612
|
+
yield* Output.ok("Init complete");
|
|
611
613
|
}).pipe(Effect.catch((error) => Effect.gen(function* () {
|
|
612
614
|
if (!quiet) {
|
|
613
615
|
yield* Effect.logError(error instanceof InitError ? error.message : `Init failed: ${String(error)}`);
|
|
614
|
-
|
|
616
|
+
yield* CliExit.set(1);
|
|
615
617
|
}
|
|
616
618
|
})));
|
|
617
619
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { CliExit } from "@effected/cli";
|
|
3
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
4
5
|
import { Console, Effect } from "effect";
|
|
5
6
|
|
|
@@ -15,7 +16,7 @@ import { Console, Effect } from "effect";
|
|
|
15
16
|
* The command resolves the directory argument, delegates to
|
|
16
17
|
* {@link ChangesetLinter.validate}, and logs each {@link LintMessage} as a
|
|
17
18
|
* single colon-delimited line. When no errors are found and `--quiet` is not
|
|
18
|
-
* set, a success message is printed. Sets
|
|
19
|
+
* set, a success message is printed. Sets exit code 1 through `CliExit.set` when errors
|
|
19
20
|
* are found.
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
@@ -35,7 +36,7 @@ const quietOption = Flag.Boolean("quiet").pipe(Flag.withAlias("q"), Flag.withDes
|
|
|
35
36
|
* Run machine-readable lint validation on all changeset files in `dir`.
|
|
36
37
|
*
|
|
37
38
|
* Outputs one line per error in `file:line:col rule message` format. Sets
|
|
38
|
-
*
|
|
39
|
+
* exit code 1 through `CliExit.set` when one or more errors are found.
|
|
39
40
|
*
|
|
40
41
|
* @param dir - Path to the changeset directory (resolved relative to cwd)
|
|
41
42
|
* @param quiet - When `true`, suppress the "No lint errors found" message
|
|
@@ -49,7 +50,7 @@ function runLint(dir, quiet) {
|
|
|
49
50
|
const messages = yield* Effect.try(() => ChangesetLinter.validate(resolved));
|
|
50
51
|
for (const msg of messages) yield* Console.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
51
52
|
if (!quiet && messages.length === 0) yield* Console.log("No lint errors found.");
|
|
52
|
-
if (messages.length > 0)
|
|
53
|
+
if (messages.length > 0) yield* CliExit.set(1);
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
/* v8 ignore next 3 -- CLI registration; handler tested via runLint */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { requireValidConfig } from "../utils/config-gate.js";
|
|
2
3
|
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { CliExit } from "@effected/cli";
|
|
4
6
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
5
7
|
import { Effect } from "effect";
|
|
6
8
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
@@ -63,18 +65,18 @@ function runTransform(file, dryRun, check) {
|
|
|
63
65
|
const content = yield* Effect.try(() => readFileSync(resolved, "utf-8"));
|
|
64
66
|
const result = ChangelogTransformer.transformContent(content);
|
|
65
67
|
if (dryRun) {
|
|
66
|
-
yield*
|
|
68
|
+
yield* Output.line(result);
|
|
67
69
|
return;
|
|
68
70
|
}
|
|
69
71
|
if (check) {
|
|
70
72
|
if (result !== content) {
|
|
71
|
-
yield*
|
|
72
|
-
|
|
73
|
-
} else yield*
|
|
73
|
+
yield* Output.warn(`${resolved} would be modified by transform`);
|
|
74
|
+
yield* CliExit.set(1);
|
|
75
|
+
} else yield* Output.ok(`${resolved} is already formatted`);
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
78
|
yield* Effect.try(() => writeFileSync(resolved, result, "utf-8"));
|
|
77
|
-
yield*
|
|
79
|
+
yield* Output.ok(`Transformed ${resolved}`);
|
|
78
80
|
});
|
|
79
81
|
}
|
|
80
82
|
/* v8 ignore next 5 -- CLI registration; handler tested via runTransform */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { Argument, Command } from "effect/unstable/cli";
|
|
3
|
+
import { CliExit } from "@effected/cli";
|
|
2
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
5
|
import { Effect } from "effect";
|
|
4
6
|
|
|
@@ -24,7 +26,7 @@ const fileArg = Argument.File("file");
|
|
|
24
26
|
* Run lint validation on a single changeset file.
|
|
25
27
|
*
|
|
26
28
|
* Outputs one line per error in `file:line:col rule message` format.
|
|
27
|
-
* Logs "Valid." when the file passes. Sets
|
|
29
|
+
* Logs "Valid." when the file passes. Sets exit code 1 through `CliExit.set`
|
|
28
30
|
* when errors are found or the file cannot be read.
|
|
29
31
|
*
|
|
30
32
|
* @param filePath - Path to the changeset `.md` file
|
|
@@ -35,14 +37,14 @@ const fileArg = Argument.File("file");
|
|
|
35
37
|
function runValidateFile(filePath) {
|
|
36
38
|
return Effect.gen(function* () {
|
|
37
39
|
const result = yield* Effect.try(() => ChangesetLinter.validateFile(filePath)).pipe(Effect.catch((error) => Effect.gen(function* () {
|
|
38
|
-
yield* Effect.
|
|
39
|
-
|
|
40
|
+
yield* Effect.logError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
yield* CliExit.set(1);
|
|
40
42
|
return null;
|
|
41
43
|
})));
|
|
42
44
|
if (result === null) return;
|
|
43
|
-
for (const msg of result) yield*
|
|
44
|
-
if (result.length > 0)
|
|
45
|
-
else yield*
|
|
45
|
+
for (const msg of result) yield* Output.line(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
46
|
+
if (result.length > 0) yield* CliExit.set(1);
|
|
47
|
+
else yield* Output.ok("Valid");
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
/* v8 ignore next 3 -- CLI registration; handler tested via runValidateFile */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Output } from "../../../internal/output.js";
|
|
1
2
|
import { requireValidConfig } from "../utils/config-gate.js";
|
|
2
3
|
import { Command, Flag } from "effect/unstable/cli";
|
|
3
4
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
@@ -29,13 +30,13 @@ function runVersion(dryRun) {
|
|
|
29
30
|
yield* requireValidConfig(cwd);
|
|
30
31
|
const result = yield* (yield* Changesets.ReleasePlanner).apply(cwd, { dryRun });
|
|
31
32
|
if (result.releases.length === 0) {
|
|
32
|
-
yield*
|
|
33
|
+
yield* Output.ok("No pending changesets");
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
const verb = dryRun ? "Would release" : "Released";
|
|
36
|
-
for (const r of result.releases) yield*
|
|
37
|
-
if (!dryRun) yield*
|
|
38
|
-
for (const u of result.versionFileUpdates) yield*
|
|
37
|
+
for (const r of result.releases) yield* Output.ok(`${verb} ${r.name}: ${r.oldVersion} -> ${r.newVersion} (${r.type})`);
|
|
38
|
+
if (!dryRun) yield* Output.detail(`Touched ${result.touchedFiles.length} file(s)`);
|
|
39
|
+
for (const u of result.versionFileUpdates) yield* Output.detail(`${dryRun ? "Would update" : "Updated"} ${u.filePath} -> ${u.version}`);
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
42
|
/* v8 ignore next 4 -- CLI registration; handler tested via runVersion */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
+
import { CliExit } from "@effected/cli";
|
|
2
3
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
4
|
import { Effect } from "effect";
|
|
4
5
|
import { existsSync } from "node:fs";
|
|
@@ -26,7 +27,7 @@ import { existsSync } from "node:fs";
|
|
|
26
27
|
* projects that have not yet been bootstrapped.
|
|
27
28
|
* - **Config present**: the gate invokes
|
|
28
29
|
* {@link ConfigInspector.inspect}. On `ConfigurationError`, it sets
|
|
29
|
-
*
|
|
30
|
+
* exit code 1 through `CliExit.set` and propagates the error so the caller's
|
|
30
31
|
* `Effect.gen` short-circuits.
|
|
31
32
|
*
|
|
32
33
|
* @internal
|
|
@@ -50,8 +51,7 @@ function requireValidConfig(cwd) {
|
|
|
50
51
|
const configPath = join(projectDir, ".changeset", "config.json");
|
|
51
52
|
if (!existsSync(configPath)) return;
|
|
52
53
|
yield* (yield* ConfigInspector).inspect(projectDir).pipe(Effect.catchTag("ConfigurationError", (err) => {
|
|
53
|
-
|
|
54
|
-
return Effect.fail(err);
|
|
54
|
+
return CliExit.set(1).pipe(Effect.andThen(Effect.fail(err)));
|
|
55
55
|
}));
|
|
56
56
|
});
|
|
57
57
|
}
|
package/commands/check.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Output } from "../internal/output.js";
|
|
1
2
|
import { runChangesetCheck } from "./changeset/commands/check.js";
|
|
2
3
|
import "./changeset/index.js";
|
|
3
4
|
import { runCommitCheck } from "./commit/check.js";
|
|
@@ -44,8 +45,8 @@ function runCheck(steps) {
|
|
|
44
45
|
return Effect.gen(function* () {
|
|
45
46
|
const results = yield* Effect.all([
|
|
46
47
|
Effect.result(steps.changeset),
|
|
47
|
-
Effect.result(steps.commit),
|
|
48
|
-
Effect.result(steps.lint)
|
|
48
|
+
Output.line("").pipe(Effect.andThen(Effect.result(steps.commit))),
|
|
49
|
+
Output.line("").pipe(Effect.andThen(Effect.result(steps.lint)))
|
|
49
50
|
], { concurrency: 1 });
|
|
50
51
|
for (const result of results) if (Result.isFailure(result)) return yield* Effect.fail(result.failure);
|
|
51
52
|
});
|
package/commands/clean.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Output } from "../internal/output.js";
|
|
1
2
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
3
|
import { join, sep } from "node:path";
|
|
3
4
|
import { Data, Effect } from "effect";
|
|
@@ -97,10 +98,6 @@ function removeTargets(targets, dryRun) {
|
|
|
97
98
|
};
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
|
-
/** Unicode symbols for output. */
|
|
101
|
-
const CHECK_MARK = "✓";
|
|
102
|
-
const BULLET = "•";
|
|
103
|
-
const WARN_MARK = "⚠";
|
|
104
101
|
/** Split the comma-separated `--globs` value; fall back to defaults when empty. */
|
|
105
102
|
function parseGlobs(raw) {
|
|
106
103
|
const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -147,14 +144,16 @@ function runClean(opts) {
|
|
|
147
144
|
}))));
|
|
148
145
|
for (const { g, report } of reports) {
|
|
149
146
|
if (g.targets.length === 0) continue;
|
|
150
|
-
yield*
|
|
151
|
-
|
|
152
|
-
for (const
|
|
147
|
+
yield* Output.line("");
|
|
148
|
+
yield* Output.heading(g.pkg.relativePath === "." ? "<root>" : g.pkg.relativePath);
|
|
149
|
+
for (const t of report.removed) yield* Output.detail(`${verb} [${t.kind}] ${t.path}`);
|
|
150
|
+
for (const f of report.failed) yield* Output.warn(`failed [${f.target.kind}] ${f.target.path}: ${f.reason}`);
|
|
153
151
|
total += report.removed.length;
|
|
154
152
|
failures.push(...report.failed);
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
|
-
yield*
|
|
155
|
+
yield* Output.line("");
|
|
156
|
+
yield* Output.ok(`${opts.dryRun ? "Would remove" : "Removed"} ${total} item(s)`);
|
|
158
157
|
if (failures.length > 0) {
|
|
159
158
|
for (const f of failures) yield* Effect.logError(`Failed to remove ${f.target.path}: ${f.reason}`);
|
|
160
159
|
return yield* Effect.fail(new CleanError({
|
package/commands/commit/check.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Output } from "../../internal/output.js";
|
|
1
2
|
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
2
3
|
import { SECTION_DEF, savvyCommitBlock } from "./init.js";
|
|
3
4
|
import { ChangesetConfigReader, Commitlint, SavvyBaseSection, SavvyHooksSection, SavvyToolchainSection, savvyBasePreamble, savvyHooksHygiene, savvyInstallBlock, savvyToolchainCheck } from "@savvy-web/silk-effects";
|
|
@@ -6,10 +7,6 @@ import { VersioningStrategy } from "@effected/workspaces";
|
|
|
6
7
|
import { CheckOutcome, ManagedSection } from "@effected/templates";
|
|
7
8
|
|
|
8
9
|
//#region src/commands/commit/check.ts
|
|
9
|
-
/** Unicode cross symbol. */
|
|
10
|
-
const CROSS_MARK = "✗";
|
|
11
|
-
/** Unicode bullet symbol. */
|
|
12
|
-
const BULLET = "•";
|
|
13
10
|
/** Possible commitlint configuration file names, in priority order. */
|
|
14
11
|
const CONFIG_FILES = [
|
|
15
12
|
"commitlint.config.ts",
|
|
@@ -98,41 +95,41 @@ function runCommitCheck() {
|
|
|
98
95
|
return Effect.gen(function* () {
|
|
99
96
|
const fs = yield* FileSystem.FileSystem;
|
|
100
97
|
const ms = yield* ManagedSection;
|
|
101
|
-
yield*
|
|
98
|
+
yield* Output.heading("commitlint configuration");
|
|
102
99
|
const foundConfig = yield* findConfigFile(fs);
|
|
103
|
-
if (foundConfig) yield*
|
|
104
|
-
else yield*
|
|
100
|
+
if (foundConfig) yield* Output.ok(`Config file: ${foundConfig}`);
|
|
101
|
+
else yield* Output.fail(`No commitlint config file found`);
|
|
105
102
|
const hasHuskyHook = yield* fs.exists(HUSKY_HOOK_PATH);
|
|
106
|
-
if (hasHuskyHook) yield*
|
|
107
|
-
else yield*
|
|
103
|
+
if (hasHuskyHook) yield* Output.ok(`Husky hook: ${HUSKY_HOOK_PATH}`);
|
|
104
|
+
else yield* Output.fail(`No husky commit-msg hook found`);
|
|
108
105
|
let sectionsHealthy = true;
|
|
109
106
|
if (hasHuskyHook) {
|
|
110
107
|
const baseStatus = yield* ms.check(HUSKY_HOOK_PATH, SavvyBaseSection.section(savvyBasePreamble()));
|
|
111
|
-
if (CheckOutcome.$is("UpToDate")(baseStatus)) yield*
|
|
108
|
+
if (CheckOutcome.$is("UpToDate")(baseStatus)) yield* Output.ok(`Base section: up-to-date`);
|
|
112
109
|
else if (CheckOutcome.$is("Drifted")(baseStatus)) {
|
|
113
110
|
sectionsHealthy = false;
|
|
114
|
-
yield*
|
|
111
|
+
yield* Output.warn(`Base section: outdated (run 'savvy init' to update)`);
|
|
115
112
|
} else {
|
|
116
113
|
sectionsHealthy = false;
|
|
117
|
-
yield*
|
|
114
|
+
yield* Output.skip(`Base section: not found (run 'savvy init' to add)`);
|
|
118
115
|
}
|
|
119
116
|
const block = yield* ms.read(HUSKY_HOOK_PATH, SECTION_DEF);
|
|
120
117
|
if (Option.isSome(block)) {
|
|
121
118
|
const configPath = extractConfigPathFromManaged(block.value.content);
|
|
122
119
|
if (configPath) {
|
|
123
120
|
const status = yield* ms.check(HUSKY_HOOK_PATH, savvyCommitBlock(configPath));
|
|
124
|
-
if (CheckOutcome.$is("UpToDate")(status)) yield*
|
|
121
|
+
if (CheckOutcome.$is("UpToDate")(status)) yield* Output.ok(`Commit section: up-to-date`);
|
|
125
122
|
else {
|
|
126
123
|
sectionsHealthy = false;
|
|
127
|
-
yield*
|
|
124
|
+
yield* Output.warn(`Commit section: outdated (run 'savvy init' to update)`);
|
|
128
125
|
}
|
|
129
126
|
} else {
|
|
130
127
|
sectionsHealthy = false;
|
|
131
|
-
yield*
|
|
128
|
+
yield* Output.warn(`Commit section: outdated (run 'savvy init' to update)`);
|
|
132
129
|
}
|
|
133
130
|
} else {
|
|
134
131
|
sectionsHealthy = false;
|
|
135
|
-
yield*
|
|
132
|
+
yield* Output.skip(`Commit section: not found (run 'savvy init' to add)`);
|
|
136
133
|
}
|
|
137
134
|
}
|
|
138
135
|
for (const hookPath of [
|
|
@@ -142,51 +139,52 @@ function runCommitCheck() {
|
|
|
142
139
|
]) {
|
|
143
140
|
if (!(yield* fs.exists(hookPath))) {
|
|
144
141
|
sectionsHealthy = false;
|
|
145
|
-
yield*
|
|
142
|
+
yield* Output.skip(`Hygiene hook: ${hookPath} not found (run 'savvy init' to add)`);
|
|
146
143
|
continue;
|
|
147
144
|
}
|
|
148
145
|
const hygieneStatus = yield* ms.check(hookPath, SavvyHooksSection.section(savvyHooksHygiene()));
|
|
149
|
-
if (CheckOutcome.$is("UpToDate")(hygieneStatus)) yield*
|
|
146
|
+
if (CheckOutcome.$is("UpToDate")(hygieneStatus)) yield* Output.ok(`Hygiene hook: ${hookPath}`);
|
|
150
147
|
else if (CheckOutcome.$is("Drifted")(hygieneStatus)) {
|
|
151
148
|
sectionsHealthy = false;
|
|
152
|
-
yield*
|
|
149
|
+
yield* Output.warn(`Hygiene hook: ${hookPath} outdated (run 'savvy init' to update)`);
|
|
153
150
|
} else {
|
|
154
151
|
sectionsHealthy = false;
|
|
155
|
-
yield*
|
|
152
|
+
yield* Output.skip(`Hygiene hook: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
156
153
|
}
|
|
157
154
|
if (hookPath === ".husky/post-commit") continue;
|
|
158
155
|
const installHook = hookPath === ".husky/post-checkout" ? "post-checkout" : "post-merge";
|
|
159
156
|
const installStatus = yield* ms.check(hookPath, savvyInstallBlock(installHook));
|
|
160
|
-
if (CheckOutcome.$is("UpToDate")(installStatus)) yield*
|
|
157
|
+
if (CheckOutcome.$is("UpToDate")(installStatus)) yield* Output.ok(`Dependency install: ${hookPath}`);
|
|
161
158
|
else if (CheckOutcome.$is("Drifted")(installStatus)) {
|
|
162
159
|
sectionsHealthy = false;
|
|
163
|
-
yield*
|
|
160
|
+
yield* Output.warn(`Dependency install: ${hookPath} outdated (run 'savvy init' to update)`);
|
|
164
161
|
} else {
|
|
165
162
|
sectionsHealthy = false;
|
|
166
|
-
yield*
|
|
163
|
+
yield* Output.skip(`Dependency install: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
167
164
|
}
|
|
168
165
|
const toolchainStatus = yield* ms.check(hookPath, SavvyToolchainSection.section(savvyToolchainCheck()));
|
|
169
|
-
if (CheckOutcome.$is("UpToDate")(toolchainStatus)) yield*
|
|
166
|
+
if (CheckOutcome.$is("UpToDate")(toolchainStatus)) yield* Output.ok(`Toolchain check: ${hookPath}`);
|
|
170
167
|
else if (CheckOutcome.$is("Drifted")(toolchainStatus)) {
|
|
171
168
|
sectionsHealthy = false;
|
|
172
|
-
yield*
|
|
169
|
+
yield* Output.warn(`Toolchain check: ${hookPath} outdated (run 'savvy init' to update)`);
|
|
173
170
|
} else {
|
|
174
171
|
sectionsHealthy = false;
|
|
175
|
-
yield*
|
|
172
|
+
yield* Output.skip(`Toolchain check: ${hookPath} section not found (run 'savvy init' to add)`);
|
|
176
173
|
}
|
|
177
174
|
}
|
|
178
|
-
if (yield* fs.exists(DCO_FILE_PATH)) yield*
|
|
179
|
-
else yield*
|
|
180
|
-
yield*
|
|
181
|
-
yield*
|
|
175
|
+
if (yield* fs.exists(DCO_FILE_PATH)) yield* Output.ok(`DCO file: ${DCO_FILE_PATH}`);
|
|
176
|
+
else yield* Output.skip(`No DCO file (signoff not required)`);
|
|
177
|
+
yield* Output.line("");
|
|
178
|
+
yield* Output.heading("Detected settings");
|
|
179
|
+
yield* Output.detail(`DCO required: ${Commitlint.detectDCO()}`);
|
|
182
180
|
const releaseFormat = yield* detectReleaseFormat;
|
|
183
|
-
yield*
|
|
181
|
+
yield* Output.detail(`Release format: ${releaseFormat}`);
|
|
184
182
|
const scopes = yield* Effect.catch(Commitlint.detectScopes, () => Effect.succeed([]));
|
|
185
183
|
const scopeDisplay = scopes.length > 0 ? scopes.join(", ") : "(none - not a monorepo or no packages found)";
|
|
186
|
-
yield*
|
|
187
|
-
yield*
|
|
188
|
-
if (!foundConfig || !hasHuskyHook || !sectionsHealthy) yield*
|
|
189
|
-
else yield*
|
|
184
|
+
yield* Output.detail(`Detected scopes: ${scopeDisplay}`);
|
|
185
|
+
yield* Output.line("");
|
|
186
|
+
if (!foundConfig || !hasHuskyHook || !sectionsHealthy) yield* Output.fail(`Commitlint needs configuration. Run: savvy init`);
|
|
187
|
+
else yield* Output.ok(`Commitlint is configured correctly.`);
|
|
190
188
|
});
|
|
191
189
|
}
|
|
192
190
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
//#region src/commands/commit/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared constants for CLI commands.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
2
7
|
/** Husky commit-msg hook path (savvy-base + savvy-commit sections). */
|
|
3
8
|
const HUSKY_HOOK_PATH = ".husky/commit-msg";
|
|
4
9
|
/** Husky post-checkout hook path (savvy-hooks hygiene). */
|