@savvy-web/cli 3.4.3 → 3.5.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/README.md +1 -1
- package/commands/changeset/commands/deps-regen.js +45 -9
- package/commands/lint/index.js +2 -1
- package/commands/lint/text.js +71 -0
- package/index.d.ts +1 -1
- package/package.json +4 -4
- package/version.js +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npx savvy clean --globs dist,.turbo,coverage
|
|
|
54
54
|
- `savvy clean` — removes build and cache artifacts (`dist`, `.turbo`, `coverage`, `node_modules`, `.rslib` by default) from every workspace package (leaves first) and the repo root (last); `--globs` to customize, `--dry-run` to preview.
|
|
55
55
|
- `savvy commit` — the husky/Claude hook handlers (session-start, pre-commit-message, post-commit-verify).
|
|
56
56
|
- `savvy changeset` — changeset lint, check, transform, version, config validation, and dependency changesets.
|
|
57
|
-
- `savvy lint` — formatters for package.json, the pnpm workspace file and YAML.
|
|
57
|
+
- `savvy lint` — formatters for package.json, the pnpm workspace file and YAML, plus `savvy lint text`, which fails files that contain a NUL byte or invalid UTF-8 (`--staged` checks the git index copy).
|
|
58
58
|
- `savvy repos` — the vendored reference repos declared in `.repos/config.json`: `status` (with `--drift`), `sync`, `pin`, `add`, `note`, `remove`, `rename` and `restore`.
|
|
59
59
|
|
|
60
60
|
## Vendored repos are read-only
|
|
@@ -39,6 +39,16 @@ import { Console, Effect, Option } from "effect";
|
|
|
39
39
|
* (the regen default) and protocol specifiers (`catalog:`/`workspace:`)
|
|
40
40
|
* are resolved to concrete versions.
|
|
41
41
|
*
|
|
42
|
+
* **Dry-run reporting.** With `--dry-run` nothing is written or deleted, so
|
|
43
|
+
* the human output is phrased as a plan (`Would delete N pure dependency
|
|
44
|
+
* changeset(s):` / `Would write N dependency changeset(s):`) rather than the
|
|
45
|
+
* real run's `✓ Deleted …` / `✓ Wrote …`. A real run reports what
|
|
46
|
+
* `execute()` returned, not what the plan listed: deletes are tolerant, so a
|
|
47
|
+
* planned delete that found nothing or could not remove the file is reported
|
|
48
|
+
* as not removed. `--json`
|
|
49
|
+
* emits the plan's fields plus an explicit `dryRun` boolean in both modes,
|
|
50
|
+
* and a real run adds `execute()`'s `result`.
|
|
51
|
+
*
|
|
42
52
|
* @example
|
|
43
53
|
* ```bash
|
|
44
54
|
* savvy changeset deps regen
|
|
@@ -69,22 +79,48 @@ function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
|
69
79
|
...Option.isSome(base) ? { base: base.value } : {},
|
|
70
80
|
...Option.isSome(pkg) ? { package: pkg.value } : {}
|
|
71
81
|
}).pipe(Effect.tapError(() => CliExit.set(1)));
|
|
72
|
-
|
|
73
|
-
if (json) yield* Console.log(JSON.stringify(
|
|
74
|
-
|
|
82
|
+
const result = dryRun ? void 0 : yield* service.execute(plan);
|
|
83
|
+
if (json) yield* Console.log(JSON.stringify({
|
|
84
|
+
...plan,
|
|
85
|
+
dryRun,
|
|
86
|
+
...result ? { result } : {}
|
|
87
|
+
}, null, 2));
|
|
88
|
+
else yield* renderHumanPlan(plan, result);
|
|
75
89
|
});
|
|
76
90
|
}
|
|
77
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Render the plan for a person. A dry run (no `result`) changed nothing, so its
|
|
93
|
+
* headings are plan-phrased and carry no `✓`. A real run reports what
|
|
94
|
+
* `execute` actually did: deletes are tolerant, so a planned delete absent from
|
|
95
|
+
* `result.deleted` was already gone or could not be removed, and is reported as
|
|
96
|
+
* not removed, never as deleted.
|
|
97
|
+
*/
|
|
98
|
+
function renderHumanPlan(plan, result) {
|
|
78
99
|
return Effect.gen(function* () {
|
|
100
|
+
const dryRun = result === void 0;
|
|
79
101
|
if (plan.toDelete.length === 0 && plan.toWrite.length === 0) yield* Output.ok("No dependency changes to regenerate");
|
|
80
102
|
else {
|
|
81
|
-
if (plan.toDelete.length > 0) {
|
|
82
|
-
yield* Output.
|
|
103
|
+
if (dryRun && plan.toDelete.length > 0) {
|
|
104
|
+
yield* Output.heading(`Would delete ${plan.toDelete.length} pure dependency changeset(s):`);
|
|
83
105
|
for (const entry of plan.toDelete) yield* Output.detail(`${entry.file} (${entry.package})`);
|
|
84
106
|
}
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
if (!dryRun) {
|
|
108
|
+
const deleted = new Set(result.deleted);
|
|
109
|
+
const removed = plan.toDelete.filter((entry) => deleted.has(entry.file));
|
|
110
|
+
const notRemoved = plan.toDelete.filter((entry) => !deleted.has(entry.file));
|
|
111
|
+
if (removed.length > 0) {
|
|
112
|
+
yield* Output.ok(`Deleted ${removed.length} pure dependency changeset(s):`);
|
|
113
|
+
for (const entry of removed) yield* Output.detail(`${entry.file} (${entry.package})`);
|
|
114
|
+
}
|
|
115
|
+
if (notRemoved.length > 0) {
|
|
116
|
+
yield* Output.skip(`${notRemoved.length} planned deletion(s) not removed (already gone or undeletable):`);
|
|
117
|
+
for (const entry of notRemoved) yield* Output.detail(`${entry.file} (${entry.package})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const writes = dryRun ? plan.toWrite : plan.toWrite.filter((entry) => result.written.includes(entry.file));
|
|
121
|
+
if (writes.length > 0) {
|
|
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"})`);
|
|
88
124
|
}
|
|
89
125
|
}
|
|
90
126
|
if (plan.skippedMixed.length > 0) {
|
package/commands/lint/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { runLintCheck } from "./check.js";
|
|
2
2
|
import { runLintInit } from "./init.js";
|
|
3
3
|
import { fmtCommand } from "./fmt.js";
|
|
4
|
+
import { runLintText, textCommand } from "./text.js";
|
|
4
5
|
import { Command } from "effect/cli";
|
|
5
6
|
|
|
6
7
|
//#region src/commands/lint/index.ts
|
|
7
8
|
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
|
8
|
-
const _lintCommand = Command.make("lint").pipe(Command.withSubcommands([fmtCommand]), Command.withDescription("Code-quality: lint-staged config, checks, and in-place formatting"));
|
|
9
|
+
const _lintCommand = Command.make("lint").pipe(Command.withSubcommands([fmtCommand, textCommand]), Command.withDescription("Code-quality: lint-staged config, checks, and in-place formatting"));
|
|
9
10
|
/**
|
|
10
11
|
* The `savvy lint` command group for use in Task B7 root assembly.
|
|
11
12
|
*
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Output } from "../../internal/output.js";
|
|
2
|
+
import { Argument, Command, Flag } from "effect/cli";
|
|
3
|
+
import { CliExit } from "@effected/cli";
|
|
4
|
+
import { Lint } from "@savvy-web/silk-effects";
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/lint/text.ts
|
|
8
|
+
/**
|
|
9
|
+
* `savvy lint text` — fail when a source file is not grep-visible text.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* A NUL byte or invalid UTF-8 makes `grep`/`rg` silently skip a file, so a
|
|
13
|
+
* search returns a false negative instead of an error (savvy-web/systems#388).
|
|
14
|
+
* With file arguments the command checks those files (the lint-staged path,
|
|
15
|
+
* `Lint.TextFiles.create`); with none it checks every git-tracked file matching
|
|
16
|
+
* `Lint.TextFiles.glob`. `--staged` reads each file's bytes from the git
|
|
17
|
+
* index instead of the working tree — what the lint-staged handler passes, so
|
|
18
|
+
* a concurrent Biome `--write` on the worktree cannot race the read, and the
|
|
19
|
+
* check sees exactly what the commit records. The classification itself lives in silk-effects, so
|
|
20
|
+
* this command and the lint-staged handler cannot drift.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```bash
|
|
24
|
+
* savvy lint text # every tracked source/text file
|
|
25
|
+
* savvy lint text src/a.ts docs/b.md
|
|
26
|
+
* savvy lint text --staged src/a.ts # the index copy, as lint-staged runs it
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @internal
|
|
30
|
+
*/
|
|
31
|
+
/* v8 ignore start -- CLI option definitions */
|
|
32
|
+
const filesArg = Argument.File("files", { mustExist: true }).pipe(Argument.withDescription("Files to check; omit to check every git-tracked source/text file"), Argument.variadic());
|
|
33
|
+
const stagedFlag = Flag.Boolean("staged").pipe(Flag.withDescription("Read each file's staged content from the git index instead of the working tree"), Flag.withDefault(false));
|
|
34
|
+
/**
|
|
35
|
+
* Check `files` (or, when empty, every tracked file under `cwd`) and print one
|
|
36
|
+
* line per finding; any finding sets exit code 1. Under `--staged`, every file
|
|
37
|
+
* is still checked when one has no index entry: that file's read error is
|
|
38
|
+
* logged to stderr, counted as failed, and also sets exit code 1. Exported for
|
|
39
|
+
* tests.
|
|
40
|
+
*
|
|
41
|
+
* @internal
|
|
42
|
+
*/
|
|
43
|
+
const runLintText = (files, options = {}) => Effect.gen(function* () {
|
|
44
|
+
const staged = options.staged ?? false;
|
|
45
|
+
const targets = files.length > 0 ? files : yield* Lint.TextFiles.listTracked(options.cwd ?? ".", { staged });
|
|
46
|
+
const { findings, unreadable } = staged ? yield* Lint.TextFiles.checkStagedFiles(targets) : {
|
|
47
|
+
findings: yield* Lint.TextFiles.checkFiles(targets),
|
|
48
|
+
unreadable: []
|
|
49
|
+
};
|
|
50
|
+
for (const error of unreadable) yield* Effect.logError(error.message);
|
|
51
|
+
if (findings.length === 0 && unreadable.length === 0) {
|
|
52
|
+
yield* Output.ok(`${targets.length} ${targets.length === 1 ? "file is" : "files are"} grep-visible text`);
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
for (const finding of findings) yield* Output.fail(`${finding.location} ${finding.message}`);
|
|
56
|
+
const failedFiles = (/* @__PURE__ */ new Set([...findings.map((finding) => finding.path), ...unreadable.map((error) => error.path)])).size;
|
|
57
|
+
yield* Output.summary({
|
|
58
|
+
ok: targets.length - failedFiles,
|
|
59
|
+
fail: failedFiles
|
|
60
|
+
});
|
|
61
|
+
yield* CliExit.set(1);
|
|
62
|
+
return findings;
|
|
63
|
+
});
|
|
64
|
+
/** The `savvy lint text` subcommand. */
|
|
65
|
+
const textCommand = Command.make("text", {
|
|
66
|
+
files: filesArg,
|
|
67
|
+
staged: stagedFlag
|
|
68
|
+
}, ({ files, staged }) => runLintText(files, { staged })).pipe(Command.withDescription("Fail when a source file contains a NUL byte or is not valid UTF-8 (grep-invisible)"));
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { runLintText, textCommand };
|
package/index.d.ts
CHANGED
|
@@ -206,7 +206,7 @@ export declare function runLintInit(opts: {
|
|
|
206
206
|
* errors from Effect's internal types. Task B7 should import and use this directly
|
|
207
207
|
* as `Command.withSubcommands([lintCommand])` — the cast is for declaration emit only.
|
|
208
208
|
*/
|
|
209
|
-
export declare const lintCommand: Command.Command<"lint", {}, {}, import("@effected/yaml").YamlParseError,
|
|
209
|
+
export declare const lintCommand: Command.Command<"lint", {}, {}, import("@effected/git").GitCommandError | import("@effected/git").NotARepositoryError | import("effect/PlatformError").PlatformError | import("@effected/git").UnknownRefError | import("@effected/yaml").YamlParseError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("@effected/cli").CliExit | import("effect/FileSystem").FileSystem | import("@effected/git").Git | import("effect/Path").Path | import("effect/Stdio").Stdio>;
|
|
210
210
|
//#endregion
|
|
211
211
|
//#region src/commands/repos/commands/status.d.ts
|
|
212
212
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The savvy CLI — unified commit, changeset, and lint commands for the Silk Suite",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/systems/tree/main/packages/cli",
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
"@effected/cli": "^0.10.0",
|
|
41
41
|
"@effected/commands": "^0.10.0",
|
|
42
42
|
"@effected/engine": "^0.2.0",
|
|
43
|
-
"@effected/git": "^0.
|
|
43
|
+
"@effected/git": "^0.19.0",
|
|
44
44
|
"@effected/jsonc": "^0.14.0",
|
|
45
45
|
"@effected/templates": "^0.9.1",
|
|
46
|
-
"@effected/workspaces": "^0.30.
|
|
46
|
+
"@effected/workspaces": "^0.30.2",
|
|
47
47
|
"@effected/yaml": "^0.18.0",
|
|
48
|
-
"@savvy-web/silk-effects": "9.
|
|
48
|
+
"@savvy-web/silk-effects": "9.3.0",
|
|
49
49
|
"effect": "4.0.0-rc.118"
|
|
50
50
|
}
|
|
51
51
|
}
|