@savvy-web/cli 1.6.0 → 2.0.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.
@@ -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 "@effect/cli";
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
- * Typed as `unknown` at the export boundary to avoid TypeScript declaration-emit
30
- * errors from Effect's internal types. Task B7 should import and use this via
31
- * `Command.withSubcommands([changesetCommand as never])` or re-infer the type.
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 { Command, Options } from "@effect/cli";
6
- import { Effect } from "effect";
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 = Options.text("changeset-dir").pipe(Options.withDescription("Path to the changeset directory"), Options.withDefault(".changeset"));
29
- const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Only output warnings from lint check"), Options.withDefault(false));
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.all([
45
- steps.changeset,
46
- steps.commit,
47
- steps.lint
48
- ], {
49
- concurrency: 1,
50
- mode: "validate"
51
- }).pipe(Effect.asVoid);
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 { Command, Options } from "@effect/cli";
1
+ import { WorkspaceDiscovery } from "@effected/workspaces";
2
2
  import { Data, Effect } from "effect";
3
- import { WorkspaceDiscovery } from "workspaces-effect";
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(process.cwd()).pipe(Effect.mapError((e) => new CleanError({
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.isRootWorkspace);
121
- const roots = packages.filter((p) => p.isRootWorkspace);
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.isRootWorkspace);
139
- const rootGroups = groups.filter((g) => g.pkg.isRootWorkspace);
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 = Options.text("globs").pipe(Options.withAlias("g"), Options.withDescription(`Comma-separated glob patterns to remove from each workspace root (default: ${DEFAULT_GLOBS.join(",")})`), Options.withDefault(DEFAULT_GLOBS.join(",")));
169
- const dryRunOption = Options.boolean("dry-run").pipe(Options.withAlias("n"), Options.withDescription("Report what would be removed without deleting anything"), Options.withDefault(false));
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
@@ -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.catchAll(discovery.listPackages(), () => Effect.succeed([]))).filter((pkg) => !pkg.private || pkg.publishConfig?.access !== void 0).map((pkg) => pkg.name);
82
- const result = yield* Effect.catchAll(versioning.detect(publishableNames, process.cwd()), () => Effect.succeed({ type: "single" }));
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.catchAll(Commitlint.detectScopes, () => Effect.succeed([]));
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("");
@@ -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 "@effect/cli";
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 { Command } from "@effect/cli";
1
+ import { Git } from "@effected/git";
2
2
  import { Commitlint } from "@savvy-web/silk-effects";
3
3
  import { Effect } from "effect";
4
- import { execFile } from "node:child_process";
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
- async function getRepoRoot() {
61
- try {
62
- const { stdout } = await execFileP("git", ["rev-parse", "--show-toplevel"]);
63
- return stdout.trim();
64
- } catch {
65
- return process.cwd();
66
- }
67
- }
68
- async function runCommitlintLast() {
69
- const root = await getRepoRoot();
70
- const { command, args } = buildCommitlintInvocation(await Commitlint.detectPackageManager(root), await Commitlint.readCommitlintConfigPath(root));
71
- try {
72
- await execFileP(command, args, { cwd: root });
73
- return false;
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 commitlintFailed = yield* Effect.promise(runCommitlintLast);
106
- const sigStatus = yield* Effect.promise(readSignatureStatus);
107
- const body = yield* Effect.promise(readLastCommitBody);
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
@@ -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 "@effect/cli";
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 */
@@ -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 = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing config files and hooks across all tools"), Options.withDefault(false));
28
- const commitConfigOption = Options.text("commit-config").pipe(Options.withDescription("Relative path for the commitlint config file"), Options.withDefault(DEFAULT_COMMIT_CONFIG));
29
- const lintConfigOption = Options.text("lint-config").pipe(Options.withDescription("Relative path for the lint-staged config file"), Options.withDefault(DEFAULT_LINT_CONFIG));
30
- const lintPresetOption = Options.choice("lint-preset", [
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(Options.withDescription("lint-staged preset: minimal, standard, or silk"), Options.withDefault("silk"));
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.
@@ -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 { parse } from "jsonc-effect";
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) if ((yield* parse(yield* fs.readFileString(configPath))).$schema === expectedSchema) statuses.push({
115
- path: configPath,
116
- matches: true
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: false
117
+ matches: true
122
118
  });
123
- warnings.push(`${WARNING} ${configPath}: biome $schema is outdated.\n Run 'savvy init' to update it.`);
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.catchAll(() => Effect.succeed({
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
  })));
@@ -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 { parse, stringify } from "yaml";
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
- singleQuote: false
23
- };
21
+ lineWidth: 0
22
+ });
24
23
  /** Repeated file path arguments. */
25
- const filesArg = Args.repeated(Args.file({
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.sync(() => {
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 parsed = parse(readFileSync(filepath, "utf-8"));
42
- writeFileSync(filepath, stringify(Lint.PnpmWorkspace.sortContent(parsed), YAML_STRINGIFY_OPTIONS), "utf-8");
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* () {
@@ -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 "@effect/cli";
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 */
@@ -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 { applyEdits, modify, parse } from "jsonc-effect";
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 = yield* applyEdits(updatedText, edits);
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 = yield* applyEdits(updatedText, edits);
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 = Args.text({ name: "url" });
32
- const refOption = Options.text("ref").pipe(Options.withDescription("Ref (tag, branch, or commit) to check out"));
33
- const purposeOption = Options.text("purpose").pipe(Options.withDescription("Why this repo is vendored"));
34
- const nameOption = Options.text("name").pipe(Options.withDescription("Vendored directory name; defaults to the URL's last path segment"), Options.optional);
35
- const sparseOption = Options.text("sparse").pipe(Options.withDescription("Sparse-checkout path; repeatable"), Options.repeated);
36
- const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to add within"), Options.withDefault("."));
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 = Args.text({ name: "name" });
56
- const noteTextArg = Args.text({ name: "text" });
57
- const noteIdArg = Args.text({ name: "id" });
58
- const intoOption = Options.choice("into", ["layout", "startHere"]).pipe(Options.withDescription("Curated orientation field to promote the note into"));
59
- const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root whose manifest holds the notes"), Options.withDefault("."));
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
- cwd: cwdOption
63
- });
64
- const addLeaf = Command.make("add", { note: noteTextArg }, ({ note }) => Effect.gen(function* () {
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", { id: noteIdArg }, ({ id }) => Effect.gen(function* () {
72
- const { name, cwd } = yield* _noteGroup;
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 { name, cwd } = yield* _noteGroup;
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 = Args.text({ name: "name" });
30
- const refArg = Args.text({ name: "ref" });
31
- const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to pin within"), Options.withDefault("."));
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 = Options.boolean("json").pipe(Options.withDescription("Emit the structured drift report as JSON"), Options.withDefault(false));
29
- const cwdOption = Options.directory("cwd").pipe(Options.withDescription("Repo root to inspect"), Options.withDefault("."));
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.