@savvy-web/cli 3.7.14 → 3.7.15

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.
@@ -1,6 +1,7 @@
1
+ import { DisplayPath } from "../../../internal/display-path.js";
1
2
  import { Report } from "../../../internal/report.js";
2
3
  import { Argument, Command } from "effect/cli";
3
- import { isAbsolute, relative, resolve } from "node:path";
4
+ import { resolve } from "node:path";
4
5
  import { CliExit, Doc } from "@effected/cli";
5
6
  import { Changesets } from "@savvy-web/silk-effects";
6
7
  import { Effect } from "effect";
@@ -38,15 +39,6 @@ const FINDING_COLUMNS = [
38
39
  { header: "rule" },
39
40
  { header: "message" }
40
41
  ];
41
- /**
42
- * A finding's file as the report and the runner show it: relative to the
43
- * working directory when it lies under it (what a GitHub Actions annotation
44
- * resolves against the workspace), absolute otherwise.
45
- */
46
- const displayPath = (cwd, file) => {
47
- const rel = relative(cwd, file);
48
- return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? file : rel;
49
- };
50
42
  /* v8 ignore next */
51
43
  const dirArg = Argument.Directory("dir").pipe(Argument.withDefault(".changeset"));
52
44
  /**
@@ -89,7 +81,7 @@ function changesetCheckSection(dir) {
89
81
  const cwd = resolve();
90
82
  const blocks = [];
91
83
  for (const [file, fileMessages] of byFile) {
92
- const display = displayPath(cwd, file);
84
+ const display = DisplayPath.under(cwd, file);
93
85
  blocks.push(Doc.section(Doc.file(display), [Doc.table(FINDING_COLUMNS, fileMessages.map((msg) => [
94
86
  `${msg.line}:${msg.column}`,
95
87
  msg.rule,
@@ -1,7 +1,7 @@
1
1
  import { Report } from "../../../internal/report.js";
2
2
  import { CommandError } from "../../../internal/command-error.js";
3
3
  import { join } from "node:path";
4
- import { CliDoc, CliExit, Doc } from "@effected/cli";
4
+ import { CliDoc, CliExit, Doc, Fmt } from "@effected/cli";
5
5
  import { Changesets } from "@savvy-web/silk-effects";
6
6
  import { Data, Effect, Option, Result, Schema } from "effect";
7
7
  import { TerminalEnv } from "@effected/env";
@@ -192,7 +192,7 @@ function ensureChangesetDir(root) {
192
192
  },
193
193
  catch: (error) => new InitError({
194
194
  step: ".changeset directory",
195
- reason: error instanceof Error ? error.message : String(error)
195
+ reason: Fmt.describe(error)
196
196
  })
197
197
  });
198
198
  }
@@ -234,7 +234,7 @@ function handleConfig(changesetDir, repoSlug, force) {
234
234
  },
235
235
  catch: (error) => new InitError({
236
236
  step: ".changeset/config.json",
237
- reason: error instanceof Error ? error.message : String(error)
237
+ reason: Fmt.describe(error)
238
238
  })
239
239
  });
240
240
  }
@@ -330,7 +330,7 @@ function handleBaseMarkdownlint(root) {
330
330
  } catch (error) {
331
331
  return yield* Effect.fail(new InitError({
332
332
  step: "markdownlint config",
333
- reason: error instanceof Error ? error.message : String(error)
333
+ reason: Fmt.describe(error)
334
334
  }));
335
335
  }
336
336
  let parsed = yield* Jsonc.parse(text);
@@ -363,7 +363,7 @@ function handleBaseMarkdownlint(root) {
363
363
  } catch (error) {
364
364
  return yield* Effect.fail(new InitError({
365
365
  step: "markdownlint config",
366
- reason: error instanceof Error ? error.message : String(error)
366
+ reason: Fmt.describe(error)
367
367
  }));
368
368
  }
369
369
  return `Updated ${foundPath}`;
@@ -371,7 +371,7 @@ function handleBaseMarkdownlint(root) {
371
371
  if (error instanceof InitError) return Effect.fail(error);
372
372
  return Effect.fail(new InitError({
373
373
  step: "markdownlint config",
374
- reason: error instanceof Error ? error.message : String(error)
374
+ reason: Fmt.describe(error)
375
375
  }));
376
376
  }));
377
377
  }
@@ -411,7 +411,7 @@ function handleChangesetMarkdownlint(changesetDir, root, force) {
411
411
  },
412
412
  catch: (error) => new InitError({
413
413
  step: ".changeset/.markdownlint.json",
414
- reason: error instanceof Error ? error.message : String(error)
414
+ reason: Fmt.describe(error)
415
415
  })
416
416
  });
417
417
  }
@@ -40,7 +40,7 @@ function runValidateFile(filePath) {
40
40
  return Effect.gen(function* () {
41
41
  const result = yield* Effect.try({
42
42
  try: () => ChangesetLinter.validateFile(filePath),
43
- catch: (error) => CommandError.from(error instanceof Error ? error : String(error), { message: `could not validate ${filePath}` })
43
+ catch: (error) => CommandError.from(error, { message: `could not validate ${filePath}` })
44
44
  });
45
45
  if (result.length > 0) {
46
46
  yield* Report.print([Report.verbatim(result.map((msg) => `${msg.file}:${msg.line}:${msg.column} ${msg.rule} ${msg.message}`).join("\n"))]);
package/commands/clean.js CHANGED
@@ -2,7 +2,7 @@ import { Report } from "../internal/report.js";
2
2
  import { CommandError } from "../internal/command-error.js";
3
3
  import { Command, Flag } from "effect/cli";
4
4
  import { join, sep } from "node:path";
5
- import { CliInteractive } from "@effected/cli";
5
+ import { CliInteractive, Fmt } from "@effected/cli";
6
6
  import { Console, Effect, PubSub } from "effect";
7
7
  import { WorkspaceDiscovery } from "@effected/workspaces";
8
8
  import { glob, realpath, rm } from "node:fs/promises";
@@ -87,7 +87,7 @@ function removeTargets(targets, dryRun) {
87
87
  }),
88
88
  onFailure: (e) => ({
89
89
  target,
90
- reason: e instanceof Error ? e.message : String(e)
90
+ reason: Fmt.describe(e)
91
91
  })
92
92
  })), { concurrency: REMOVE_CONCURRENCY });
93
93
  return {
@@ -1,5 +1,28 @@
1
+ import { LocalExec } from "@effected/commands";
2
+
1
3
  // src/commands/commit/commitlint-invocation.ts
2
4
  /**
5
+ * Shared, pure builder for the package-manager-aware `commitlint` CLI
6
+ * invocation.
7
+ *
8
+ * @remarks
9
+ * Both commit-message validators shell out to the SAME `commitlint` binary
10
+ * so the real Silk preset stays the single source of truth — they differ only
11
+ * in the trailing mode args:
12
+ *
13
+ * - `savvy commit hook post-commit-verify` replays the most recent commit
14
+ * (`--last`), AFTER it exists.
15
+ * - `savvy commit lint <file>` validates a candidate message file
16
+ * (`--edit <file>`), BEFORE it is committed.
17
+ *
18
+ * Keeping the builder pure and package-manager-aware in one place means a
19
+ * change to how `commitlint` is invoked lands for both paths at once. The
20
+ * per-package-manager argv prefix comes from `@effected/commands`'
21
+ * `LocalExec.prefixes`, the kit's single home of that knowledge.
22
+ *
23
+ * @internal
24
+ */
25
+ /**
3
26
  * Build the `commitlint` invocation for the detected package manager.
4
27
  *
5
28
  * @param pm - The detected package manager.
@@ -15,28 +38,11 @@ function buildCommitlintInvocation(pm, configPath, tail) {
15
38
  configPath,
16
39
  ...tail
17
40
  ] : ["commitlint", ...tail];
18
- switch (pm) {
19
- case "pnpm": return {
20
- command: "pnpm",
21
- args: ["exec", ...base]
22
- };
23
- case "yarn": return {
24
- command: "yarn",
25
- args: ["exec", ...base]
26
- };
27
- case "bun": return {
28
- command: "bunx",
29
- args: base
30
- };
31
- case "npm": return {
32
- command: "npx",
33
- args: [
34
- "--no",
35
- "--",
36
- ...base
37
- ]
38
- };
39
- }
41
+ const [command = pm, ...prefixArgs] = LocalExec.prefixes(pm).prefix;
42
+ return {
43
+ command,
44
+ args: [...prefixArgs, ...base]
45
+ };
40
46
  }
41
47
 
42
48
  export { buildCommitlintInvocation };
@@ -3,8 +3,8 @@ import { Command } from "effect/cli";
3
3
  import { Commitlint } from "@savvy-web/silk-effects";
4
4
  import { Effect } from "effect";
5
5
  import { Git } from "@effected/git";
6
- import { promisify } from "node:util";
7
- import { execFile } from "node:child_process";
6
+ import { Run } from "@effected/commands";
7
+ import { ChildProcess } from "effect/process";
8
8
 
9
9
  // src/commands/commit/hooks/post-commit-verify.ts
10
10
  /**
@@ -13,7 +13,6 @@ import { execFile } from "node:child_process";
13
13
  *
14
14
  * @internal
15
15
  */
16
- const execFileP = promisify(execFile);
17
16
  function buildPostCommitAdvice(i) {
18
17
  const lines = [];
19
18
  if (i.commitlintFailed) lines.push("The commit you just created fails commitlint validation. Run pnpm exec commitlint --last to see the violations, then fix with git commit --amend.");
@@ -33,25 +32,28 @@ function buildPostCommitAdvice(i) {
33
32
  const getRepoRoot = Effect.gen(function* () {
34
33
  return yield* (yield* Git).repoRoot(process.cwd()).pipe(Effect.catch(() => Effect.succeed(process.cwd())));
35
34
  });
35
+ /**
36
+ * Replay commitlint against the most recent commit; `true` means it FAILED.
37
+ *
38
+ * A launcher that never starts counts as a failure too — `Run.succeeds`
39
+ * collapses it to `false`, the prior execFile catch's contract.
40
+ *
41
+ * @internal
42
+ */
36
43
  function runCommitlintLast(root) {
37
- return (async () => {
38
- const pm = await Commitlint.detectPackageManager(root);
39
- const configPath = await Commitlint.readCommitlintConfigPath(root);
44
+ return Effect.gen(function* () {
45
+ const pm = yield* Effect.promise(() => Commitlint.detectPackageManager(root));
46
+ const configPath = yield* Effect.promise(() => Commitlint.readCommitlintConfigPath(root));
40
47
  const { command, args } = buildCommitlintInvocation(pm, configPath, ["--last"]);
41
- try {
42
- await execFileP(command, args, { cwd: root });
43
- return false;
44
- } catch {
45
- return true;
46
- }
47
- })();
48
+ return !(yield* Run.succeeds(ChildProcess.make(command, args, { cwd: root })));
49
+ });
48
50
  }
49
51
  const postCommitVerifyCommand = Command.make("post-commit-verify", {}, () => Effect.gen(function* () {
50
52
  const git = yield* Git;
51
53
  const branch = yield* Commitlint.readBranchInfo();
52
54
  const signing = yield* Commitlint.readSigningDiagnostic();
53
55
  const root = yield* getRepoRoot;
54
- const commitlintFailed = yield* Effect.promise(() => runCommitlintLast(root));
56
+ const commitlintFailed = yield* runCommitlintLast(root);
55
57
  const lastCommit = yield* git.commitInfo(root).pipe(Effect.catch(() => Effect.succeed({
56
58
  signatureStatus: "N",
57
59
  message: ""
@@ -69,4 +71,4 @@ const postCommitVerifyCommand = Command.make("post-commit-verify", {}, () => Eff
69
71
  if (advice !== null) yield* Effect.sync(() => process.stdout.write(`${JSON.stringify(Commitlint.postToolUseAdvise(advice))}\n`));
70
72
  }).pipe(Effect.provide(Commitlint.HookSilencer))).pipe(Command.withDescription("Verify the most recent commit (commitlint replay + signature + closes trailer)"));
71
73
 
72
- export { buildPostCommitAdvice, postCommitVerifyCommand };
74
+ export { buildPostCommitAdvice, postCommitVerifyCommand, runCommitlintLast };
@@ -4,8 +4,8 @@ import { Argument, CliError, Command } from "effect/cli";
4
4
  import { Commitlint } from "@savvy-web/silk-effects";
5
5
  import { Effect } from "effect";
6
6
  import { Git } from "@effected/git";
7
- import { promisify } from "node:util";
8
- import { execFile } from "node:child_process";
7
+ import { Run } from "@effected/commands";
8
+ import { ChildProcess } from "effect/process";
9
9
 
10
10
  // src/commands/commit/lint.ts
11
11
  /**
@@ -21,7 +21,6 @@ import { execFile } from "node:child_process";
21
21
  *
22
22
  * @internal
23
23
  */
24
- const execFileP = promisify(execFile);
25
24
  /** The repo root via `@effected/git`, degrading to `process.cwd()`. */
26
25
  const getRepoRoot = Effect.gen(function* () {
27
26
  return yield* (yield* Git).repoRoot(process.cwd()).pipe(Effect.catch(() => Effect.succeed(process.cwd())));
@@ -31,31 +30,27 @@ const getRepoRoot = Effect.gen(function* () {
31
30
  *
32
31
  * A zero exit resolves to `passed: true`; a non-zero exit (commitlint's way
33
32
  * of signalling violations) resolves to `passed: false` with the captured
34
- * diagnostics — the promise never rejects.
33
+ * diagnostics. A run that never starts (the launcher is missing) or whose
34
+ * output is unusable also resolves to `passed: false`, its message standing
35
+ * in for stderr — the effect never fails.
35
36
  *
36
37
  * @internal
37
38
  */
38
39
  function runCommitlintEdit(root, file) {
39
- return (async () => {
40
- const pm = await Commitlint.detectPackageManager(root);
41
- const configPath = await Commitlint.readCommitlintConfigPath(root);
40
+ return Effect.gen(function* () {
41
+ const pm = yield* Effect.promise(() => Commitlint.detectPackageManager(root));
42
+ const configPath = yield* Effect.promise(() => Commitlint.readCommitlintConfigPath(root));
42
43
  const { command, args } = buildCommitlintInvocation(pm, configPath, ["--edit", file]);
43
- try {
44
- const { stdout, stderr } = await execFileP(command, args, { cwd: root });
45
- return {
46
- passed: true,
47
- stdout,
48
- stderr
49
- };
50
- } catch (error) {
51
- const e = error;
52
- return {
53
- passed: false,
54
- stdout: e.stdout ?? "",
55
- stderr: e.stderr ?? e.message ?? String(error)
56
- };
57
- }
58
- })();
44
+ return yield* Run.collect(ChildProcess.make(command, args, { cwd: root })).pipe(Effect.map((output) => ({
45
+ passed: output.exitCode === 0,
46
+ stdout: output.stdout,
47
+ stderr: output.stderr
48
+ })), Effect.catch((error) => Effect.succeed({
49
+ passed: false,
50
+ stdout: "",
51
+ stderr: error.message
52
+ })));
53
+ });
59
54
  }
60
55
  /**
61
56
  * Validate a candidate commit-message file against the Silk commitlint preset.
@@ -71,8 +66,7 @@ function runCommitlintEdit(root, file) {
71
66
  */
72
67
  function runCommitLint(file) {
73
68
  return Effect.gen(function* () {
74
- const root = yield* getRepoRoot;
75
- const { passed, stdout, stderr } = yield* Effect.promise(() => runCommitlintEdit(root, file));
69
+ const { passed, stdout, stderr } = yield* runCommitlintEdit(yield* getRepoRoot, file);
76
70
  if (stdout.length > 0) yield* Effect.sync(() => process.stdout.write(stdout));
77
71
  if (stderr.length > 0) yield* Effect.sync(() => process.stderr.write(stderr));
78
72
  if (!passed) return yield* Effect.fail(new CliError.UserError({ cause: `Commit message at ${file} failed commitlint validation` }));
@@ -84,4 +78,4 @@ const fileArg = Argument.File("file", { mustExist: true });
84
78
  /* v8 ignore next 3 -- CLI registration; handler tested via runCommitLint */
85
79
  const lintCommand = Command.make("lint", { file: fileArg }, ({ file }) => runCommitLint(file)).pipe(Command.withDescription("Validate a candidate commit-message file against the Silk commitlint preset"));
86
80
 
87
- export { lintCommand, runCommitLint };
81
+ export { lintCommand, runCommitLint, runCommitlintEdit };
@@ -1,8 +1,9 @@
1
+ import { DisplayPath } from "../../internal/display-path.js";
1
2
  import { Report } from "../../internal/report.js";
2
3
  import { Argument, Command, Flag } from "effect/cli";
3
4
  import { CliExit, Doc } from "@effected/cli";
4
5
  import { Lint } from "@savvy-web/silk-effects";
5
- import { Effect, Path } from "effect";
6
+ import { Effect } from "effect";
6
7
 
7
8
  // src/commands/lint/text.ts
8
9
  /**
@@ -44,7 +45,6 @@ const stagedFlag = Flag.Boolean("staged").pipe(Flag.withDescription("Read each f
44
45
  * @internal
45
46
  */
46
47
  const runLintText = (files, options = {}) => Effect.gen(function* () {
47
- const path = yield* Path.Path;
48
48
  const cwd = options.cwd ?? ".";
49
49
  const staged = options.staged ?? false;
50
50
  const targets = files.length > 0 ? files : yield* Lint.TextFiles.listTracked(cwd, { staged });
@@ -57,15 +57,10 @@ const runLintText = (files, options = {}) => Effect.gen(function* () {
57
57
  yield* Report.print([Report.ok(`${targets.length} ${targets.length === 1 ? "file is" : "files are"} grep-visible text`)]);
58
58
  return findings;
59
59
  }
60
- const root = path.resolve(cwd);
61
- const annotationPath = (file) => {
62
- const relative = path.relative(root, path.resolve(file));
63
- return relative.startsWith("..") || path.isAbsolute(relative) ? file : relative;
64
- };
65
60
  const failedFiles = (/* @__PURE__ */ new Set([...findings.map((finding) => finding.path), ...unreadable.map((error) => error.path)])).size;
66
61
  yield* Report.print([...findings.flatMap((finding) => [Report.fail(`${finding.location} ${finding.message}`), Doc.annotation({
67
62
  level: "error",
68
- file: annotationPath(finding.path),
63
+ file: DisplayPath.under(cwd, finding.path),
69
64
  line: finding.line,
70
65
  col: finding.column,
71
66
  title: "grep-invisible text"
@@ -0,0 +1,28 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
2
+
3
+ // src/internal/display-path.ts
4
+ /**
5
+ * How a reported file path is shown.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /**
10
+ * The one rule for showing a finding's file in a report or a GitHub Actions
11
+ * annotation.
12
+ *
13
+ * @internal
14
+ */
15
+ var DisplayPath = class {
16
+ constructor() {}
17
+ /**
18
+ * `file` relative to `cwd` when it lies strictly under it (what a GitHub
19
+ * Actions annotation resolves against the workspace), else `file` exactly
20
+ * as given — a path outside `cwd`, on another drive, or `cwd` itself.
21
+ */
22
+ static under = (cwd, file) => {
23
+ const rel = relative(resolve(cwd), resolve(file));
24
+ return rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel) ? file : rel;
25
+ };
26
+ };
27
+
28
+ export { DisplayPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/cli",
3
- "version": "3.7.14",
3
+ "version": "3.7.15",
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",
@@ -37,20 +37,20 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@effect/platform-node": "^4.0.6",
40
- "@effected/cli": "^0.22.2",
41
- "@effected/commands": "^0.16.0",
42
- "@effected/config-file": "^0.20.1",
43
- "@effected/engine": "^0.8.0",
44
- "@effected/env": "^0.5.0",
45
- "@effected/git": "^0.25.1",
46
- "@effected/glob": "^0.14.0",
47
- "@effected/jsonc": "^0.20.0",
40
+ "@effected/cli": "^0.23.0",
41
+ "@effected/commands": "^0.17.0",
42
+ "@effected/config-file": "^0.21.0",
43
+ "@effected/engine": "^0.9.0",
44
+ "@effected/env": "^0.6.0",
45
+ "@effected/git": "^0.26.0",
46
+ "@effected/glob": "^0.15.0",
47
+ "@effected/jsonc": "^0.20.1",
48
48
  "@effected/templates": "^0.14.1",
49
49
  "@effected/toml": "^0.16.0",
50
- "@effected/walker": "^0.20.1",
51
- "@effected/workspaces": "^0.39.0",
50
+ "@effected/walker": "^0.21.0",
51
+ "@effected/workspaces": "^0.39.1",
52
52
  "@effected/yaml": "^0.25.1",
53
- "@savvy-web/silk-effects": "^10.1.0",
53
+ "@savvy-web/silk-effects": "^10.2.0",
54
54
  "effect": "^4.0.6",
55
55
  "ink": "^8.0.0",
56
56
  "react": "^19.3.1"
package/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  * @packageDocumentation
8
8
  */
9
9
  /** @internal */
10
- const CLI_VERSION = "3.7.14";
10
+ const CLI_VERSION = "3.7.15";
11
11
 
12
12
  export { CLI_VERSION };