@savvy-web/cli 3.5.0 → 3.6.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.
Files changed (43) hide show
  1. package/cli/index.js +19 -3
  2. package/commands/changeset/commands/check.js +82 -21
  3. package/commands/changeset/commands/config-validate.js +9 -6
  4. package/commands/changeset/commands/deps-detect.js +2 -2
  5. package/commands/changeset/commands/deps-regen.js +94 -48
  6. package/commands/changeset/commands/init.js +56 -23
  7. package/commands/changeset/commands/transform.js +82 -8
  8. package/commands/changeset/commands/validate-file.js +12 -11
  9. package/commands/changeset/commands/version.js +48 -17
  10. package/commands/changeset/index.js +3 -3
  11. package/commands/changeset/utils/config-gate.js +12 -10
  12. package/commands/check-section.js +34 -0
  13. package/commands/check.js +48 -39
  14. package/commands/clean/progress.js +71 -0
  15. package/commands/clean/view.js +26 -0
  16. package/commands/clean.js +113 -50
  17. package/commands/commit/check.js +80 -77
  18. package/commands/commit/init.js +21 -10
  19. package/commands/commit/lint.js +2 -2
  20. package/commands/init.js +122 -34
  21. package/commands/lint/check.js +126 -171
  22. package/commands/lint/init.js +52 -33
  23. package/commands/lint/text.js +28 -13
  24. package/commands/repos/commands/add.js +14 -17
  25. package/commands/repos/commands/deregister.js +37 -18
  26. package/commands/repos/commands/note.js +78 -26
  27. package/commands/repos/commands/pin.js +39 -24
  28. package/commands/repos/commands/remove.js +30 -56
  29. package/commands/repos/commands/rename.js +32 -23
  30. package/commands/repos/commands/restore.js +42 -56
  31. package/commands/repos/commands/status.js +43 -47
  32. package/commands/repos/commands/sync.js +11 -40
  33. package/commands/repos/index.js +7 -10
  34. package/commands/repos/shared.js +129 -0
  35. package/index.d.ts +162 -61
  36. package/internal/command-error.js +60 -0
  37. package/internal/confirm.js +52 -0
  38. package/internal/failure-line.js +26 -12
  39. package/internal/report.js +101 -0
  40. package/main.js +23 -16
  41. package/package.json +17 -12
  42. package/version.js +1 -1
  43. package/internal/output.js +0 -52
@@ -1,4 +1,4 @@
1
- import { Cause } from "effect";
1
+ import { CliDoc } from "@effected/cli";
2
2
 
3
3
  //#region src/internal/failure-line.ts
4
4
  /** Where a defect report asks to be filed; mirrors `package.json#bugs.url`. */
@@ -29,6 +29,8 @@ const describe = (error) => {
29
29
  }
30
30
  return String(error);
31
31
  };
32
+ /** Whether a typed failure draws itself through the kit's `CliDoc` protocol. */
33
+ const drawsItself = (error) => typeof error === "object" && error !== null && CliDoc in error && typeof error[CliDoc] === "function";
32
34
  /**
33
35
  * Renders a failure for `CliRuntime.main`'s `render` option.
34
36
  *
@@ -37,23 +39,35 @@ const describe = (error) => {
37
39
  * or a defect, so nothing here guesses that from the error's shape.
38
40
  *
39
41
  * - A typed failure is an expected outcome and reads as one line. The kit's
40
- * default is `String(error)`, which prints a `Data.TaggedError` with no
41
- * `message` as its bare tag and drops the fields that say what went wrong
42
- * (`CleanError`, with the `reason` lost), so this prefers the error's own
43
- * message, then its tag with its fields, then `String`.
42
+ * default report (`details.defaultLines`) is a status line plus a cleaned
43
+ * stack, which is noise for an expected outcome, and a `Data.TaggedError`
44
+ * with no `message` loses the fields that say what went wrong (`CleanError`,
45
+ * with the `reason` lost), so this prefers the error's own message, then
46
+ * its tag with its fields, then `String`.
47
+ * - A typed failure whose error implements the kit's `CliDoc` protocol (the
48
+ * CLI's own `CommandError`) has already said how it reads, so it gets the
49
+ * kit's report of that document (`details.defaultLines`), drawn for the
50
+ * run's audience, instead of one line.
44
51
  * - A defect — a `die`, a thrown exception — is a bug in savvy. It gets the
45
- * issue-report treatment: a headline, the whole pretty-printed cause with
46
- * its stack, and where to report it.
52
+ * kit's default report (`details.defaultLines`: the status line and the
53
+ * program's own stack frames, drawn for the run's audience) followed by
54
+ * where to report it.
47
55
  *
48
56
  * @internal
49
57
  */
50
58
  var FailureLine = class {
51
59
  constructor() {}
52
- static render = (error, details) => details.isDefect ? [
53
- `savvy hit an unexpected error: ${describe(error)}`,
54
- ...Cause.pretty(details.cause).split("\n"),
55
- `This is a bug in savvy. Please report it with the output above at ${ISSUES_URL}`
56
- ] : describe(error);
60
+ static render = (error, details) => {
61
+ if (details.isDefect) return [...details.defaultLines, `This is a bug in savvy. Please report it with the output above at ${ISSUES_URL}`];
62
+ return drawsItself(error) ? details.defaultLines : describe(error);
63
+ };
64
+ /**
65
+ * One line for a typed failure: its own message, else its tag with its
66
+ * fields, else `String`. What {@link FailureLine.render} prints for a typed
67
+ * failure that does not draw itself, and what `CommandError.from` keeps of
68
+ * a foreign error.
69
+ */
70
+ static describe = (error) => describe(error);
57
71
  };
58
72
 
59
73
  //#endregion
@@ -0,0 +1,101 @@
1
+ import { Doc, Status } from "@effected/cli";
2
+ import { Effect } from "effect";
3
+ import { TerminalEnv } from "@effected/env";
4
+
5
+ //#region src/internal/report.ts
6
+ /** A status line, with any detail lines muted and indented under it. */
7
+ const item = (status, text, detail) => {
8
+ const head = [
9
+ Doc.status(Status.core, status),
10
+ " ",
11
+ ...Array.isArray(text) ? text : [text]
12
+ ];
13
+ return detail.length === 0 ? Doc.line(head) : Doc.lines([head, ...detail.map((line) => Doc.text(` ${line}`, "muted"))]);
14
+ };
15
+ /**
16
+ * The result a person ran a command to see, as `Doc` blocks.
17
+ *
18
+ * @remarks
19
+ * A command builds its report from these blocks and writes it once with
20
+ * {@link Report.print}, which renders for the run's audience on stdout: ANSI
21
+ * for a person at a terminal, plain text for an agent or a pipe, and a GitHub
22
+ * Actions log under Actions. Only the status glyph and headings are painted,
23
+ * so the text reads the same piped. Progress and diagnostics stay on
24
+ * `Effect.log*`, which the CLI logger sends to stderr; a failure is the
25
+ * runtime's to report, never a block here — fail with `CommandError`
26
+ * (`./command-error.ts`), which draws itself.
27
+ *
28
+ * `Report` covers what every command repeats: status items, headings, detail
29
+ * lines, the summary and the audience-aware print. Anything richer — a
30
+ * table, a titled section, a callout, a tree, a file link, a GitHub Actions
31
+ * annotation — is built with `Doc.*` from `@effected/cli` directly and mixed
32
+ * into the same document (both build `Block`s); `Report` deliberately does not
33
+ * re-wrap those constructors.
34
+ *
35
+ * @internal
36
+ */
37
+ var Report = class {
38
+ constructor() {}
39
+ /** A passing item: `✓ text`, with optional muted detail lines under it. */
40
+ static ok = (text, ...detail) => item("success", text, detail);
41
+ /** An item that passed with a caveat: `⚠ text`. */
42
+ static warn = (text, ...detail) => item("warning", text, detail);
43
+ /** A finding — still output, not a crash: `✗ text`. */
44
+ static fail = (text, ...detail) => item("failure", text, detail);
45
+ /** An item that is absent or not applicable — neither a pass nor a finding: `↷ text`. */
46
+ static skip = (text, ...detail) => item("skip", text, detail);
47
+ /** A section title. */
48
+ static heading = (text) => Doc.heading(2, text);
49
+ /** Supporting detail under the block before it, indented and muted. */
50
+ static detail = (text) => Doc.line(Doc.text(` ${text}`, "muted"));
51
+ /**
52
+ * A line printed as given; an empty string is a blank separator between
53
+ * blocks of one document. A document holding only blank lines renders as
54
+ * nothing, so a separator between two separately printed documents is a
55
+ * `Console.log("")`, not a `Report.line("")`.
56
+ */
57
+ static line = (text) => Doc.line(text);
58
+ /** Text already laid out (a diff, a config dump), kept verbatim. */
59
+ static verbatim = (text) => Doc.verbatim(text);
60
+ /** `3 ok, 1 warning, 2 failed`, zero parts left out; `nothing to do` when every count is zero. */
61
+ static summary = (counts) => {
62
+ const ok = counts.ok ?? 0;
63
+ const warn = counts.warn ?? 0;
64
+ const fail = counts.fail ?? 0;
65
+ if (ok + warn + fail === 0) return Doc.line("nothing to do");
66
+ return Doc.counts({
67
+ layout: "inline",
68
+ share: false,
69
+ counters: [
70
+ Doc.counter(Status.core, "success", {
71
+ key: "ok",
72
+ label: "ok",
73
+ n: ok
74
+ }),
75
+ Doc.counter(Status.core, "warning", {
76
+ key: "warn",
77
+ label: warn === 1 ? "warning" : "warnings",
78
+ n: warn
79
+ }),
80
+ Doc.counter(Status.core, "failure", {
81
+ key: "fail",
82
+ label: "failed",
83
+ n: fail
84
+ })
85
+ ]
86
+ });
87
+ };
88
+ /**
89
+ * Render `doc` for the run's audience and write it to stdout.
90
+ *
91
+ * @remarks
92
+ * A terminal wraps to its width. A pipe has no width to honour — the kit
93
+ * would otherwise wrap a person's piped output at 80 columns and split a
94
+ * long path or a `file:line:col` finding a caller greps — so off a terminal
95
+ * nothing is wrapped.
96
+ */
97
+ static print = (doc) => Effect.flatMap(TerminalEnv, (terminal) => Doc.print(doc, terminal.stdout.isTerminal ? {} : { width: Number.POSITIVE_INFINITY }));
98
+ };
99
+
100
+ //#endregion
101
+ export { Report };
package/main.js CHANGED
@@ -1,9 +1,8 @@
1
- import { AppLive, CliPlatform, rootCommand } from "./cli/index.js";
2
1
  import { FailureLine } from "./internal/failure-line.js";
2
+ import { AppLive, CliConfigLive, CliPlatform, rootCommand } from "./cli/index.js";
3
3
  import { VersionLine } from "./internal/version-line.js";
4
4
  import { CLI_VERSION } from "./version.js";
5
- import { Command } from "effect/cli";
6
- import { CliColor, CliRuntime } from "@effected/cli";
5
+ import { CliAudience, CliRuntime } from "@effected/cli";
7
6
  import { Effect, Layer, Option } from "effect";
8
7
  import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
9
8
  import { CurrentDistribution } from "@effected/engine";
@@ -14,15 +13,19 @@ import { CurrentDistribution } from "@effected/engine";
14
13
  * never import this module from the barrel.
15
14
  *
16
15
  * @remarks
17
- * Assembles the `Command.run` Effect over `rootCommand`, provides the merged
18
- * `AppLive` stack and the carrier-aware version formatter, and hands the
19
- * program to `@effected/cli`'s `CliRuntime.main`, which provides the platform
20
- * and the kit-default CLI logger (every log line on stderr), reports failures
21
- * through that logger (a typed failure as one line, a defect as a bug report),
22
- * applies the `CliExit` code a command set, and exits `64` on a usage error
23
- * with the help document on stderr, so stdout stays clean for a caller that
24
- * parses it. The version formatter rides in `platform`, not `program`: help
25
- * routing only sees a Formatter provided there.
16
+ * Runs `rootCommand` through `CliAudience.run`, which resolves `--audience`,
17
+ * `--human`, `--agent` and `--ci` before core parses, provides the merged
18
+ * `AppLive` stack and `CliConfigLive` (core's built-in flags minus
19
+ * `--wizard`), and hands the program to `@effected/cli`'s
20
+ * `CliRuntime.main`. Its `env` builds the presentation environment once — the
21
+ * audience (overridable through `SAVVY_AUDIENCE`), the terminal, the theme,
22
+ * editor links and the prompt gate — installs `CliLog` (diagnostics opt-in
23
+ * through `SAVVY_LOG_LEVEL`, every log line on stderr) and the coloured help
24
+ * formatter with the carrier-aware `--version` line. `main` reports failures
25
+ * through that logger (a typed failure as one line, a defect as a bug
26
+ * report), applies the `CliExit` code a command set, and exits `64` on a
27
+ * usage error with the help document on stderr, so stdout stays clean for a
28
+ * caller that parses it.
26
29
  * No type casts: the layer graph is validated by the compiler.
27
30
  *
28
31
  * @packageDocumentation
@@ -35,11 +38,15 @@ import { CurrentDistribution } from "@effected/engine";
35
38
  */
36
39
  const main = (options = {}) => {
37
40
  const distribution = Option.fromNullishOr(options.distribution);
38
- const VersionFormatterLive = CliColor.formatterLayer({ formatVersion: (name, version) => VersionLine.format(name, version, distribution) });
39
- const program = Command.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provide(AppLive), Effect.provideService(CurrentDistribution, distribution));
40
- const platform = VersionFormatterLive.pipe(Layer.provideMerge(CliPlatform));
41
+ const program = CliAudience.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provide(Layer.merge(AppLive, CliConfigLive)), Effect.provideService(CurrentDistribution, distribution));
41
42
  NodeRuntime.runMain(CliRuntime.main(program, {
42
- platform,
43
+ platform: CliPlatform,
44
+ env: {
45
+ audienceEnvVar: "SAVVY_AUDIENCE",
46
+ stderrIsTerminal: Effect.sync(() => process.stderr.isTTY === true),
47
+ log: { envVar: "SAVVY_LOG_LEVEL" },
48
+ formatter: { formatVersion: (name, version) => VersionLine.format(name, version, distribution) }
49
+ },
43
50
  render: FailureLine.render,
44
51
  helpOnUsageError: "stderr"
45
52
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/cli",
3
- "version": "3.5.0",
3
+ "version": "3.6.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",
@@ -36,16 +36,21 @@
36
36
  "savvy": "bin/savvy.js"
37
37
  },
38
38
  "dependencies": {
39
- "@effect/platform-node": "4.0.0-rc.118",
40
- "@effected/cli": "^0.10.0",
41
- "@effected/commands": "^0.10.0",
42
- "@effected/engine": "^0.2.0",
43
- "@effected/git": "^0.19.0",
44
- "@effected/jsonc": "^0.14.0",
45
- "@effected/templates": "^0.9.1",
46
- "@effected/workspaces": "^0.30.2",
47
- "@effected/yaml": "^0.18.0",
48
- "@savvy-web/silk-effects": "9.3.0",
49
- "effect": "4.0.0-rc.118"
39
+ "@effect/platform-node": "^4.0.0",
40
+ "@effected/cli": "^0.11.0",
41
+ "@effected/commands": "^0.11.0",
42
+ "@effected/engine": "^0.3.0",
43
+ "@effected/env": "^0.1.0",
44
+ "@effected/git": "^0.20.0",
45
+ "@effected/glob": "^0.10.0",
46
+ "@effected/jsonc": "^0.15.0",
47
+ "@effected/templates": "^0.10.0",
48
+ "@effected/walker": "^0.15.0",
49
+ "@effected/workspaces": "^0.31.0",
50
+ "@effected/yaml": "^0.19.0",
51
+ "@savvy-web/silk-effects": "9.4.0",
52
+ "effect": "^4.0.0",
53
+ "ink": "^7.1.1",
54
+ "react": "^19.3.0"
50
55
  }
51
56
  }
package/version.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * @packageDocumentation
8
8
  */
9
9
  /** @internal */
10
- const CLI_VERSION = "3.5.0";
10
+ const CLI_VERSION = "3.6.0";
11
11
 
12
12
  //#endregion
13
13
  export { CLI_VERSION };
@@ -1,52 +0,0 @@
1
- import { CliColor } from "@effected/cli";
2
- import { Console, Effect } from "effect";
3
-
4
- //#region src/internal/output.ts
5
- /**
6
- * Human-readable command output.
7
- *
8
- * @packageDocumentation
9
- */
10
- const paint = (on, code, text) => on ? `\u001b[${code}m${text}\u001b[0m` : text;
11
- const glyphLine = (code, glyph, text) => Effect.flatMap(CliColor.enabled, (on) => Console.log(`${paint(on, code, glyph)} ${text}`));
12
- /**
13
- * The result a person ran a command to see, written to stdout.
14
- *
15
- * @remarks
16
- * Every line goes through `Console.log`, so stdout carries a command's product
17
- * and nothing else; progress and diagnostics stay on `Effect.log*`, which the
18
- * CLI logger sends to stderr. Colour follows `CliColor.enabled` — off when
19
- * stdout is not a terminal or `NO_COLOR` is set — and only ever tints the
20
- * glyph or heading, so the text reads the same piped.
21
- *
22
- * @internal
23
- */
24
- var Output = class {
25
- constructor() {}
26
- /** A passing item: `✓ text`. */
27
- static ok = (text) => glyphLine("32", "✓", text);
28
- /** An item that passed with a caveat: `⚠ text`. */
29
- static warn = (text) => glyphLine("33", "⚠", text);
30
- /** A finding — still output, not a crash: `✗ text`. */
31
- static fail = (text) => glyphLine("31", "✗", text);
32
- /** An item that is absent or not applicable — neither a pass nor a finding: `• text`, dimmed. */
33
- static skip = (text) => glyphLine("2", "•", text);
34
- /** A section title, bold on a terminal. */
35
- static heading = (text) => Effect.flatMap(CliColor.enabled, (on) => Console.log(paint(on, "1", text)));
36
- /** Supporting detail under the line before it, indented and dimmed on a terminal. */
37
- static detail = (text) => Effect.flatMap(CliColor.enabled, (on) => Console.log(` ${paint(on, "2", text)}`));
38
- /** A line printed as given — a blank separator, or a block already formatted. */
39
- static line = (text) => Console.log(text);
40
- /** `3 ok · 1 warning · 2 failed`, zero parts left out; `nothing to do` when every count is zero. */
41
- static summary = (counts) => {
42
- const parts = [
43
- counts.ok ? `${counts.ok} ok` : void 0,
44
- counts.warn ? `${counts.warn} ${counts.warn === 1 ? "warning" : "warnings"}` : void 0,
45
- counts.fail ? `${counts.fail} failed` : void 0
46
- ].filter((part) => part !== void 0);
47
- return Console.log(parts.length === 0 ? "nothing to do" : parts.join(" · "));
48
- };
49
- };
50
-
51
- //#endregion
52
- export { Output };