@savvy-web/cli 1.6.1 → 2.0.1
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 +62 -88
- package/commands/changeset/commands/check.js +2 -2
- package/commands/changeset/commands/config-validate.js +2 -2
- package/commands/changeset/commands/deps-detect.js +12 -18
- package/commands/changeset/commands/deps-regen.js +12 -18
- package/commands/changeset/commands/init.js +39 -40
- package/commands/changeset/commands/lint.js +3 -3
- package/commands/changeset/commands/transform.js +4 -4
- package/commands/changeset/commands/validate-file.js +3 -3
- package/commands/changeset/commands/version.js +2 -2
- package/commands/changeset/index.js +7 -4
- package/commands/check.js +12 -12
- package/commands/clean.js +9 -9
- package/commands/commit/check.js +5 -6
- package/commands/commit/hook.js +1 -1
- package/commands/commit/hooks/post-commit-verify.js +26 -46
- package/commands/commit/hooks/pre-commit-message.js +1 -1
- package/commands/commit/hooks/session-start.js +1 -1
- package/commands/commit/index.js +1 -1
- package/commands/commit/init.js +1 -2
- package/commands/init.js +6 -6
- package/commands/lint/check.js +15 -13
- package/commands/lint/fmt.js +12 -14
- package/commands/lint/index.js +1 -1
- package/commands/lint/init.js +7 -8
- package/commands/repos/commands/add.js +7 -7
- package/commands/repos/commands/note.js +19 -20
- package/commands/repos/commands/pin.js +4 -4
- package/commands/repos/commands/status.js +3 -3
- package/commands/repos/commands/sync.js +2 -2
- package/commands/repos/index.js +7 -4
- package/index.d.ts +62 -38
- package/package.json +10 -18
|
@@ -7,7 +7,7 @@ import { transformCommand } from "./commands/transform.js";
|
|
|
7
7
|
import { validateFileCommand } from "./commands/validate-file.js";
|
|
8
8
|
import { versionCommand } from "./commands/version.js";
|
|
9
9
|
import { runChangesetInit } from "./commands/init.js";
|
|
10
|
-
import { Command } from "
|
|
10
|
+
import { Command } from "effect/unstable/cli";
|
|
11
11
|
|
|
12
12
|
//#region src/commands/changeset/index.ts
|
|
13
13
|
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
|
@@ -26,9 +26,12 @@ const _changesetCommand = Command.make("changeset").pipe(Command.withSubcommands
|
|
|
26
26
|
* The `savvy changeset` command group for use in Task B7 root assembly.
|
|
27
27
|
*
|
|
28
28
|
* @remarks
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Annotated with the exact five-parameter `Command.Command` instantiation
|
|
30
|
+
* (verified against the inferred type): the bare inferred type additionally
|
|
31
|
+
* prints the structural `subcommands` tree, whose inline command types
|
|
32
|
+
* reference effect's non-exported `Inspectable` module (TS4023). The
|
|
33
|
+
* annotation preserves the exact Error/Requirements channels, so the root
|
|
34
|
+
* layer graph stays compiler-validated.
|
|
32
35
|
*/
|
|
33
36
|
const changesetCommand = _changesetCommand;
|
|
34
37
|
|
package/commands/check.js
CHANGED
|
@@ -2,8 +2,8 @@ import { runChangesetCheck } from "./changeset/commands/check.js";
|
|
|
2
2
|
import "./changeset/index.js";
|
|
3
3
|
import { runCommitCheck } from "./commit/check.js";
|
|
4
4
|
import { runLintCheck } from "./lint/check.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Effect, Result } from "effect";
|
|
6
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
7
7
|
|
|
8
8
|
//#region src/commands/check.ts
|
|
9
9
|
/**
|
|
@@ -25,8 +25,8 @@ import { Effect } from "effect";
|
|
|
25
25
|
* @internal
|
|
26
26
|
*/
|
|
27
27
|
/* v8 ignore start -- CLI option definitions; orchestration logic tested via runCheck */
|
|
28
|
-
const changesetDirOption =
|
|
29
|
-
const quietOption =
|
|
28
|
+
const changesetDirOption = Flag.string("changeset-dir").pipe(Flag.withDescription("Path to the changeset directory"), Flag.withDefault(".changeset"));
|
|
29
|
+
const quietOption = Flag.boolean("quiet").pipe(Flag.withAlias("q"), Flag.withDescription("Only output warnings from lint check"), Flag.withDefault(false));
|
|
30
30
|
/* v8 ignore stop */
|
|
31
31
|
/**
|
|
32
32
|
* Run all three check step Effects without short-circuiting.
|
|
@@ -41,14 +41,14 @@ const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Option
|
|
|
41
41
|
* union of all failing steps' errors.
|
|
42
42
|
*/
|
|
43
43
|
function runCheck(steps) {
|
|
44
|
-
return Effect.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
concurrency: 1
|
|
50
|
-
|
|
51
|
-
})
|
|
44
|
+
return Effect.gen(function* () {
|
|
45
|
+
const results = yield* Effect.all([
|
|
46
|
+
Effect.result(steps.changeset),
|
|
47
|
+
Effect.result(steps.commit),
|
|
48
|
+
Effect.result(steps.lint)
|
|
49
|
+
], { concurrency: 1 });
|
|
50
|
+
for (const result of results) if (Result.isFailure(result)) return yield* Effect.fail(result.failure);
|
|
51
|
+
});
|
|
52
52
|
}
|
|
53
53
|
/* v8 ignore start -- CLI registration; orchestration logic tested via runCheck */
|
|
54
54
|
const _checkCommand = Command.make("check", {
|
package/commands/clean.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
2
2
|
import { Data, Effect } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
4
4
|
import { join, sep } from "node:path";
|
|
5
5
|
import { glob, realpath, rm } from "node:fs/promises";
|
|
6
6
|
|
|
@@ -113,12 +113,12 @@ function parseGlobs(raw) {
|
|
|
113
113
|
function runClean(opts) {
|
|
114
114
|
const patterns = parseGlobs(opts.globs);
|
|
115
115
|
return Effect.gen(function* () {
|
|
116
|
-
const packages = yield* (yield* WorkspaceDiscovery).listPackages(
|
|
116
|
+
const packages = yield* (yield* WorkspaceDiscovery).listPackages().pipe(Effect.mapError((e) => new CleanError({
|
|
117
117
|
step: "discover workspaces",
|
|
118
118
|
reason: e.message
|
|
119
119
|
})));
|
|
120
|
-
const leaves = packages.filter((p) => !p.
|
|
121
|
-
const roots = packages.filter((p) => p.
|
|
120
|
+
const leaves = packages.filter((p) => !(p.relativePath === "."));
|
|
121
|
+
const roots = packages.filter((p) => p.relativePath === ".");
|
|
122
122
|
const ordered = [...leaves, ...roots];
|
|
123
123
|
const planned = yield* Effect.forEach(ordered, (pkg) => collectTargets(pkg.path, patterns).pipe(Effect.map((targets) => ({
|
|
124
124
|
pkg,
|
|
@@ -135,8 +135,8 @@ function runClean(opts) {
|
|
|
135
135
|
})
|
|
136
136
|
};
|
|
137
137
|
});
|
|
138
|
-
const leafGroups = groups.filter((g) => !g.pkg.
|
|
139
|
-
const rootGroups = groups.filter((g) => g.pkg.
|
|
138
|
+
const leafGroups = groups.filter((g) => !(g.pkg.relativePath === "."));
|
|
139
|
+
const rootGroups = groups.filter((g) => g.pkg.relativePath === ".");
|
|
140
140
|
const verb = opts.dryRun ? "would remove" : "removed";
|
|
141
141
|
let total = 0;
|
|
142
142
|
const failures = [];
|
|
@@ -165,8 +165,8 @@ function runClean(opts) {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
/* v8 ignore start -- CLI option/registration; orchestration tested via runClean */
|
|
168
|
-
const globsOption =
|
|
169
|
-
const dryRunOption =
|
|
168
|
+
const globsOption = Flag.string("globs").pipe(Flag.withAlias("g"), Flag.withDescription(`Comma-separated glob patterns to remove from each workspace root (default: ${DEFAULT_GLOBS.join(",")})`), Flag.withDefault(DEFAULT_GLOBS.join(",")));
|
|
169
|
+
const dryRunOption = Flag.boolean("dry-run").pipe(Flag.withAlias("n"), Flag.withDescription("Report what would be removed without deleting anything"), Flag.withDefault(false));
|
|
170
170
|
const _cleanCommand = Command.make("clean", {
|
|
171
171
|
globs: globsOption,
|
|
172
172
|
dryRun: dryRunOption
|
package/commands/commit/check.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
2
2
|
import { SECTION_DEF, savvyCommitBlock } from "./init.js";
|
|
3
|
+
import { WorkspaceDiscovery } from "@effected/workspaces";
|
|
3
4
|
import { CheckResult, Commitlint, ManagedSection, SavvyBaseSection, SavvyHooksSection, VersioningStrategy, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
4
|
-
import { Effect } from "effect";
|
|
5
|
-
import { WorkspaceDiscovery } from "workspaces-effect";
|
|
6
|
-
import { FileSystem } from "@effect/platform";
|
|
5
|
+
import { Effect, FileSystem } from "effect";
|
|
7
6
|
|
|
8
7
|
//#region src/commands/commit/check.ts
|
|
9
8
|
/**
|
|
@@ -78,8 +77,8 @@ function extractConfigPathFromManaged(managedContent) {
|
|
|
78
77
|
const detectReleaseFormat = Effect.gen(function* () {
|
|
79
78
|
const versioning = yield* VersioningStrategy;
|
|
80
79
|
const discovery = yield* WorkspaceDiscovery;
|
|
81
|
-
const publishableNames = (yield* Effect.
|
|
82
|
-
const result = yield* Effect.
|
|
80
|
+
const publishableNames = (yield* Effect.catch(discovery.listPackages(), () => Effect.succeed([]))).filter((pkg) => !pkg.private || pkg.publishConfig?.access !== void 0).map((pkg) => pkg.name);
|
|
81
|
+
const result = yield* Effect.catch(versioning.detect(publishableNames, process.cwd()), () => Effect.succeed({ type: "single" }));
|
|
83
82
|
return STRATEGY_TO_FORMAT[result.type] ?? "semver";
|
|
84
83
|
});
|
|
85
84
|
/**
|
|
@@ -159,7 +158,7 @@ function runCommitCheck() {
|
|
|
159
158
|
yield* Effect.log(` DCO required: ${Commitlint.detectDCO()}`);
|
|
160
159
|
const releaseFormat = yield* detectReleaseFormat;
|
|
161
160
|
yield* Effect.log(` Release format: ${releaseFormat}`);
|
|
162
|
-
const scopes = yield* Effect.
|
|
161
|
+
const scopes = yield* Effect.catch(Commitlint.detectScopes, () => Effect.succeed([]));
|
|
163
162
|
const scopeDisplay = scopes.length > 0 ? scopes.join(", ") : "(none - not a monorepo or no packages found)";
|
|
164
163
|
yield* Effect.log(` Detected scopes: ${scopeDisplay}`);
|
|
165
164
|
yield* Effect.log("");
|
package/commands/commit/hook.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { postCommitVerifyCommand } from "./hooks/post-commit-verify.js";
|
|
2
2
|
import { preCommitMessageCommand } from "./hooks/pre-commit-message.js";
|
|
3
3
|
import { sessionStartCommand } from "./hooks/session-start.js";
|
|
4
|
-
import { Command } from "
|
|
4
|
+
import { Command } from "effect/unstable/cli";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/commit/hook.ts
|
|
7
7
|
/**
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Git } from "@effected/git";
|
|
2
2
|
import { Commitlint } from "@savvy-web/silk-effects";
|
|
3
3
|
import { Effect } from "effect";
|
|
4
|
-
import {
|
|
4
|
+
import { Command } from "effect/unstable/cli";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
6
7
|
|
|
7
8
|
//#region src/commands/commit/hooks/post-commit-verify.ts
|
|
8
9
|
/**
|
|
@@ -57,54 +58,33 @@ function buildCommitlintInvocation(pm, configPath) {
|
|
|
57
58
|
};
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} catch {
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function readSignatureStatus() {
|
|
79
|
-
try {
|
|
80
|
-
const { stdout } = await execFileP("git", [
|
|
81
|
-
"log",
|
|
82
|
-
"-1",
|
|
83
|
-
"--format=%G?"
|
|
84
|
-
]);
|
|
85
|
-
return stdout.trim();
|
|
86
|
-
} catch {
|
|
87
|
-
return "N";
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async function readLastCommitBody() {
|
|
91
|
-
try {
|
|
92
|
-
const { stdout } = await execFileP("git", [
|
|
93
|
-
"log",
|
|
94
|
-
"-1",
|
|
95
|
-
"--format=%B"
|
|
96
|
-
]);
|
|
97
|
-
return stdout;
|
|
98
|
-
} catch {
|
|
99
|
-
return "";
|
|
100
|
-
}
|
|
61
|
+
/** The repo root via `@effected/git`, degrading to `process.cwd()` — the prior execFile contract. */
|
|
62
|
+
const getRepoRoot = Effect.gen(function* () {
|
|
63
|
+
return yield* (yield* Git).repoRoot(process.cwd()).pipe(Effect.catch(() => Effect.succeed(process.cwd())));
|
|
64
|
+
});
|
|
65
|
+
function runCommitlintLast(root) {
|
|
66
|
+
return (async () => {
|
|
67
|
+
const { command, args } = buildCommitlintInvocation(await Commitlint.detectPackageManager(root), await Commitlint.readCommitlintConfigPath(root));
|
|
68
|
+
try {
|
|
69
|
+
await execFileP(command, args, { cwd: root });
|
|
70
|
+
return false;
|
|
71
|
+
} catch {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
101
75
|
}
|
|
102
76
|
const postCommitVerifyCommand = Command.make("post-commit-verify", {}, () => Effect.gen(function* () {
|
|
77
|
+
const git = yield* Git;
|
|
103
78
|
const branch = yield* Commitlint.readBranchInfo();
|
|
104
79
|
const signing = yield* Commitlint.readSigningDiagnostic();
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
80
|
+
const root = yield* getRepoRoot;
|
|
81
|
+
const commitlintFailed = yield* Effect.promise(() => runCommitlintLast(root));
|
|
82
|
+
const lastCommit = yield* git.commitInfo(root).pipe(Effect.catch(() => Effect.succeed({
|
|
83
|
+
signatureStatus: "N",
|
|
84
|
+
message: ""
|
|
85
|
+
})));
|
|
86
|
+
const sigStatus = lastCommit.signatureStatus;
|
|
87
|
+
const body = lastCommit.message;
|
|
108
88
|
const bodyHasClosing = branch.inferredTicketId !== null && Commitlint.hasClosingTrailer(body, branch.inferredTicketId);
|
|
109
89
|
const advice = buildPostCommitAdvice({
|
|
110
90
|
commitlintFailed,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Command } from "@effect/cli";
|
|
2
1
|
import { Commitlint } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Command } from "effect/unstable/cli";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/commit/hooks/pre-commit-message.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Command } from "@effect/cli";
|
|
2
1
|
import { Commitlint } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect } from "effect";
|
|
3
|
+
import { Command } from "effect/unstable/cli";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/commit/hooks/session-start.ts
|
package/commands/commit/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runCommitInit } from "./init.js";
|
|
2
2
|
import { runCommitCheck } from "./check.js";
|
|
3
3
|
import { hookCommand } from "./hook.js";
|
|
4
|
-
import { Command } from "
|
|
4
|
+
import { Command } from "effect/unstable/cli";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/commit/index.ts
|
|
7
7
|
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
package/commands/commit/init.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
2
2
|
import { ManagedSection, SavvyBaseSection, SavvyHooksSection, SectionDefinition, savvyBasePreamble, savvyHooksHygiene, savvyToolSection } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
3
|
+
import { Effect, FileSystem } from "effect";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { FileSystem } from "@effect/platform";
|
|
6
5
|
import { chmod } from "node:fs/promises";
|
|
7
6
|
|
|
8
7
|
//#region src/commands/commit/init.ts
|
package/commands/init.js
CHANGED
|
@@ -2,8 +2,8 @@ import { runChangesetInit } from "./changeset/commands/init.js";
|
|
|
2
2
|
import "./changeset/index.js";
|
|
3
3
|
import { runCommitInit } from "./commit/init.js";
|
|
4
4
|
import { runLintInit } from "./lint/init.js";
|
|
5
|
-
import { Command, Options } from "@effect/cli";
|
|
6
5
|
import { Effect } from "effect";
|
|
6
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
7
7
|
|
|
8
8
|
//#region src/commands/init.ts
|
|
9
9
|
/**
|
|
@@ -24,14 +24,14 @@ import { Effect } from "effect";
|
|
|
24
24
|
const DEFAULT_COMMIT_CONFIG = "lib/configs/commitlint.config.ts";
|
|
25
25
|
const DEFAULT_LINT_CONFIG = "lib/configs/lint-staged.config.ts";
|
|
26
26
|
/* v8 ignore start -- CLI option definitions; orchestration logic tested via runInit */
|
|
27
|
-
const forceOption =
|
|
28
|
-
const commitConfigOption =
|
|
29
|
-
const lintConfigOption =
|
|
30
|
-
const lintPresetOption =
|
|
27
|
+
const forceOption = Flag.boolean("force").pipe(Flag.withAlias("f"), Flag.withDescription("Overwrite existing config files and hooks across all tools"), Flag.withDefault(false));
|
|
28
|
+
const commitConfigOption = Flag.string("commit-config").pipe(Flag.withDescription("Relative path for the commitlint config file"), Flag.withDefault(DEFAULT_COMMIT_CONFIG));
|
|
29
|
+
const lintConfigOption = Flag.string("lint-config").pipe(Flag.withDescription("Relative path for the lint-staged config file"), Flag.withDefault(DEFAULT_LINT_CONFIG));
|
|
30
|
+
const lintPresetOption = Flag.choice("lint-preset", [
|
|
31
31
|
"minimal",
|
|
32
32
|
"standard",
|
|
33
33
|
"silk"
|
|
34
|
-
]).pipe(
|
|
34
|
+
]).pipe(Flag.withDescription("lint-staged preset: minimal, standard, or silk"), Flag.withDefault("silk"));
|
|
35
35
|
/* v8 ignore stop */
|
|
36
36
|
/**
|
|
37
37
|
* Run the three init step Effects in order: changeset → commit → lint.
|
package/commands/lint/check.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { BIOME_VERSION } from "./biome-version.js";
|
|
2
2
|
import { CheckResult, ConfigDiscovery, Lint, ManagedSection, SavvyBaseSection, SavvyHooksSection, ToolDefinition, ToolDiscovery, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
4
|
-
import {
|
|
5
|
-
import { FileSystem } from "@effect/platform";
|
|
3
|
+
import { Effect, FileSystem } from "effect";
|
|
4
|
+
import { Jsonc } from "@effected/jsonc";
|
|
6
5
|
import { isDeepStrictEqual } from "node:util";
|
|
7
6
|
|
|
8
7
|
//#region src/commands/lint/check.ts
|
|
@@ -83,7 +82,7 @@ function extractConfigPathFromManaged(managedContent) {
|
|
|
83
82
|
*/
|
|
84
83
|
function checkMarkdownlintConfig(content) {
|
|
85
84
|
return Effect.gen(function* () {
|
|
86
|
-
const parsed = yield* parse(content);
|
|
85
|
+
const parsed = yield* Jsonc.parse(content);
|
|
87
86
|
const schemaMatches = parsed.$schema === Lint.MARKDOWNLINT_SCHEMA;
|
|
88
87
|
const existingConfig = parsed.config;
|
|
89
88
|
const configMatches = existingConfig !== void 0 && isDeepStrictEqual(existingConfig, Lint.MARKDOWNLINT_CONFIG);
|
|
@@ -111,16 +110,19 @@ function checkBiomeSchemas() {
|
|
|
111
110
|
const warnings = [];
|
|
112
111
|
const expectedSchema = `https://biomejs.dev/schemas/${BIOME_VERSION}/schema.json`;
|
|
113
112
|
const configPaths = Lint.Biome.findAllConfigs();
|
|
114
|
-
for (const configPath of configPaths)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
118
|
-
else {
|
|
119
|
-
statuses.push({
|
|
113
|
+
for (const configPath of configPaths) {
|
|
114
|
+
const content = yield* fs.readFileString(configPath);
|
|
115
|
+
if ((yield* Jsonc.parse(content)).$schema === expectedSchema) statuses.push({
|
|
120
116
|
path: configPath,
|
|
121
|
-
matches:
|
|
117
|
+
matches: true
|
|
122
118
|
});
|
|
123
|
-
|
|
119
|
+
else {
|
|
120
|
+
statuses.push({
|
|
121
|
+
path: configPath,
|
|
122
|
+
matches: false
|
|
123
|
+
});
|
|
124
|
+
warnings.push(`${WARNING} ${configPath}: biome $schema is outdated.\n Run 'savvy init' to update it.`);
|
|
125
|
+
}
|
|
124
126
|
}
|
|
125
127
|
return {
|
|
126
128
|
statuses,
|
|
@@ -214,7 +216,7 @@ function runLintCheck(opts) {
|
|
|
214
216
|
warnings.push(`${WARNING} ${hookPath} savvy-hooks section is outdated.\n Run 'savvy init' to update.`);
|
|
215
217
|
}
|
|
216
218
|
}
|
|
217
|
-
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.
|
|
219
|
+
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.catch(() => Effect.succeed({
|
|
218
220
|
statuses: [],
|
|
219
221
|
warnings: [`${WARNING} Could not check biome $schema URLs.`]
|
|
220
222
|
})));
|
package/commands/lint/fmt.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Args, Command } from "@effect/cli";
|
|
2
1
|
import { Lint } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect } from "effect";
|
|
3
|
+
import { Argument, Command } from "effect/unstable/cli";
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import { Yaml, YamlStringifyOptions } from "@effected/yaml";
|
|
6
6
|
|
|
7
7
|
//#region src/commands/lint/fmt.ts
|
|
8
8
|
/**
|
|
@@ -15,17 +15,13 @@ import { parse, stringify } from "yaml";
|
|
|
15
15
|
*
|
|
16
16
|
* @internal
|
|
17
17
|
*/
|
|
18
|
-
/** Default YAML stringify options matching PnpmWorkspace handler. */
|
|
19
|
-
const YAML_STRINGIFY_OPTIONS = {
|
|
18
|
+
/** Default YAML stringify options matching PnpmWorkspace handler (plain scalars = the v3 singleQuote:false posture). */
|
|
19
|
+
const YAML_STRINGIFY_OPTIONS = YamlStringifyOptions.make({
|
|
20
20
|
indent: 2,
|
|
21
|
-
lineWidth: 0
|
|
22
|
-
|
|
23
|
-
};
|
|
21
|
+
lineWidth: 0
|
|
22
|
+
});
|
|
24
23
|
/** Repeated file path arguments. */
|
|
25
|
-
const filesArg =
|
|
26
|
-
name: "files",
|
|
27
|
-
exists: "yes"
|
|
28
|
-
}));
|
|
24
|
+
const filesArg = Argument.file("files", { mustExist: true }).pipe(Argument.variadic());
|
|
29
25
|
/** Sort package.json files with sort-package-json. */
|
|
30
26
|
const packageJsonCommand = Command.make("package-json", { files: filesArg }, ({ files }) => Effect.sync(() => {
|
|
31
27
|
for (const filepath of files) {
|
|
@@ -35,11 +31,13 @@ const packageJsonCommand = Command.make("package-json", { files: filesArg }, ({
|
|
|
35
31
|
}
|
|
36
32
|
}));
|
|
37
33
|
/** Sort and format pnpm-workspace.yaml. */
|
|
38
|
-
const pnpmWorkspaceCommand = Command.make("pnpm-workspace", {}, () => Effect.
|
|
34
|
+
const pnpmWorkspaceCommand = Command.make("pnpm-workspace", {}, () => Effect.gen(function* () {
|
|
39
35
|
const filepath = "pnpm-workspace.yaml";
|
|
40
36
|
if (!existsSync(filepath)) return;
|
|
41
|
-
const
|
|
42
|
-
|
|
37
|
+
const content = readFileSync(filepath, "utf-8");
|
|
38
|
+
const parsed = yield* Yaml.parse(content);
|
|
39
|
+
const sorted = Lint.PnpmWorkspace.sortContent(parsed);
|
|
40
|
+
writeFileSync(filepath, yield* Yaml.stringify(sorted, YAML_STRINGIFY_OPTIONS), "utf-8");
|
|
43
41
|
}));
|
|
44
42
|
/** Format YAML files with Prettier. */
|
|
45
43
|
const yamlCommand = Command.make("yaml", { files: filesArg }, ({ files }) => Effect.gen(function* () {
|
package/commands/lint/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runLintCheck } from "./check.js";
|
|
2
2
|
import { runLintInit } from "./init.js";
|
|
3
3
|
import { fmtCommand } from "./fmt.js";
|
|
4
|
-
import { Command } from "
|
|
4
|
+
import { Command } from "effect/unstable/cli";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/lint/index.ts
|
|
7
7
|
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
package/commands/lint/init.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { BIOME_VERSION } from "./biome-version.js";
|
|
2
2
|
import { BiomeSchemaSync, Lint, ManagedSection, SavvyBaseSection, SavvyHooksSection, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
3
|
+
import { Effect, FileSystem } from "effect";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import { FileSystem } from "@effect/platform";
|
|
5
|
+
import { Jsonc, JsoncEdit, JsoncModifier } from "@effected/jsonc";
|
|
7
6
|
import { chmod } from "node:fs/promises";
|
|
8
7
|
import { isDeepStrictEqual } from "node:util";
|
|
9
8
|
|
|
@@ -90,20 +89,20 @@ function writeMarkdownlintConfig(fs, preset, force) {
|
|
|
90
89
|
return;
|
|
91
90
|
}
|
|
92
91
|
const existingText = yield* fs.readFileString(Lint.MARKDOWNLINT_CONFIG_PATH);
|
|
93
|
-
const existingParsed = yield* parse(existingText);
|
|
92
|
+
const existingParsed = yield* Jsonc.parse(existingText);
|
|
94
93
|
let updatedText = existingText;
|
|
95
94
|
const applied = [];
|
|
96
95
|
if (existingParsed.$schema !== Lint.MARKDOWNLINT_SCHEMA) {
|
|
97
|
-
const edits = yield* modify(updatedText, ["$schema"], Lint.MARKDOWNLINT_SCHEMA, { formattingOptions: JSONC_FORMAT });
|
|
98
|
-
updatedText =
|
|
96
|
+
const edits = yield* JsoncModifier.modify(updatedText, ["$schema"], Lint.MARKDOWNLINT_SCHEMA, { formattingOptions: JSONC_FORMAT });
|
|
97
|
+
updatedText = JsoncEdit.applyAll(updatedText, edits);
|
|
99
98
|
applied.push("$schema");
|
|
100
99
|
}
|
|
101
100
|
const existingIgnores = Array.isArray(existingParsed.ignores) ? existingParsed.ignores : [];
|
|
102
101
|
const missingIgnores = Lint.MARKDOWNLINT_TEMPLATE.ignores.filter((glob) => !existingIgnores.includes(glob));
|
|
103
102
|
if (missingIgnores.length > 0) {
|
|
104
103
|
const mergedIgnores = [...existingIgnores, ...missingIgnores];
|
|
105
|
-
const edits = yield* modify(updatedText, ["ignores"], mergedIgnores, { formattingOptions: JSONC_FORMAT });
|
|
106
|
-
updatedText =
|
|
104
|
+
const edits = yield* JsoncModifier.modify(updatedText, ["ignores"], mergedIgnores, { formattingOptions: JSONC_FORMAT });
|
|
105
|
+
updatedText = JsoncEdit.applyAll(updatedText, edits);
|
|
107
106
|
applied.push(`ignores (+${missingIgnores.length})`);
|
|
108
107
|
}
|
|
109
108
|
const existingConfig = existingParsed.config;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
1
|
import { Repos } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect, Option } from "effect";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
4
|
|
|
5
5
|
//#region src/commands/repos/commands/add.ts
|
|
6
6
|
/**
|
|
@@ -28,12 +28,12 @@ import { Effect, Option } from "effect";
|
|
|
28
28
|
* @internal
|
|
29
29
|
*/
|
|
30
30
|
/* v8 ignore start -- CLI option/arg definitions */
|
|
31
|
-
const urlArg =
|
|
32
|
-
const refOption =
|
|
33
|
-
const purposeOption =
|
|
34
|
-
const nameOption =
|
|
35
|
-
const sparseOption =
|
|
36
|
-
const cwdOption =
|
|
31
|
+
const urlArg = Argument.string("url");
|
|
32
|
+
const refOption = Flag.string("ref").pipe(Flag.withDescription("Ref (tag, branch, or commit) to check out"));
|
|
33
|
+
const purposeOption = Flag.string("purpose").pipe(Flag.withDescription("Why this repo is vendored"));
|
|
34
|
+
const nameOption = Flag.string("name").pipe(Flag.withDescription("Vendored directory name; defaults to the URL's last path segment"), Flag.optional);
|
|
35
|
+
const sparseOption = Flag.string("sparse").pipe(Flag.withDescription("Sparse-checkout path; repeatable"), Flag.atLeast(0));
|
|
36
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to add within"), Flag.withDefault("."));
|
|
37
37
|
/* v8 ignore stop */
|
|
38
38
|
/**
|
|
39
39
|
* Add handler; exported for tests.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
1
|
import { Repos } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect } from "effect";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
4
|
|
|
5
5
|
//#region src/commands/repos/commands/note.ts
|
|
6
6
|
/**
|
|
@@ -52,34 +52,38 @@ const runReposNote = (cwd, name, op) => Effect.gen(function* () {
|
|
|
52
52
|
return Effect.log(error.message);
|
|
53
53
|
}));
|
|
54
54
|
/* v8 ignore start -- CLI registration; handler tested via runReposNote */
|
|
55
|
-
const nameArg =
|
|
56
|
-
const noteTextArg =
|
|
57
|
-
const noteIdArg =
|
|
58
|
-
const intoOption =
|
|
59
|
-
const cwdOption =
|
|
60
|
-
const _noteGroup = Command.make("note"
|
|
55
|
+
const nameArg = Argument.string("name");
|
|
56
|
+
const noteTextArg = Argument.string("text");
|
|
57
|
+
const noteIdArg = Argument.string("id");
|
|
58
|
+
const intoOption = Flag.choice("into", ["layout", "startHere"]).pipe(Flag.withDescription("Curated orientation field to promote the note into"));
|
|
59
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root whose manifest holds the notes"), Flag.withDefault("."));
|
|
60
|
+
const _noteGroup = Command.make("note").pipe(Command.withSharedFlags({ cwd: cwdOption }));
|
|
61
|
+
const addLeaf = Command.make("add", {
|
|
61
62
|
name: nameArg,
|
|
62
|
-
|
|
63
|
-
})
|
|
64
|
-
const
|
|
65
|
-
const { name, cwd } = yield* _noteGroup;
|
|
63
|
+
note: noteTextArg
|
|
64
|
+
}, ({ name, note }) => Effect.gen(function* () {
|
|
65
|
+
const { cwd } = yield* _noteGroup;
|
|
66
66
|
yield* runReposNote(cwd, name, {
|
|
67
67
|
op: "add",
|
|
68
68
|
note
|
|
69
69
|
});
|
|
70
70
|
})).pipe(Command.withDescription("Append an agent note to a vendored repo"));
|
|
71
|
-
const removeLeaf = Command.make("remove", {
|
|
72
|
-
|
|
71
|
+
const removeLeaf = Command.make("remove", {
|
|
72
|
+
name: nameArg,
|
|
73
|
+
id: noteIdArg
|
|
74
|
+
}, ({ name, id }) => Effect.gen(function* () {
|
|
75
|
+
const { cwd } = yield* _noteGroup;
|
|
73
76
|
yield* runReposNote(cwd, name, {
|
|
74
77
|
op: "remove",
|
|
75
78
|
id
|
|
76
79
|
});
|
|
77
80
|
})).pipe(Command.withDescription("Remove an agent note from a vendored repo"));
|
|
78
81
|
const promoteLeaf = Command.make("promote", {
|
|
82
|
+
name: nameArg,
|
|
79
83
|
id: noteIdArg,
|
|
80
84
|
into: intoOption
|
|
81
|
-
}, ({ id, into }) => Effect.gen(function* () {
|
|
82
|
-
const {
|
|
85
|
+
}, ({ name, id, into }) => Effect.gen(function* () {
|
|
86
|
+
const { cwd } = yield* _noteGroup;
|
|
83
87
|
yield* runReposNote(cwd, name, {
|
|
84
88
|
op: "promote",
|
|
85
89
|
id,
|
|
@@ -93,11 +97,6 @@ const _noteCommand = _noteGroup.pipe(Command.withSubcommands([
|
|
|
93
97
|
]), Command.withDescription("Agent notes for a vendored repo: add, remove, promote"));
|
|
94
98
|
/**
|
|
95
99
|
* The `savvy repos note` command group.
|
|
96
|
-
*
|
|
97
|
-
* @remarks
|
|
98
|
-
* Typed as `unknown` at the export boundary to avoid TypeScript declaration-emit
|
|
99
|
-
* errors from Effect's internal types (TS4023), matching the `reposCommand`
|
|
100
|
-
* export pattern.
|
|
101
100
|
*/
|
|
102
101
|
const noteCommand = _noteCommand;
|
|
103
102
|
/* v8 ignore stop */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
1
|
import { Repos } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect } from "effect";
|
|
3
|
+
import { Argument, Command, Flag } from "effect/unstable/cli";
|
|
4
4
|
|
|
5
5
|
//#region src/commands/repos/commands/pin.ts
|
|
6
6
|
/**
|
|
@@ -26,9 +26,9 @@ import { Effect } from "effect";
|
|
|
26
26
|
* @internal
|
|
27
27
|
*/
|
|
28
28
|
/* v8 ignore start -- CLI option/arg definitions */
|
|
29
|
-
const nameArg =
|
|
30
|
-
const refArg =
|
|
31
|
-
const cwdOption =
|
|
29
|
+
const nameArg = Argument.string("name");
|
|
30
|
+
const refArg = Argument.string("ref");
|
|
31
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to pin within"), Flag.withDefault("."));
|
|
32
32
|
/* v8 ignore stop */
|
|
33
33
|
/**
|
|
34
34
|
* Pin handler; exported for tests.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
|
2
1
|
import { Repos } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Console, Effect } from "effect";
|
|
3
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
4
4
|
|
|
5
5
|
//#region src/commands/repos/commands/status.ts
|
|
6
6
|
/**
|
|
@@ -25,8 +25,8 @@ import { Console, Effect } from "effect";
|
|
|
25
25
|
* @internal
|
|
26
26
|
*/
|
|
27
27
|
/* v8 ignore start -- CLI option definitions */
|
|
28
|
-
const jsonOption =
|
|
29
|
-
const cwdOption =
|
|
28
|
+
const jsonOption = Flag.boolean("json").pipe(Flag.withDescription("Emit the structured drift report as JSON"), Flag.withDefault(false));
|
|
29
|
+
const cwdOption = Flag.directory("cwd").pipe(Flag.withDescription("Repo root to inspect"), Flag.withDefault("."));
|
|
30
30
|
/* v8 ignore stop */
|
|
31
31
|
/**
|
|
32
32
|
* Drift report handler; exported for tests.
|