@savvy-web/cli 0.5.0 → 1.1.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/cli/index.js +4 -4
- package/commands/changeset/commands/check.js +1 -1
- package/commands/changeset/commands/deps-detect.js +4 -4
- package/commands/changeset/commands/deps-regen.js +3 -3
- package/commands/changeset/commands/init.js +1 -17
- package/commands/changeset/commands/lint.js +3 -3
- package/commands/changeset/index.js +3 -9
- package/commands/commit/check.js +6 -5
- package/commands/commit/constants.js +3 -1
- package/commands/commit/init.js +7 -27
- package/commands/lint/check.js +5 -7
- package/commands/lint/init.js +6 -35
- package/package.json +5 -2
- package/commands/changeset/commands/analyze-branch.js +0 -108
- package/commands/changeset/commands/classify.js +0 -69
- package/commands/changeset/commands/config-show.js +0 -100
- package/commands/changeset/commands/release-surface.js +0 -96
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ npx savvy clean --globs dist,.turbo,coverage
|
|
|
53
53
|
- `savvy check` — orchestrator that runs all three checks and reports every failure (it does not short-circuit).
|
|
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, user-prompt-submit).
|
|
56
|
-
- `savvy changeset` — changeset lint, transform, version,
|
|
56
|
+
- `savvy changeset` — changeset lint, check, transform, version, config validation, and dependency changesets.
|
|
57
57
|
- `savvy lint` — formatters for package.json, the pnpm workspace file and YAML.
|
|
58
58
|
|
|
59
59
|
Run any command with `--help` to see its full surface:
|
package/cli/index.js
CHANGED
|
@@ -56,7 +56,7 @@ const rootCommand = Command.make("savvy").pipe(Command.withSubcommands([
|
|
|
56
56
|
]));
|
|
57
57
|
const cli = Command.run(rootCommand, {
|
|
58
58
|
name: "savvy",
|
|
59
|
-
version: "
|
|
59
|
+
version: "1.1.0"
|
|
60
60
|
});
|
|
61
61
|
/**
|
|
62
62
|
* Shared base layer: workspace services, the changeset config reader, and the
|
|
@@ -91,14 +91,14 @@ const BaseLive = Layer.mergeAll(WorkspaceLive, ChangesetConfigReaderLive, Manage
|
|
|
91
91
|
* `ToolDiscoveryLive` needs `WorkspaceRoot`, `PackageManagerDetector`, and
|
|
92
92
|
* `CommandExecutor`; `VersioningStrategyLive` needs `ChangesetConfigReader`;
|
|
93
93
|
* `Changesets.ConfigInspectorLive` needs `ChangesetConfigReader`,
|
|
94
|
-
* `WorkspaceDiscovery`, and `FileSystem` (the last for its
|
|
94
|
+
* `WorkspaceDiscovery`, and `FileSystem` (the last for its publishConfig-driven
|
|
95
95
|
* fallback when no explicit `packages` record is configured);
|
|
96
96
|
* `Changesets.BranchAnalyzerLive` needs `ConfigInspector`.
|
|
97
97
|
*
|
|
98
98
|
* `ConfigInspectorLive` is built once via {@link Layer.provideMerge}: the merge
|
|
99
99
|
* feeds that single `ConfigInspector` instance into `BranchAnalyzerLive` AND
|
|
100
|
-
* re-exposes it for the
|
|
101
|
-
* so it is never constructed twice per run.
|
|
100
|
+
* re-exposes it for the surviving `config validate` handler that yields it
|
|
101
|
+
* directly, so it is never constructed twice per run.
|
|
102
102
|
*
|
|
103
103
|
* `provideMerge(BaseLive)` feeds the remaining deps and re-exposes the base
|
|
104
104
|
* services for handlers that yield them directly. `provideMerge(NodeContext.layer)`
|
|
@@ -68,4 +68,4 @@ function runChangesetCheck(dir) {
|
|
|
68
68
|
const checkCommand = Command.make("check", { dir: dirArg }, ({ dir }) => runChangesetCheck(dir)).pipe(Command.withDescription("Full changeset validation with summary"));
|
|
69
69
|
|
|
70
70
|
//#endregion
|
|
71
|
-
export { runChangesetCheck };
|
|
71
|
+
export { checkCommand, runChangesetCheck };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Options } from "@effect/cli";
|
|
2
2
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect, Option } from "effect";
|
|
3
|
+
import { Console, Effect, Option } from "effect";
|
|
4
4
|
import { WorkspaceDiscovery } from "workspaces-effect";
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
|
|
@@ -15,8 +15,8 @@ import { resolve } from "node:path";
|
|
|
15
15
|
*
|
|
16
16
|
* Defaults:
|
|
17
17
|
* - `--from` → `git merge-base <baseBranch> HEAD`
|
|
18
|
-
* - `--to` → working tree (i.e., `HEAD` plus staged + unstaged + untracked
|
|
19
|
-
*
|
|
18
|
+
* - `--to` → working tree (i.e., `HEAD` plus staged + unstaged + untracked).
|
|
19
|
+
* Passed as the special value
|
|
20
20
|
* `WORKTREE` to {@link WorkspaceSnapshotReader} — implementations resolve
|
|
21
21
|
* this against the live working tree rather than `git show`.
|
|
22
22
|
*
|
|
@@ -86,7 +86,7 @@ function runDepsDetect(cwd, from, to, pkg, json, markdown) {
|
|
|
86
86
|
yield* Effect.log(renderMarkdownBlocks(diffs));
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
yield*
|
|
89
|
+
yield* Console.log(JSON.stringify(diffs, null, 2));
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
/* v8 ignore next 7 */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Options } from "@effect/cli";
|
|
2
2
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect, Option } from "effect";
|
|
3
|
+
import { Console, Effect, Option } from "effect";
|
|
4
4
|
import { WorkspaceDiscovery } from "workspaces-effect";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -231,7 +231,7 @@ function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
|
231
231
|
skippedMixed
|
|
232
232
|
};
|
|
233
233
|
if (dryRun) {
|
|
234
|
-
if (json) yield*
|
|
234
|
+
if (json) yield* Console.log(JSON.stringify(plan, null, 2));
|
|
235
235
|
else yield* renderHumanPlan(plan);
|
|
236
236
|
return;
|
|
237
237
|
}
|
|
@@ -241,7 +241,7 @@ function runDepsRegen(cwd, base, pkg, dryRun, json) {
|
|
|
241
241
|
yield* Effect.logWarning(`Failed to delete ${entry.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
242
|
}
|
|
243
243
|
for (const entry of toWrite) writeFileSync(entry.file, renderChangesetContent(entry.diff));
|
|
244
|
-
if (json) yield*
|
|
244
|
+
if (json) yield* Console.log(JSON.stringify(plan, null, 2));
|
|
245
245
|
else yield* renderHumanPlan(plan);
|
|
246
246
|
});
|
|
247
247
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
|
2
1
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Data, Effect, Schema } from "effect";
|
|
4
3
|
import { WorkspaceRoot } from "workspaces-effect";
|
|
@@ -33,8 +32,7 @@ import { applyEdits, modify, parse } from "jsonc-effect";
|
|
|
33
32
|
*
|
|
34
33
|
* @remarks
|
|
35
34
|
* `runChangesetInit` backs the changeset step of the unified `savvy init`
|
|
36
|
-
* orchestrator; there is no standalone `savvy changeset init` subcommand.
|
|
37
|
-
* exported `initCommand` is retained only as a direct test entry point.
|
|
35
|
+
* orchestrator; there is no standalone `savvy changeset init` subcommand.
|
|
38
36
|
*
|
|
39
37
|
* @example
|
|
40
38
|
* ```bash
|
|
@@ -105,12 +103,6 @@ var InitError = class extends InitErrorBase {
|
|
|
105
103
|
return `Init failed at ${this.step}: ${this.reason}`;
|
|
106
104
|
}
|
|
107
105
|
};
|
|
108
|
-
/* v8 ignore start -- CLI option definitions; handler functions tested individually */
|
|
109
|
-
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing config files"));
|
|
110
|
-
const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Silence warnings, always exit 0"));
|
|
111
|
-
const skipMarkdownlintOption = Options.boolean("skip-markdownlint").pipe(Options.withDescription("Skip registering rules in base markdownlint config"));
|
|
112
|
-
const checkOption = Options.boolean("check").pipe(Options.withDescription("Check configuration without writing (for postinstall scripts)"));
|
|
113
|
-
/* v8 ignore stop */
|
|
114
106
|
/**
|
|
115
107
|
* Detect the `owner/repo` slug from the git remote origin URL.
|
|
116
108
|
*
|
|
@@ -621,14 +613,6 @@ function runChangesetInit(opts) {
|
|
|
621
613
|
}
|
|
622
614
|
})));
|
|
623
615
|
}
|
|
624
|
-
/* v8 ignore start -- CLI orchestration; individual functions tested separately */
|
|
625
|
-
const initCommand = Command.make("init", {
|
|
626
|
-
force: forceOption,
|
|
627
|
-
quiet: quietOption,
|
|
628
|
-
skipMarkdownlint: skipMarkdownlintOption,
|
|
629
|
-
check: checkOption
|
|
630
|
-
}, (opts) => runChangesetInit(opts)).pipe(Command.withDescription("Bootstrap a repo for @savvy-web/changesets"));
|
|
631
|
-
/* v8 ignore stop */
|
|
632
616
|
|
|
633
617
|
//#endregion
|
|
634
618
|
export { runChangesetInit };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
2
|
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
3
|
+
import { Console, Effect } from "effect";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
|
|
6
6
|
//#region src/commands/changeset/commands/lint.ts
|
|
@@ -47,8 +47,8 @@ function runLint(dir, quiet) {
|
|
|
47
47
|
return Effect.gen(function* () {
|
|
48
48
|
const resolved = resolve(dir);
|
|
49
49
|
const messages = yield* Effect.try(() => ChangesetLinter.validate(resolved));
|
|
50
|
-
for (const msg of messages) yield*
|
|
51
|
-
if (!quiet && messages.length === 0) yield*
|
|
50
|
+
for (const msg of messages) yield* Console.log(`${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`);
|
|
51
|
+
if (!quiet && messages.length === 0) yield* Console.log("No lint errors found.");
|
|
52
52
|
if (messages.length > 0) process.exitCode = 1;
|
|
53
53
|
});
|
|
54
54
|
}
|
|
@@ -1,30 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { classifyCommand } from "./commands/classify.js";
|
|
3
|
-
import { configShowCommand } from "./commands/config-show.js";
|
|
1
|
+
import { checkCommand, runChangesetCheck } from "./commands/check.js";
|
|
4
2
|
import { configValidateCommand } from "./commands/config-validate.js";
|
|
5
3
|
import { depsDetectCommand } from "./commands/deps-detect.js";
|
|
6
4
|
import { depsRegenCommand } from "./commands/deps-regen.js";
|
|
7
5
|
import { lintCommand } from "./commands/lint.js";
|
|
8
|
-
import { releaseSurfaceCommand } from "./commands/release-surface.js";
|
|
9
6
|
import { transformCommand } from "./commands/transform.js";
|
|
10
7
|
import { validateFileCommand } from "./commands/validate-file.js";
|
|
11
8
|
import { versionCommand } from "./commands/version.js";
|
|
12
|
-
import { runChangesetCheck } from "./commands/check.js";
|
|
13
9
|
import { runChangesetInit } from "./commands/init.js";
|
|
14
10
|
import { Command } from "@effect/cli";
|
|
15
11
|
|
|
16
12
|
//#region src/commands/changeset/index.ts
|
|
17
13
|
/* v8 ignore start -- CLI registration; each command tested via exported handler */
|
|
18
|
-
const configGroup = Command.make("config").pipe(Command.withSubcommands([
|
|
14
|
+
const configGroup = Command.make("config").pipe(Command.withSubcommands([configValidateCommand]), Command.withDescription("Inspect or validate .changeset/config.json"));
|
|
19
15
|
const depsGroup = Command.make("deps").pipe(Command.withSubcommands([depsDetectCommand, depsRegenCommand]), Command.withDescription("Generate or regenerate dependency changesets"));
|
|
20
16
|
const _changesetCommand = Command.make("changeset").pipe(Command.withSubcommands([
|
|
21
17
|
lintCommand,
|
|
18
|
+
checkCommand,
|
|
22
19
|
transformCommand,
|
|
23
20
|
validateFileCommand,
|
|
24
21
|
versionCommand,
|
|
25
|
-
classifyCommand,
|
|
26
|
-
analyzeBranchCommand,
|
|
27
|
-
releaseSurfaceCommand,
|
|
28
22
|
configGroup,
|
|
29
23
|
depsGroup
|
|
30
24
|
]), Command.withDescription("Section-aware changeset tooling"));
|
package/commands/commit/check.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
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 { Command } from "@effect/cli";
|
|
4
3
|
import { CheckResult, Commitlint, ManagedSection, SavvyBaseSection, SavvyHooksSection, VersioningStrategy, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
5
4
|
import { Effect } from "effect";
|
|
6
5
|
import { WorkspaceDiscovery } from "workspaces-effect";
|
|
@@ -133,7 +132,11 @@ function runCommitCheck() {
|
|
|
133
132
|
yield* Effect.log(`${BULLET} Commit section: not found (run 'savvy init' to add)`);
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
|
-
for (const hookPath of [
|
|
135
|
+
for (const hookPath of [
|
|
136
|
+
POST_CHECKOUT_HOOK_PATH,
|
|
137
|
+
POST_MERGE_HOOK_PATH,
|
|
138
|
+
POST_COMMIT_HOOK_PATH
|
|
139
|
+
]) {
|
|
137
140
|
if (!(yield* fs.exists(hookPath))) {
|
|
138
141
|
sectionsHealthy = false;
|
|
139
142
|
yield* Effect.log(`${BULLET} Hygiene hook: ${hookPath} not found (run 'savvy init' to add)`);
|
|
@@ -163,8 +166,6 @@ function runCommitCheck() {
|
|
|
163
166
|
else yield* Effect.log(`${"✓"} Commitlint is configured correctly.`);
|
|
164
167
|
});
|
|
165
168
|
}
|
|
166
|
-
/* v8 ignore next 3 -- CLI registration; handler tested via runCommitCheck */
|
|
167
|
-
const checkCommand = Command.make("check", {}, () => runCommitCheck()).pipe(Command.withDescription("Check current commitlint configuration and detected settings"));
|
|
168
169
|
|
|
169
170
|
//#endregion
|
|
170
171
|
export { runCommitCheck };
|
|
@@ -5,6 +5,8 @@ const HUSKY_HOOK_PATH = ".husky/commit-msg";
|
|
|
5
5
|
const POST_CHECKOUT_HOOK_PATH = ".husky/post-checkout";
|
|
6
6
|
/** Husky post-merge hook path (savvy-hooks hygiene). */
|
|
7
7
|
const POST_MERGE_HOOK_PATH = ".husky/post-merge";
|
|
8
|
+
/** Husky post-commit hook path (savvy-hooks hygiene). */
|
|
9
|
+
const POST_COMMIT_HOOK_PATH = ".husky/post-commit";
|
|
8
10
|
|
|
9
11
|
//#endregion
|
|
10
|
-
export { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_MERGE_HOOK_PATH };
|
|
12
|
+
export { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH };
|
package/commands/commit/init.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
2
|
-
import { Command, Options } from "@effect/cli";
|
|
1
|
+
import { HUSKY_HOOK_PATH, POST_CHECKOUT_HOOK_PATH, POST_COMMIT_HOOK_PATH, POST_MERGE_HOOK_PATH } from "./constants.js";
|
|
3
2
|
import { ManagedSection, SavvyBaseSection, SavvyHooksSection, SectionDefinition, savvyBasePreamble, savvyHooksHygiene, savvyToolSection } from "@savvy-web/silk-effects";
|
|
4
3
|
import { Effect } from "effect";
|
|
5
4
|
import { dirname } from "node:path";
|
|
@@ -14,13 +13,11 @@ import { chmod } from "node:fs/promises";
|
|
|
14
13
|
*/
|
|
15
14
|
/** Executable file permission mode. */
|
|
16
15
|
const EXECUTABLE_MODE = 493;
|
|
17
|
-
/** Default path for the commitlint config file. */
|
|
18
|
-
const DEFAULT_CONFIG_PATH = "lib/configs/commitlint.config.ts";
|
|
19
16
|
/** Section definition for the savvy-commit tool section (identity for read/check/remove). */
|
|
20
17
|
const SECTION_DEF = SectionDefinition.make({ toolName: "savvy-commit" });
|
|
21
18
|
/** Header written when creating a fresh commit-msg hook. */
|
|
22
19
|
const COMMIT_MSG_HEADER = "#!/usr/bin/env sh\n# Commit-msg hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
|
|
23
|
-
/** Header written when creating a fresh hygiene hook (post-checkout / post-merge). */
|
|
20
|
+
/** Header written when creating a fresh hygiene hook (post-checkout / post-merge / post-commit). */
|
|
24
21
|
const HYGIENE_HEADER = "#!/usr/bin/env sh\n# Managed by savvy-hooks\n# Custom hooks can go above or below the managed section\n\n";
|
|
25
22
|
/**
|
|
26
23
|
* Build the commitlint command run inside the savvy-commit tool section.
|
|
@@ -49,10 +46,6 @@ function ensureHookFile(path, header) {
|
|
|
49
46
|
if (!(yield* fs.exists(path))) yield* fs.writeFileString(path, header);
|
|
50
47
|
});
|
|
51
48
|
}
|
|
52
|
-
/* v8 ignore start -- CLI option definitions; handler tested individually */
|
|
53
|
-
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite the commit-msg hook and config file entirely (managed sections in post-checkout/post-merge are never force-reset)"), Options.withDefault(false));
|
|
54
|
-
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the commitlint config file (from repo root)"), Options.withDefault(DEFAULT_CONFIG_PATH));
|
|
55
|
-
/* v8 ignore stop */
|
|
56
49
|
/** Content for the commitlint config file. */
|
|
57
50
|
const CONFIG_CONTENT = `import { CommitlintConfig } from "@savvy-web/silk/commitlint";
|
|
58
51
|
|
|
@@ -89,7 +82,11 @@ function runCommitInit(opts) {
|
|
|
89
82
|
const commitResults = yield* ms.syncMany(HUSKY_HOOK_PATH, [SavvyBaseSection.block(savvyBasePreamble()), savvyCommitBlock(config)]);
|
|
90
83
|
yield* makeExecutable(HUSKY_HOOK_PATH);
|
|
91
84
|
yield* Effect.log(`${"✓"} ${force ? "Replaced" : "Synced"} ${HUSKY_HOOK_PATH} (${commitResults.map((r) => r._tag).join(", ")})`);
|
|
92
|
-
for (const hookPath of [
|
|
85
|
+
for (const hookPath of [
|
|
86
|
+
POST_CHECKOUT_HOOK_PATH,
|
|
87
|
+
POST_MERGE_HOOK_PATH,
|
|
88
|
+
POST_COMMIT_HOOK_PATH
|
|
89
|
+
]) {
|
|
93
90
|
yield* ensureHookFile(hookPath, HYGIENE_HEADER);
|
|
94
91
|
yield* ms.sync(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
95
92
|
yield* makeExecutable(hookPath);
|
|
@@ -105,23 +102,6 @@ function runCommitInit(opts) {
|
|
|
105
102
|
yield* Effect.log("\nDone! Install @commitlint/cli if not already installed.");
|
|
106
103
|
});
|
|
107
104
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Init command implementation.
|
|
110
|
-
*
|
|
111
|
-
* @remarks
|
|
112
|
-
* Writes:
|
|
113
|
-
* - `.husky/commit-msg` — savvy-base preamble + savvy-commit tool section.
|
|
114
|
-
* - `.husky/post-checkout` and `.husky/post-merge` — savvy-hooks hygiene
|
|
115
|
-
* (co-owned with `@savvy-web/lint-staged`; idempotent).
|
|
116
|
-
* - The commitlint config file.
|
|
117
|
-
*
|
|
118
|
-
* Users may add custom commands above, below, or between the managed sections.
|
|
119
|
-
*/
|
|
120
|
-
/* v8 ignore next 3 -- CLI registration; handler tested via runCommitInit */
|
|
121
|
-
const initCommand = Command.make("init", {
|
|
122
|
-
force: forceOption,
|
|
123
|
-
config: configOption
|
|
124
|
-
}, (opts) => runCommitInit(opts)).pipe(Command.withDescription("Initialize commitlint configuration and husky hooks"));
|
|
125
105
|
|
|
126
106
|
//#endregion
|
|
127
107
|
export { SECTION_DEF, runCommitInit, savvyCommitBlock };
|
package/commands/lint/check.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
|
2
1
|
import { CheckResult, ConfigDiscovery, Lint, ManagedSection, SavvyBaseSection, SavvyHooksSection, ToolDefinition, ToolDiscovery, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect } from "effect";
|
|
4
3
|
import { parse } from "jsonc-effect";
|
|
@@ -133,9 +132,6 @@ function checkBiomeSchemas() {
|
|
|
133
132
|
};
|
|
134
133
|
});
|
|
135
134
|
}
|
|
136
|
-
/* v8 ignore start -- CLI option definition; handler tested individually */
|
|
137
|
-
const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings (for postinstall usage)"), Options.withDefault(false));
|
|
138
|
-
/* v8 ignore stop */
|
|
139
135
|
/**
|
|
140
136
|
* Run the lint check validation pipeline.
|
|
141
137
|
*
|
|
@@ -191,7 +187,11 @@ function runLintCheck(opts) {
|
|
|
191
187
|
warnings.push(`${WARNING} No husky pre-commit hook found.\n Run 'savvy init' to create it.`);
|
|
192
188
|
}
|
|
193
189
|
if (!foundConfig) warnings.push(`${WARNING} No lint-staged config file found.\n Run 'savvy init' to create one.`);
|
|
194
|
-
const shellHookPaths = [
|
|
190
|
+
const shellHookPaths = [
|
|
191
|
+
Lint.POST_CHECKOUT_HOOK_PATH,
|
|
192
|
+
Lint.POST_MERGE_HOOK_PATH,
|
|
193
|
+
Lint.POST_COMMIT_HOOK_PATH
|
|
194
|
+
];
|
|
195
195
|
const shellHookStatuses = [];
|
|
196
196
|
for (const hookPath of shellHookPaths) {
|
|
197
197
|
if (!(yield* fs.exists(hookPath))) {
|
|
@@ -299,8 +299,6 @@ function runLintCheck(opts) {
|
|
|
299
299
|
else yield* Effect.log(`${CHECK_MARK} Lint-staged is configured correctly.`);
|
|
300
300
|
});
|
|
301
301
|
}
|
|
302
|
-
/* v8 ignore next 3 -- CLI registration; handler tested via runLintCheck */
|
|
303
|
-
const checkCommand = Command.make("check", { quiet: quietOption }, (opts) => runLintCheck(opts)).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
|
|
304
302
|
|
|
305
303
|
//#endregion
|
|
306
304
|
export { runLintCheck };
|
package/commands/lint/init.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
|
2
1
|
import { BiomeSchemaSync, Lint, ManagedSection, SavvyBaseSection, SavvyHooksSection, savvyBasePreamble, savvyHooksHygiene } from "@savvy-web/silk-effects";
|
|
3
2
|
import { Effect } from "effect";
|
|
4
3
|
import { dirname } from "node:path";
|
|
@@ -26,7 +25,7 @@ const JSONC_FORMAT = {
|
|
|
26
25
|
};
|
|
27
26
|
/** Header written when creating a fresh pre-commit hook. */
|
|
28
27
|
const PRE_COMMIT_HEADER = "#!/usr/bin/env sh\n# Pre-commit hook with savvy managed sections\n# Custom hooks can go above, below, or between the managed sections\n\n";
|
|
29
|
-
/** Header written when creating a fresh hygiene hook (post-checkout / post-merge). */
|
|
28
|
+
/** Header written when creating a fresh hygiene hook (post-checkout / post-merge / post-commit). */
|
|
30
29
|
const HYGIENE_HEADER = "#!/usr/bin/env sh\n# Managed by savvy-hooks\n# Custom hooks can go above or below the managed section\n\n";
|
|
31
30
|
/**
|
|
32
31
|
* Check if a preset includes the ShellScripts handler.
|
|
@@ -127,15 +126,6 @@ function syncBiomeSchemas() {
|
|
|
127
126
|
for (const configPath of result.updated) yield* Effect.log(`${CHECK_MARK} Updated $schema in ${configPath}`);
|
|
128
127
|
});
|
|
129
128
|
}
|
|
130
|
-
/* v8 ignore start -- CLI option definitions; handler tested individually */
|
|
131
|
-
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite the pre-commit hook and config file entirely (managed sections in post-checkout/post-merge are never force-reset)"), Options.withDefault(false));
|
|
132
|
-
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the lint-staged config file (from repo root)"), Options.withDefault(Lint.DEFAULT_CONFIG_PATH));
|
|
133
|
-
const presetOption = Options.choice("preset", [
|
|
134
|
-
"minimal",
|
|
135
|
-
"standard",
|
|
136
|
-
"silk"
|
|
137
|
-
]).pipe(Options.withAlias("p"), Options.withDescription("Preset to use: minimal, standard, or silk"), Options.withDefault("silk"));
|
|
138
|
-
/* v8 ignore stop */
|
|
139
129
|
/** Make a file executable. */
|
|
140
130
|
function makeExecutable(path) {
|
|
141
131
|
return Effect.tryPromise({
|
|
@@ -174,7 +164,11 @@ function runLintInit(opts) {
|
|
|
174
164
|
const preCommitResults = yield* ms.syncMany(Lint.HUSKY_HOOK_PATH, [SavvyBaseSection.block(savvyBasePreamble()), Lint.savvyLintBlock(config)]);
|
|
175
165
|
yield* makeExecutable(Lint.HUSKY_HOOK_PATH);
|
|
176
166
|
yield* Effect.log(`${CHECK_MARK} ${force ? "Replaced" : "Synced"} ${Lint.HUSKY_HOOK_PATH} (${preCommitResults.map((r) => r._tag).join(", ")})`);
|
|
177
|
-
if (presetIncludesShellScripts(preset)) for (const hookPath of [
|
|
167
|
+
if (presetIncludesShellScripts(preset)) for (const hookPath of [
|
|
168
|
+
Lint.POST_CHECKOUT_HOOK_PATH,
|
|
169
|
+
Lint.POST_MERGE_HOOK_PATH,
|
|
170
|
+
Lint.POST_COMMIT_HOOK_PATH
|
|
171
|
+
]) {
|
|
178
172
|
yield* ensureHookFile(hookPath, HYGIENE_HEADER);
|
|
179
173
|
yield* ms.remove(hookPath, Lint.LegacySavvyLintHygieneDef);
|
|
180
174
|
yield* ms.sync(hookPath, SavvyHooksSection.block(savvyHooksHygiene()));
|
|
@@ -193,29 +187,6 @@ function runLintInit(opts) {
|
|
|
193
187
|
yield* Effect.log("\nDone! Lint-staged is ready to use.");
|
|
194
188
|
});
|
|
195
189
|
}
|
|
196
|
-
/**
|
|
197
|
-
* Init command implementation.
|
|
198
|
-
*
|
|
199
|
-
* @remarks
|
|
200
|
-
* Writes:
|
|
201
|
-
* - `.husky/pre-commit` — `savvy-base` preamble + `savvy-lint` tool section, in order
|
|
202
|
-
* (via `ManagedSection.syncMany`).
|
|
203
|
-
* - `.husky/post-checkout` and `.husky/post-merge` — co-owned `savvy-hooks` hygiene
|
|
204
|
-
* (idempotent, shared with `@savvy-web/commitlint`). Migrates legacy `SAVVY-LINT`
|
|
205
|
-
* hygiene blocks by removing them before writing the new section.
|
|
206
|
-
* - `lib/configs/.markdownlint-cli2.jsonc` config (when preset includes Markdown).
|
|
207
|
-
* - lint-staged config at the specified path.
|
|
208
|
-
*
|
|
209
|
-
* Users may add custom commands above, below, or between the managed sections.
|
|
210
|
-
* `--force` resets only the pre-commit hook and the config file; the hygiene sections
|
|
211
|
-
* are always reconciled with `sync`.
|
|
212
|
-
*/
|
|
213
|
-
/* v8 ignore next 3 -- CLI registration; handler tested via runLintInit */
|
|
214
|
-
const initCommand = Command.make("init", {
|
|
215
|
-
force: forceOption,
|
|
216
|
-
config: configOption,
|
|
217
|
-
preset: presetOption
|
|
218
|
-
}, (opts) => runLintInit(opts)).pipe(Command.withDescription("Initialize lint-staged configuration and husky hooks"));
|
|
219
190
|
|
|
220
191
|
//#endregion
|
|
221
192
|
export { runLintInit };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@effect/cli": "^0.75.2",
|
|
34
|
+
"@effect/cluster": "^0.59.0",
|
|
34
35
|
"@effect/platform": "^0.96.1",
|
|
35
36
|
"@effect/platform-node": "^0.107.0",
|
|
36
|
-
"@
|
|
37
|
+
"@effect/rpc": "^0.75.1",
|
|
38
|
+
"@effect/sql": "^0.51.1",
|
|
39
|
+
"@savvy-web/silk-effects": "1.2.0",
|
|
37
40
|
"effect": "^3.21.3",
|
|
38
41
|
"jsonc-effect": "^0.2.1",
|
|
39
42
|
"workspaces-effect": "^1.2.0",
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Command, Options } from "@effect/cli";
|
|
2
|
-
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect, Option } from "effect";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/commands/changeset/commands/analyze-branch.ts
|
|
7
|
-
/**
|
|
8
|
-
* `analyze-branch` command -- diff the current branch and classify every
|
|
9
|
-
* changed file.
|
|
10
|
-
*
|
|
11
|
-
* @remarks
|
|
12
|
-
* Wraps {@link BranchAnalyzer.analyzeBranch}. Returns the merge-base SHA,
|
|
13
|
-
* the per-file classification, the deduped package list affected by the
|
|
14
|
-
* branch, and the list of paths that didn't map to any release surface
|
|
15
|
-
* (candidates for an AskUserQuestion in agent workflows).
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```bash
|
|
19
|
-
* savvy changeset analyze-branch
|
|
20
|
-
* savvy changeset analyze-branch --base main --json
|
|
21
|
-
* savvy changeset analyze-branch --cwd ./project --base develop
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @internal
|
|
25
|
-
*/
|
|
26
|
-
const { BranchAnalyzer } = Changesets;
|
|
27
|
-
/* v8 ignore start -- CLI option definitions */
|
|
28
|
-
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
29
|
-
const baseOption = Options.text("base").pipe(Options.withDescription("Override the base branch (defaults to config baseBranch or origin/HEAD)"), Options.optional);
|
|
30
|
-
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
31
|
-
/* v8 ignore stop */
|
|
32
|
-
/**
|
|
33
|
-
* Render a {@link BranchAnalysis} as human-readable text.
|
|
34
|
-
*
|
|
35
|
-
* @internal
|
|
36
|
-
*/
|
|
37
|
-
function renderHuman(analysis) {
|
|
38
|
-
const lines = [];
|
|
39
|
-
lines.push(`Base branch: ${analysis.baseBranch}`);
|
|
40
|
-
lines.push(`Merge base SHA: ${analysis.mergeBaseSha}`);
|
|
41
|
-
lines.push("");
|
|
42
|
-
if (analysis.packagesAffected.length > 0) {
|
|
43
|
-
lines.push(`Packages affected (${analysis.packagesAffected.length}):`);
|
|
44
|
-
for (const p of analysis.packagesAffected) lines.push(` ${p}`);
|
|
45
|
-
} else lines.push("Packages affected: (none)");
|
|
46
|
-
lines.push("");
|
|
47
|
-
if (analysis.files.length === 0) lines.push("Changes: (no files changed)");
|
|
48
|
-
else {
|
|
49
|
-
lines.push(`Changes (${analysis.files.length}):`);
|
|
50
|
-
const statusGlyph = {
|
|
51
|
-
added: "A",
|
|
52
|
-
modified: "M",
|
|
53
|
-
deleted: "D",
|
|
54
|
-
renamed: "R",
|
|
55
|
-
copied: "C",
|
|
56
|
-
typechange: "T",
|
|
57
|
-
unmerged: "U",
|
|
58
|
-
unknown: "?"
|
|
59
|
-
};
|
|
60
|
-
for (const f of analysis.files) {
|
|
61
|
-
const glyph = statusGlyph[f.status] ?? "?";
|
|
62
|
-
const owner = f.package ?? "<unmapped>";
|
|
63
|
-
const reason = f.reason === "workspace" ? "workspace" : f.reason !== null ? `${f.reason.kind}: ${f.reason.glob}` : "—";
|
|
64
|
-
lines.push(` ${glyph} ${f.path}\t${owner}\t${reason}`);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (analysis.unmappedFiles.length > 0) {
|
|
68
|
-
lines.push("");
|
|
69
|
-
lines.push(`Unmapped (${analysis.unmappedFiles.length}):`);
|
|
70
|
-
for (const p of analysis.unmappedFiles) lines.push(` ${p}`);
|
|
71
|
-
}
|
|
72
|
-
return lines.join("\n");
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Resolve cwd + base branch, invoke `BranchAnalyzer.analyzeBranch`, and
|
|
76
|
-
* render the result. Sets `process.exitCode = 1` on `ConfigurationError`
|
|
77
|
-
* or `GitError`.
|
|
78
|
-
*
|
|
79
|
-
* @internal
|
|
80
|
-
*/
|
|
81
|
-
function runAnalyzeBranch(cwd, base, json) {
|
|
82
|
-
return Effect.gen(function* () {
|
|
83
|
-
const analyzer = yield* BranchAnalyzer;
|
|
84
|
-
const resolvedCwd = resolve(cwd);
|
|
85
|
-
const baseBranch = Option.getOrUndefined(base);
|
|
86
|
-
const analysis = yield* analyzer.analyzeBranch(resolvedCwd, baseBranch ? { baseBranch } : void 0).pipe(Effect.catchTags({
|
|
87
|
-
ConfigurationError: (err) => {
|
|
88
|
-
process.exitCode = 1;
|
|
89
|
-
return Effect.fail(err);
|
|
90
|
-
},
|
|
91
|
-
GitError: (err) => {
|
|
92
|
-
process.exitCode = 1;
|
|
93
|
-
return Effect.fail(err);
|
|
94
|
-
}
|
|
95
|
-
}));
|
|
96
|
-
const output = json ? JSON.stringify(analysis, null, 2) : renderHuman(analysis);
|
|
97
|
-
yield* Effect.log(output);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
/* v8 ignore next 6 -- CLI registration */
|
|
101
|
-
const analyzeBranchCommand = Command.make("analyze-branch", {
|
|
102
|
-
cwd: cwdOption,
|
|
103
|
-
base: baseOption,
|
|
104
|
-
json: jsonOption
|
|
105
|
-
}, ({ cwd, base, json }) => runAnalyzeBranch(cwd, base, json)).pipe(Command.withDescription("Diff the current branch and classify every changed file by owning package"));
|
|
106
|
-
|
|
107
|
-
//#endregion
|
|
108
|
-
export { analyzeBranchCommand };
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/commands/changeset/commands/classify.ts
|
|
7
|
-
/**
|
|
8
|
-
* `classify` command -- map one or more paths to their owning package.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Wraps {@link ConfigInspector.classify}. Each path resolves to a package
|
|
12
|
-
* via (in order): workspace match → `additionalScopes` glob → `versionFiles`
|
|
13
|
-
* glob → `null` (unmapped).
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```bash
|
|
17
|
-
* savvy changeset classify packages/foo/src/index.ts plugin/SKILL.md
|
|
18
|
-
* savvy changeset classify --cwd ./monorepo plugin/x.md --json
|
|
19
|
-
* ```
|
|
20
|
-
*
|
|
21
|
-
* @internal
|
|
22
|
-
*/
|
|
23
|
-
const { ConfigInspector } = Changesets;
|
|
24
|
-
/* v8 ignore start -- CLI option definitions */
|
|
25
|
-
const pathsArg = Args.text({ name: "path" }).pipe(Args.repeated);
|
|
26
|
-
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
27
|
-
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
28
|
-
/* v8 ignore stop */
|
|
29
|
-
/**
|
|
30
|
-
* Format a single {@link Classification} as a human-readable line.
|
|
31
|
-
*
|
|
32
|
-
* @internal
|
|
33
|
-
*/
|
|
34
|
-
function renderClassificationLine(c) {
|
|
35
|
-
if (c.package === null) return `${c.path}\t<unmapped>`;
|
|
36
|
-
if (c.reason === "workspace") return `${c.path}\t${c.package}\tworkspace`;
|
|
37
|
-
if (c.reason !== null) return `${c.path}\t${c.package}\t${c.reason.kind}: ${c.reason.glob}`;
|
|
38
|
-
return `${c.path}\t${c.package}`;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Resolve the cwd, invoke `ConfigInspector.classify`, and render the result.
|
|
42
|
-
* Sets `process.exitCode = 1` on `ConfigurationError`.
|
|
43
|
-
*
|
|
44
|
-
* @internal
|
|
45
|
-
*/
|
|
46
|
-
function runClassify(cwd, paths, json) {
|
|
47
|
-
return Effect.gen(function* () {
|
|
48
|
-
const inspector = yield* ConfigInspector;
|
|
49
|
-
const resolvedCwd = resolve(cwd);
|
|
50
|
-
const classifications = yield* inspector.classify(resolvedCwd, paths).pipe(Effect.catchTag("ConfigurationError", (err) => {
|
|
51
|
-
process.exitCode = 1;
|
|
52
|
-
return Effect.fail(err);
|
|
53
|
-
}));
|
|
54
|
-
if (json) {
|
|
55
|
-
yield* Effect.log(JSON.stringify(classifications, null, 2));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
for (const c of classifications) yield* Effect.log(renderClassificationLine(c));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
/* v8 ignore next 6 -- CLI registration */
|
|
62
|
-
const classifyCommand = Command.make("classify", {
|
|
63
|
-
paths: pathsArg,
|
|
64
|
-
cwd: cwdOption,
|
|
65
|
-
json: jsonOption
|
|
66
|
-
}, ({ paths, cwd, json }) => runClassify(cwd, paths, json)).pipe(Command.withDescription("Map paths to their owning package per .changeset/config.json"));
|
|
67
|
-
|
|
68
|
-
//#endregion
|
|
69
|
-
export { classifyCommand };
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/commands/changeset/commands/config-show.ts
|
|
7
|
-
/**
|
|
8
|
-
* `config show` command -- emit the resolved `.changeset/config.json`.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Wraps {@link ConfigInspector.inspect} and renders either human-readable
|
|
12
|
-
* output (default) or JSON (`--json` / `--format=json`). The same data shape
|
|
13
|
-
* the agent consumes is what the user sees, so debugging an unexpected
|
|
14
|
-
* classification result becomes a single `savvy changeset config show --json`
|
|
15
|
-
* invocation.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```bash
|
|
19
|
-
* savvy changeset config show
|
|
20
|
-
* savvy changeset config show --json
|
|
21
|
-
* savvy changeset config show ./path/to/project --json
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @internal
|
|
25
|
-
*/
|
|
26
|
-
const { ConfigInspector } = Changesets;
|
|
27
|
-
/* v8 ignore start -- CLI option definitions */
|
|
28
|
-
const dirArg = Args.directory({ name: "dir" }).pipe(Args.withDefault("."));
|
|
29
|
-
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
30
|
-
/* v8 ignore stop */
|
|
31
|
-
/**
|
|
32
|
-
* Render an {@link InspectedConfig} as human-readable text.
|
|
33
|
-
*
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
function renderHuman(config) {
|
|
37
|
-
const lines = [];
|
|
38
|
-
lines.push(`Config: ${config.configPath}`);
|
|
39
|
-
lines.push(`Project: ${config.projectDir}`);
|
|
40
|
-
lines.push(`Changelog: ${config.changelog ?? "(none)"}`);
|
|
41
|
-
lines.push(`Base: ${config.baseBranch}`);
|
|
42
|
-
lines.push(`Access: ${config.access}`);
|
|
43
|
-
if (config.ignore.length > 0) lines.push(`Ignore: ${config.ignore.join(", ")}`);
|
|
44
|
-
if (config.legacyVersionFilesUsed) {
|
|
45
|
-
lines.push("");
|
|
46
|
-
lines.push("⚠ This config still uses the deprecated top-level `versionFiles[]`.");
|
|
47
|
-
lines.push(" Migrate to `packages[<name>].versionFiles` — required for 1.0.0.");
|
|
48
|
-
}
|
|
49
|
-
lines.push("");
|
|
50
|
-
if (config.packages.length === 0) {
|
|
51
|
-
lines.push("Packages: (none declared)");
|
|
52
|
-
return lines.join("\n");
|
|
53
|
-
}
|
|
54
|
-
lines.push(`Packages (${config.packages.length}):`);
|
|
55
|
-
for (const pkg of config.packages) {
|
|
56
|
-
lines.push("");
|
|
57
|
-
lines.push(` ${pkg.name} v${pkg.version}`);
|
|
58
|
-
lines.push(` workspace: ${pkg.workspaceDir}`);
|
|
59
|
-
if (pkg.additionalScopes.length > 0) {
|
|
60
|
-
lines.push(` additionalScopes (${pkg.additionalScopes.length}):`);
|
|
61
|
-
for (const g of pkg.additionalScopes) lines.push(` - ${g}`);
|
|
62
|
-
lines.push(` additionalScopeFiles (${pkg.additionalScopeFiles.length}):`);
|
|
63
|
-
for (const f of pkg.additionalScopeFiles) lines.push(` ${f}`);
|
|
64
|
-
}
|
|
65
|
-
if (pkg.versionFiles.length > 0) {
|
|
66
|
-
lines.push(` versionFiles (${pkg.versionFiles.length}):`);
|
|
67
|
-
for (const vf of pkg.versionFiles) {
|
|
68
|
-
lines.push(` ${vf.glob} → ${vf.paths.join(", ")} (${vf.matchedFiles.length} file${vf.matchedFiles.length === 1 ? "" : "s"} matched)`);
|
|
69
|
-
for (const f of vf.matchedFiles) lines.push(` ${f}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return lines.join("\n");
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Resolve the project dir, invoke `ConfigInspector.inspect`, and render the
|
|
77
|
-
* result. Sets `process.exitCode = 1` on `ConfigurationError`.
|
|
78
|
-
*
|
|
79
|
-
* @internal
|
|
80
|
-
*/
|
|
81
|
-
function runConfigShow(dir, json) {
|
|
82
|
-
return Effect.gen(function* () {
|
|
83
|
-
const inspector = yield* ConfigInspector;
|
|
84
|
-
const resolved = resolve(dir);
|
|
85
|
-
const config = yield* inspector.inspect(resolved).pipe(Effect.catchTag("ConfigurationError", (err) => {
|
|
86
|
-
process.exitCode = 1;
|
|
87
|
-
return Effect.fail(err);
|
|
88
|
-
}));
|
|
89
|
-
const output = json ? JSON.stringify(config, null, 2) : renderHuman(config);
|
|
90
|
-
yield* Effect.log(output);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
/* v8 ignore next 6 -- CLI registration */
|
|
94
|
-
const configShowCommand = Command.make("show", {
|
|
95
|
-
dir: dirArg,
|
|
96
|
-
json: jsonOption
|
|
97
|
-
}, ({ dir, json }) => runConfigShow(dir, json)).pipe(Command.withDescription("Print the resolved .changeset/config.json"));
|
|
98
|
-
|
|
99
|
-
//#endregion
|
|
100
|
-
export { configShowCommand };
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { Args, Command, Options } from "@effect/cli";
|
|
2
|
-
import { Changesets } from "@savvy-web/silk-effects";
|
|
3
|
-
import { Effect } from "effect";
|
|
4
|
-
import { resolve } from "node:path";
|
|
5
|
-
|
|
6
|
-
//#region src/commands/changeset/commands/release-surface.ts
|
|
7
|
-
/**
|
|
8
|
-
* `release-surface` command -- list every path owned by a named package.
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* For a given package name, emits the workspace directory, every
|
|
12
|
-
* `additionalScopes` glob and its materialized files, and every
|
|
13
|
-
* `versionFiles` entry and its targets. Useful for debugging "why is this
|
|
14
|
-
* path attributed to this package?" or "what's actually in this package's
|
|
15
|
-
* release surface right now?"
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```bash
|
|
19
|
-
* savvy changeset release-surface @savvy-web/changesets
|
|
20
|
-
* savvy changeset release-surface @scope/foo --json
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* @internal
|
|
24
|
-
*/
|
|
25
|
-
const { ConfigInspector, ConfigurationError } = Changesets;
|
|
26
|
-
/* v8 ignore start -- CLI option definitions */
|
|
27
|
-
const packageArg = Args.text({ name: "package" });
|
|
28
|
-
const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Project root (defaults to the current working directory)"), Options.withDefault("."));
|
|
29
|
-
const jsonOption = Options.boolean("json").pipe(Options.withDescription("Emit JSON instead of human-readable output"), Options.withDefault(false));
|
|
30
|
-
/* v8 ignore stop */
|
|
31
|
-
/**
|
|
32
|
-
* Render a single package's resolved scope as human-readable text.
|
|
33
|
-
*
|
|
34
|
-
* @internal
|
|
35
|
-
*/
|
|
36
|
-
function renderHuman(pkg) {
|
|
37
|
-
const lines = [];
|
|
38
|
-
lines.push(`Package: ${pkg.name} v${pkg.version}`);
|
|
39
|
-
lines.push(`Workspace: ${pkg.workspaceDir}`);
|
|
40
|
-
if (pkg.additionalScopes.length === 0 && pkg.versionFiles.length === 0) {
|
|
41
|
-
lines.push("");
|
|
42
|
-
lines.push("(no additionalScopes or versionFiles — workspace dir is the entire release surface)");
|
|
43
|
-
return lines.join("\n");
|
|
44
|
-
}
|
|
45
|
-
if (pkg.additionalScopes.length > 0) {
|
|
46
|
-
lines.push("");
|
|
47
|
-
lines.push(`additionalScopes (${pkg.additionalScopes.length} glob${pkg.additionalScopes.length === 1 ? "" : "s"}):`);
|
|
48
|
-
for (const g of pkg.additionalScopes) lines.push(` - ${g}`);
|
|
49
|
-
lines.push(`Resolved files (${pkg.additionalScopeFiles.length}):`);
|
|
50
|
-
for (const f of pkg.additionalScopeFiles) lines.push(` ${f}`);
|
|
51
|
-
}
|
|
52
|
-
if (pkg.versionFiles.length > 0) {
|
|
53
|
-
lines.push("");
|
|
54
|
-
lines.push(`versionFiles (${pkg.versionFiles.length}):`);
|
|
55
|
-
for (const vf of pkg.versionFiles) {
|
|
56
|
-
lines.push(` ${vf.glob} → ${vf.paths.join(", ")}`);
|
|
57
|
-
for (const f of vf.matchedFiles) lines.push(` ${f}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return lines.join("\n");
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Resolve cwd, invoke `ConfigInspector.inspect`, find the named package's
|
|
64
|
-
* scope, and render it. Sets `process.exitCode = 1` on any error.
|
|
65
|
-
*
|
|
66
|
-
* @internal
|
|
67
|
-
*/
|
|
68
|
-
function runReleaseSurface(cwd, pkgName, json) {
|
|
69
|
-
return Effect.gen(function* () {
|
|
70
|
-
const inspector = yield* ConfigInspector;
|
|
71
|
-
const resolvedCwd = resolve(cwd);
|
|
72
|
-
const config = yield* inspector.inspect(resolvedCwd).pipe(Effect.catchTag("ConfigurationError", (err) => {
|
|
73
|
-
process.exitCode = 1;
|
|
74
|
-
return Effect.fail(err);
|
|
75
|
-
}));
|
|
76
|
-
const scope = config.packages.find((p) => p.name === pkgName);
|
|
77
|
-
if (!scope) {
|
|
78
|
-
process.exitCode = 1;
|
|
79
|
-
return yield* Effect.fail(new ConfigurationError({
|
|
80
|
-
field: `packages["${pkgName}"]`,
|
|
81
|
-
reason: `Package "${pkgName}" is not declared in .changeset/config.json#packages. Declared packages: ${config.packages.map((p) => p.name).join(", ") || "(none)"}.`
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
const output = json ? JSON.stringify(scope, null, 2) : renderHuman(scope);
|
|
85
|
-
yield* Effect.log(output);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
/* v8 ignore next 6 -- CLI registration */
|
|
89
|
-
const releaseSurfaceCommand = Command.make("release-surface", {
|
|
90
|
-
package: packageArg,
|
|
91
|
-
cwd: cwdOption,
|
|
92
|
-
json: jsonOption
|
|
93
|
-
}, ({ package: pkgName, cwd, json }) => runReleaseSurface(cwd, pkgName, json)).pipe(Command.withDescription("Print every path owned by a package — workspace dir, additionalScopes, versionFiles"));
|
|
94
|
-
|
|
95
|
-
//#endregion
|
|
96
|
-
export { releaseSurfaceCommand };
|