@okfit/cli 0.1.0 → 0.3.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.
@@ -0,0 +1,108 @@
1
+ import { setExitCode } from "../internal/exit.js";
2
+ import { CLI_VERSION } from "../version.js";
3
+ import { displayRoot } from "../render/human.js";
4
+ import { humanSync } from "../render/sync.js";
5
+ import { Argument, Command, Flag } from "effect/unstable/cli";
6
+ import { SyncEnvelope, jsonError, provideConfig, resolveProjectConfig, runSync, syncEnvelope } from "@okfit/engine";
7
+ import { Console, Effect, Layer, Option, Path, Schema } from "effect";
8
+ import { Git } from "@effected/git";
9
+ import { GitHistory } from "@okfit/profiles";
10
+
11
+ //#region src/commands/sync.ts
12
+ /** K-2: `[path]` is the PROJECT root, byte-identical to validate/init/context/verify's. */
13
+ const pathArg = Argument.path("path", { pathType: "directory" }).pipe(Argument.optional, Argument.withDescription("project root to start config discovery from (default: current directory)"));
14
+ /** K-1: no `mustExist` — the handler stats it via `provideConfig`. */
15
+ const configFlag = Flag.file("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
16
+ /**
17
+ * S-13: a REPEATED `Flag.choice`, not a comma-separated `Flag.string`.
18
+ * `Flag.atLeast(0)` allows zero occurrences (all three modes run) up
19
+ * through any number. Each occurrence is independently validated by the
20
+ * underlying `Choice` primitive, so a bad token (`--only badmode`) is a
21
+ * genuine parse-time `CliError.InvalidValue` → `ShowHelp` → `bin.ts`'s
22
+ * existing 64 remap — no new error class, no hand-built `ShowHelp`
23
+ * (contract §11, S-13, judge note 9).
24
+ *
25
+ * Note: `Flag.choice`'s installed signature (`effect/unstable/cli/Flag.d.ts:161`)
26
+ * takes `(name: string, choices: ReadonlyArray<string>)`, matching
27
+ * `formatFlag` below — not the `[value, label]` tuple-pair form
28
+ * `Flag.choiceWithValue` takes. The contract's own `onlyFlag` sketch
29
+ * writes tuple pairs; since every pair's two elements are identical
30
+ * (`["generated", "generated"]`, etc.), the plain string-array form
31
+ * below is the same flag, verified against the installed primitive
32
+ * rather than copied byte-for-byte from the contract's prose.
33
+ */
34
+ const onlyFlag = Flag.choice("only", [
35
+ "generated",
36
+ "index",
37
+ "log"
38
+ ]).pipe(Flag.atLeast(0), Flag.withDescription("restrict the run to these modes (repeatable: --only generated --only index); default: all three"));
39
+ const dryRunFlag = Flag.boolean("dry-run").pipe(Flag.withDefault(false), Flag.withDescription("compute every result and write nothing"));
40
+ const formatFlag = Flag.choice("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
41
+ /**
42
+ * `okfit sync [path] [--config <file>] [--only <mode>]... [--dry-run]
43
+ * [--format human|json]`.
44
+ *
45
+ * Handler order fixed by contract §4.3. Steps 1–3 are `context`'s/
46
+ * `validate`'s handler in substance — stat `--config` (K-1) via
47
+ * `provideConfig`, resolve the project and bundle roots through
48
+ * `resolveProjectConfig` — then it diverges: build the `--only` mode
49
+ * set (default: all three, order irrelevant — `runSync`'s own fixed
50
+ * order wins, not `--only`'s occurrence order), run `runSync` with BOTH
51
+ * `Git.layer` and `GitHistory.layer` provided (S-16, mirroring
52
+ * `verify.ts:134`'s `Git.layer`-alone provision one layer up: here two
53
+ * layers are needed because `GitHistory.layer` does not re-expose `Git`
54
+ * even though it is built on it), render, and always exit `0`. There is
55
+ * no content tier: every typed failure is exit `3` through `bin.ts`'s
56
+ * existing `reportFailures`; an unknown `--only` token never reaches
57
+ * this handler at all — it fails at parse time, exit `64`.
58
+ *
59
+ * @public
60
+ */
61
+ const syncCommand = Command.make("sync", {
62
+ path: pathArg,
63
+ config: configFlag,
64
+ only: onlyFlag,
65
+ dryRun: dryRunFlag,
66
+ format: formatFlag
67
+ }, (input) => Effect.gen(function* () {
68
+ const cwd = process.cwd();
69
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
70
+ const body = Effect.gen(function* () {
71
+ const path = yield* Path.Path;
72
+ const resolved = yield* resolveProjectConfig({
73
+ pathArg: input.path,
74
+ explicitConfigPath: input.config,
75
+ cwd
76
+ });
77
+ const modes = input.only.length === 0 ? /* @__PURE__ */ new Set([
78
+ "generated",
79
+ "index",
80
+ "log"
81
+ ]) : new Set(input.only);
82
+ const result = yield* runSync({
83
+ bundleRoot: resolved.bundleRoot,
84
+ config: resolved.config,
85
+ modes,
86
+ dryRun: input.dryRun
87
+ }).pipe(Effect.provide(Layer.mergeAll(Git.layer, GitHistory.layer)));
88
+ const displayPath = displayRoot(cwd, result.bundleRoot, path);
89
+ if (input.format === "json") {
90
+ const envelope = syncEnvelope({
91
+ okfitVersion: CLI_VERSION,
92
+ root: displayPath,
93
+ dryRun: result.dryRun,
94
+ result
95
+ });
96
+ yield* Console.log(JSON.stringify(Schema.encodeSync(SyncEnvelope)(envelope)));
97
+ } else for (const line of humanSync(result)) yield* Console.log(line);
98
+ setExitCode(0);
99
+ }).pipe(provideConfig({
100
+ explicitConfigPath: input.config,
101
+ discoveryCwd
102
+ }));
103
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
104
+ return yield* body;
105
+ })).pipe(Command.withDescription("Regenerate the derived-content families that no other command produces: generated.at, index.md, and log.md, all from git history and the bundle's own concepts. Mechanical and agent-runnable; never an attestation and never touches verified."));
106
+
107
+ //#endregion
108
+ export { syncCommand };
@@ -0,0 +1,93 @@
1
+ import { setExitCode } from "../internal/exit.js";
2
+ import { CLI_VERSION } from "../version.js";
3
+ import { useColor } from "../internal/tty.js";
4
+ import { displayRoot, human, summary } from "../render/human.js";
5
+ import { Argument, Command, Flag } from "effect/unstable/cli";
6
+ import { JsonEnvelope, Now, collect, forDiagnostics, json, jsonError, provideConfig, resolveProjectConfig, run } from "@okfit/engine";
7
+ import { Console, Effect, Layer, Option, Path, Schema } from "effect";
8
+ import { Git } from "@effected/git";
9
+ import { OKF_SPEC_VERSION } from "@okfit/core";
10
+ import { GitHistory } from "@okfit/profiles";
11
+
12
+ //#region src/commands/validate.ts
13
+ /** `[path]` is the PROJECT root (K-2), never the bundle root. Absolute at parse time (K-50). */
14
+ const pathArg = Argument.path("path", { pathType: "directory" }).pipe(Argument.optional, Argument.withDescription("project root to start config discovery from (default: current directory); never the bundle root"));
15
+ /** K-1: no `mustExist` — the handler stats the path itself, before building any layer. */
16
+ const configFlag = Flag.file("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
17
+ /** K-6: `--format` is on `validate` only. */
18
+ const formatFlag = Flag.choice("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
19
+ /** S-31: skips `Provenance.lint`'s git tier for this invocation, without touching `[lint]`. */
20
+ const skipProvenanceFlag = Flag.boolean("skip-provenance").pipe(Flag.withDefault(false), Flag.withDescription("skip the generated-at-drift lint's git tier for this run"));
21
+ /**
22
+ * `okfit validate [path] [--config <file>] [--format human|json] [--skip-provenance]`.
23
+ *
24
+ * Handler order fixed by the contract (§2 `src/commands/validate.ts`):
25
+ * stat `--config` (K-1) via `provideConfig`, discover (`OkfitConfigFile.discover`),
26
+ * resolve the profile with the K-4 warning, merge `DEFAULTS < profile < file`
27
+ * (D-28), warn on an `okf_version` mismatch (K-15), resolve the project and
28
+ * bundle roots (K-12), run (`@okfit/engine`'s `validate/run.ts#run`), collect and sort the
29
+ * diagnostics, render per `--format` (K-18, K-20 to K-22), and set the exit
30
+ * code without failing the Effect (K-7, K-8).
31
+ *
32
+ * @public
33
+ */
34
+ const validateCommand = Command.make("validate", {
35
+ path: pathArg,
36
+ config: configFlag,
37
+ format: formatFlag,
38
+ skipProvenance: skipProvenanceFlag
39
+ }, (input) => Effect.gen(function* () {
40
+ const cwd = process.cwd();
41
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
42
+ const now = yield* Now;
43
+ const path = yield* Path.Path;
44
+ const body = Effect.gen(function* () {
45
+ const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
46
+ pathArg: input.path,
47
+ explicitConfigPath: input.config,
48
+ cwd
49
+ });
50
+ const result = yield* run({
51
+ root: bundleRoot,
52
+ config: merged,
53
+ profile,
54
+ now,
55
+ skipProvenance: input.skipProvenance
56
+ }).pipe(Effect.provide(Layer.mergeAll(Git.layer, GitHistory.layer)));
57
+ const diagnostics = collect(result.report.conformance, result.report.lint, result.profileDiagnostics);
58
+ const code = forDiagnostics(diagnostics);
59
+ if (input.format === "json") {
60
+ const envelope = json({
61
+ okfitVersion: CLI_VERSION,
62
+ okfVersion: merged.okf_version ?? OKF_SPEC_VERSION,
63
+ root: bundleRoot,
64
+ profile: Option.match(profile, {
65
+ onNone: () => null,
66
+ onSome: (p) => p.name
67
+ }),
68
+ exitCode: code,
69
+ concepts: result.bundle.concepts.size,
70
+ diagnostics
71
+ });
72
+ yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
73
+ } else {
74
+ for (const diagnosticLine of human(diagnostics, { color: useColor() })) yield* Console.log(diagnosticLine);
75
+ const counts = {
76
+ errors: diagnostics.filter((d) => d.severity === "error").length,
77
+ warnings: diagnostics.filter((d) => d.severity === "warning").length,
78
+ info: diagnostics.filter((d) => d.severity === "info").length,
79
+ concepts: result.bundle.concepts.size
80
+ };
81
+ yield* Console.error(summary(counts, displayRoot(cwd, bundleRoot, path)));
82
+ }
83
+ setExitCode(code);
84
+ }).pipe(provideConfig({
85
+ explicitConfigPath: input.config,
86
+ discoveryCwd
87
+ }));
88
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
89
+ return yield* body;
90
+ })).pipe(Command.withDescription("Load config and bundle, run conformance and lint checks, and render the diagnostics."));
91
+
92
+ //#endregion
93
+ export { validateCommand };
@@ -0,0 +1,94 @@
1
+ import { setExitCode } from "../internal/exit.js";
2
+ import { CLI_VERSION } from "../version.js";
3
+ import { displayRoot } from "../render/human.js";
4
+ import { humanVerify } from "../render/verify.js";
5
+ import { Argument, Command, Flag } from "effect/unstable/cli";
6
+ import { Now, VerifyEnvelope, jsonError, provideConfig, resolveProjectConfig, runVerify, verifyEnvelope } from "@okfit/engine";
7
+ import { Console, DateTime, Effect, Option, Path, Schema } from "effect";
8
+ import { Git } from "@effected/git";
9
+ import { Timestamp } from "@okfit/core";
10
+
11
+ //#region src/commands/verify.ts
12
+ /** V-6: tolerant id, normalised through `ConceptId.normalize`; never `Argument.path`. */
13
+ const idArg = Argument.string("id").pipe(Argument.withDescription("concept id to verify, with or without a leading slash or trailing .md"));
14
+ /** K-2: `[path]` is the PROJECT root, byte-identical to validate/init/context's. */
15
+ const pathArg = Argument.path("path", { pathType: "directory" }).pipe(Argument.optional, Argument.withDescription("project root to start config discovery from (default: current directory); never the bundle root"));
16
+ /** K-1: no `mustExist`; the handler stats it via `provideConfig`. */
17
+ const configFlag = Flag.file("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
18
+ /** V-6: `Flag.string`, decoded through core's `Timestamp` — never `Flag.date`, which yields a bare Date. */
19
+ const atFlag = Flag.string("at").pipe(Flag.optional, Flag.withDescription("ISO 8601 timestamp with an explicit offset to record instead of now"));
20
+ /** V-6/V-8: the preview is a flag, never a prompt. */
21
+ const dryRunFlag = Flag.boolean("dry-run").pipe(Flag.withDefault(false), Flag.withDescription("print the exact fragment a real run would splice in; write nothing"));
22
+ const formatFlag = Flag.choice("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
23
+ /**
24
+ * `okfit verify <id> [path] [--config <file>] [--at <iso>] [--dry-run]
25
+ * [--format human|json]`.
26
+ *
27
+ * Handler order fixed by contract §2.3. Steps 1–3 are `context`'s handler
28
+ * in substance — stat `--config` (K-1) via `provideConfig`, discover,
29
+ * resolve the profile and roots through `resolveProjectConfig` — then it
30
+ * diverges: resolve `at` (the `Now` service, or `--at` through
31
+ * `Schema.decodeUnknownEffect(Timestamp)`, never the throwing sync
32
+ * decoder), provide `Git` for `Derivation.generatedBy`, run the splice and
33
+ * the atomic write, render, and always exit `0`. There is no content tier:
34
+ * every typed failure is exit `3` through `bin.ts`'s existing
35
+ * `reportFailures`.
36
+ *
37
+ * @public
38
+ */
39
+ const verifyCommand = Command.make("verify", {
40
+ id: idArg,
41
+ path: pathArg,
42
+ config: configFlag,
43
+ at: atFlag,
44
+ dryRun: dryRunFlag,
45
+ format: formatFlag
46
+ }, (input) => Effect.gen(function* () {
47
+ const cwd = process.cwd();
48
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
49
+ const body = Effect.gen(function* () {
50
+ const path = yield* Path.Path;
51
+ const resolved = yield* resolveProjectConfig({
52
+ pathArg: input.path,
53
+ explicitConfigPath: input.config,
54
+ cwd
55
+ });
56
+ const at = Option.isNone(input.at) ? DateTime.startOf(yield* Now, "second") : yield* Schema.decodeUnknownEffect(Timestamp)(input.at.value);
57
+ const result = yield* runVerify({
58
+ id: input.id,
59
+ bundleRoot: resolved.bundleRoot,
60
+ projectRoot: resolved.projectRoot,
61
+ config: resolved.config,
62
+ at,
63
+ dryRun: input.dryRun
64
+ });
65
+ const displayPath = `${displayRoot(cwd, result.bundleRoot, path)}/${result.conceptPath}`;
66
+ if (input.format === "json") {
67
+ const envelope = verifyEnvelope({
68
+ okfitVersion: CLI_VERSION,
69
+ id: result.id,
70
+ path: displayPath,
71
+ by: result.by,
72
+ at: result.at,
73
+ dryRun: result.dryRun
74
+ });
75
+ yield* Console.log(JSON.stringify(Schema.encodeSync(VerifyEnvelope)(envelope)));
76
+ } else for (const line of humanVerify({
77
+ id: result.id,
78
+ by: result.by,
79
+ at: result.at,
80
+ priorAt: result.priorAt,
81
+ dryRun: result.dryRun,
82
+ fragment: result.fragment
83
+ })) yield* Console.log(line);
84
+ setExitCode(0);
85
+ }).pipe(Effect.provide(Git.layer), provideConfig({
86
+ explicitConfigPath: input.config,
87
+ discoveryCwd
88
+ }));
89
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
90
+ return yield* body;
91
+ })).pipe(Command.withDescription("Record a human's attestation that a concept has been reviewed: append one verified entry, { by: human:<id>, at: <now> }, to its frontmatter. The actor is always your own git identity; there is no --by. This is a human-run command: no agent, hook, or MCP tool ever invokes it."));
92
+
93
+ //#endregion
94
+ export { verifyCommand };
package/errors.js ADDED
@@ -0,0 +1,70 @@
1
+ import { ConfigMalformedError, ConfigPathNotFoundError, InitOverwriteError, VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
2
+ import { ConfigIssueRenderer } from "@effected/cli";
3
+
4
+ //#region src/errors.ts
5
+ const hasTag = (error, tag) => typeof error === "object" && error !== null && "_tag" in error && error._tag === tag;
6
+ /** `path` relative to `cwd` when it is under it, else the absolute path unchanged (K-51). */
7
+ const relativeToCwd = (path, cwd) => {
8
+ if (path === cwd) return ".";
9
+ const prefix = cwd.endsWith("/") ? cwd : `${cwd}/`;
10
+ return path.startsWith(prefix) ? path.slice(prefix.length) : path;
11
+ };
12
+ /**
13
+ * The `render` option for `CliRuntime.reportFailures` (K-30, K-46, K-51). One
14
+ * string per stderr line; `reportFailures` emits each through its own
15
+ * `Effect.logError`, which `CliLogger` routes to stderr.
16
+ *
17
+ * Rules, in order:
18
+ *
19
+ * 1. A `ShowHelp` (any `_tag === "ShowHelp"`) renders as `[]` — the empty
20
+ * array. `Command.runWith` has already rendered the help document and any
21
+ * parse errors before re-failing, so a second rendering here would print
22
+ * "Help requested" after the help text. This is why the K-30 remap in
23
+ * `bin.ts` can run ahead of `reportFailures` without corrupting `--help`
24
+ * output.
25
+ * 2. `ConfigPathNotFoundError` renders as its own `error: <message>` line;
26
+ * `InitOverwriteError` renders as the K-51 header, one two-space-indented
27
+ * relativised path per conflict, and the literal `Nothing was written.`.
28
+ * 3. `ConfigMalformedError` renders as its own `error: <message>` line —
29
+ * `error: malformed config <path>: <cause message>` — since it already
30
+ * carries the offending path (`config/layer.ts#provideConfig` wraps a
31
+ * `ConfigCodecError`/`ConfigValidationError` into this the moment the
32
+ * path is known).
33
+ * 4. A `ConfigValidationError` NOT already wrapped above (detected by
34
+ * `_tag`, since the peer is optional and this module keeps it a
35
+ * type-only import — this is the "path unknown" case, K-46) renders as
36
+ * `error: ${String(error)}` followed by one two-space-indented
37
+ * `ConfigIssueRenderer.render(error)` line per entry.
38
+ * 5. `VerifyConceptNotFoundError` and `VerifyUnsupportedFrontmatterError`
39
+ * each render as their own `error: <message>` line; both messages
40
+ * already name the concept id and what to do about it, and neither
41
+ * carries a filesystem path needing K-51 relativisation.
42
+ * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
43
+ * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
44
+ * (the K-13 `HOME`-unset case) — renders as the single line
45
+ * `error: ${String(error)}`. Each of those
46
+ * classes' own `message` already names the offending path, which is all
47
+ * K-46 asserts.
48
+ *
49
+ * @public
50
+ */
51
+ const renderFailure = (error) => {
52
+ if (hasTag(error, "ShowHelp")) return [];
53
+ if (error instanceof ConfigPathNotFoundError) return [`error: ${error.message}`];
54
+ if (error instanceof ConfigMalformedError) return [`error: ${error.message}`];
55
+ if (error instanceof InitOverwriteError) return [
56
+ "error: refusing to overwrite existing files:",
57
+ ...error.paths.map((path) => ` ${relativeToCwd(path, error.cwd)}`),
58
+ "Nothing was written."
59
+ ];
60
+ if (error instanceof VerifyConceptNotFoundError) return [`error: ${error.message}`];
61
+ if (error instanceof VerifyUnsupportedFrontmatterError) return [`error: ${error.message}`];
62
+ if (hasTag(error, "ConfigValidationError")) {
63
+ const validationError = error;
64
+ return [`error: ${String(validationError)}`, ...ConfigIssueRenderer.render(validationError).map((line) => ` ${line}`)];
65
+ }
66
+ return [`error: ${String(error)}`];
67
+ };
68
+
69
+ //#endregion
70
+ export { renderFailure };
package/index.d.ts CHANGED
@@ -1,19 +1,160 @@
1
1
  import { Command } from "effect/unstable/cli";
2
+ import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
3
+ import "effect";
2
4
  //#region src/commands/root.d.ts
3
5
  /**
4
- * The `okfit` root command. Subcommands are attached in later releases.
6
+ * K-5. No handler: the framework's own default for a command with neither a
7
+ * `handle` nor a matched subcommand is
8
+ * `Effect.fail(new CliError.ShowHelp({ commandPath, errors: [] }))`, and a
9
+ * `ShowHelp` with no errors carries `[Runtime.errorExitCode] = 0`. Bare
10
+ * `okfit` therefore prints the root help and exits `0` with no code in this
11
+ * package at all.
12
+ *
13
+ * `validate`, `init`, `context`, `verify`, and `sync` are the whole command tree;
14
+ * nothing else is registered here. Each is appended in introduction order,
15
+ * never reordered in, so `--help`'s subcommand list reads that way too
16
+ * (contract §4.2). The top-level description is left unchanged: neither
17
+ * `context` nor `verify` validates or scaffolds, but widening the sentence
18
+ * for the orientation- and attestation-only commands buys nothing
19
+ * (contract §4.2).
20
+ *
21
+ * @public
22
+ */
23
+ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("@okfit/profiles").AgentActorUnconfiguredError | import("@okfit/core").BundleDepthExceededError | import("@okfit/core").BundleReadError | import("@okfit/core").BundleRootNotFoundError | import("@effected/config-file").ConfigCodecError | import("@effected/config-file").ConfigFileReadError | import("@okfit/engine").ConfigMalformedError | import("@okfit/engine").ConfigPathNotFoundError | import("@effected/config-file").ConfigValidationError | import("@effected/markdown").FrontmatterEncodeError | import("@effected/markdown").FrontmatterFormatMismatchError | import("@effected/markdown").FrontmatterValidationError | import("@effected/git").GitCommandError | import("@okfit/profiles").GitHistoryError | import("@okfit/profiles").HumanActorUnresolvedError | import("@okfit/engine").InitOverwriteError | import("@effected/markdown").MarkdownParseError | import("@effected/git").NotARepositoryError | import("effect/PlatformError").PlatformError | import("effect/Schema").SchemaError | import("@effected/git").UnknownRefError | import("@okfit/engine").VerifyConceptNotFoundError | import("@okfit/engine").VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError, import("@effected/xdg").AppDirs | import("effect/unstable/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("@effected/xdg").Xdg>;
24
+ //#endregion
25
+ //#region src/errors.d.ts
26
+ /**
27
+ * The `render` option for `CliRuntime.reportFailures` (K-30, K-46, K-51). One
28
+ * string per stderr line; `reportFailures` emits each through its own
29
+ * `Effect.logError`, which `CliLogger` routes to stderr.
30
+ *
31
+ * Rules, in order:
32
+ *
33
+ * 1. A `ShowHelp` (any `_tag === "ShowHelp"`) renders as `[]` — the empty
34
+ * array. `Command.runWith` has already rendered the help document and any
35
+ * parse errors before re-failing, so a second rendering here would print
36
+ * "Help requested" after the help text. This is why the K-30 remap in
37
+ * `bin.ts` can run ahead of `reportFailures` without corrupting `--help`
38
+ * output.
39
+ * 2. `ConfigPathNotFoundError` renders as its own `error: <message>` line;
40
+ * `InitOverwriteError` renders as the K-51 header, one two-space-indented
41
+ * relativised path per conflict, and the literal `Nothing was written.`.
42
+ * 3. `ConfigMalformedError` renders as its own `error: <message>` line —
43
+ * `error: malformed config <path>: <cause message>` — since it already
44
+ * carries the offending path (`config/layer.ts#provideConfig` wraps a
45
+ * `ConfigCodecError`/`ConfigValidationError` into this the moment the
46
+ * path is known).
47
+ * 4. A `ConfigValidationError` NOT already wrapped above (detected by
48
+ * `_tag`, since the peer is optional and this module keeps it a
49
+ * type-only import — this is the "path unknown" case, K-46) renders as
50
+ * `error: ${String(error)}` followed by one two-space-indented
51
+ * `ConfigIssueRenderer.render(error)` line per entry.
52
+ * 5. `VerifyConceptNotFoundError` and `VerifyUnsupportedFrontmatterError`
53
+ * each render as their own `error: <message>` line; both messages
54
+ * already name the concept id and what to do about it, and neither
55
+ * carries a filesystem path needing K-51 relativisation.
56
+ * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
57
+ * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
58
+ * (the K-13 `HOME`-unset case) — renders as the single line
59
+ * `error: ${String(error)}`. Each of those
60
+ * classes' own `message` already names the offending path, which is all
61
+ * K-46 asserts.
62
+ *
63
+ * @public
64
+ */
65
+ export declare const renderFailure: (error: unknown) => ReadonlyArray<string>;
66
+ //#endregion
67
+ //#region src/render/context.d.ts
68
+ /**
69
+ * The `human` format: a short header block, then one line per type and one
70
+ * per tag. Pure; the caller pipes each line through `Console.log`.
71
+ *
72
+ * The `profile:` line reads `profile: (none) (requested NAME, unknown)`
73
+ * when `profile` and `profile_requested` disagree over an actually-unknown
74
+ * profile — never for the `"none"` or no-config cases, where a `null`
75
+ * `profile` is expected, not an error.
76
+ *
77
+ * @public
78
+ */
79
+ export declare const humanContext: (envelope: ContextEnvelope) => ReadonlyArray<string>;
80
+ //#endregion
81
+ //#region src/render/human.d.ts
82
+ /** The counts the summary line reports. @public */
83
+ interface Counts {
84
+ readonly errors: number;
85
+ readonly warnings: number;
86
+ readonly info: number;
87
+ readonly concepts: number;
88
+ }
89
+ /**
90
+ * K-16. With a range:
91
+ * `<file>:<range.line + 1>:<range.character + 1> <severity> <code> <message>`.
92
+ * Without one: `<file> <severity> <code> <message>`. `file: ""` renders as
93
+ * the literal `(bundle)`. Core's range is zero-based (D-32,
94
+ * `CORE/Diagnostic.ts:49-57`); the `+ 1`s here are the only place it becomes
95
+ * one-based. Colour, when `color` is `true`, wraps ONLY the severity word
96
+ * (K-19) — never the code, the path, or the message.
97
+ *
98
+ * @public
99
+ */
100
+ export declare const line: (diagnostic: RenderedDiagnostic, options?: {
101
+ readonly color?: boolean;
102
+ }) => string;
103
+ /**
104
+ * `sort` then `line` over the whole set: the exact stdout body of
105
+ * `--format human`, one array element per stdout line.
106
+ *
107
+ * @public
108
+ */
109
+ export declare const human: (diagnostics: ReadonlyArray<RenderedDiagnostic>, options?: {
110
+ readonly color?: boolean;
111
+ }) => ReadonlyArray<string>;
112
+ /**
113
+ * K-20, verbatim and unpluralised —
114
+ * `<E> errors, <W> warnings, <I> info in <N> concepts (<root>)`. `root` is
115
+ * pre-rendered by the caller: relative to cwd when under it, absolute
116
+ * otherwise (K-51).
117
+ *
118
+ * @public
119
+ */
120
+ export declare const summary: (counts: Counts, root: string) => string;
121
+ //#endregion
122
+ //#region src/render/verify.d.ts
123
+ /** @public */
124
+ interface VerifyLines {
125
+ readonly id: string;
126
+ readonly by: string;
127
+ readonly at: string;
128
+ /** Every prior entry by the SAME actor, in list order (V-2). */
129
+ readonly priorAt: ReadonlyArray<string>;
130
+ readonly dryRun: boolean;
131
+ /** The exact bytes a real run would splice in; printed only when `dryRun`. */
132
+ readonly fragment: string;
133
+ }
134
+ /**
135
+ * V-11's human output: one line per fact. One `already verified` line per
136
+ * prior entry by the same actor, in list order (V-2 says "entries",
137
+ * plural), then the success line. A prior entry by a DIFFERENT actor is
138
+ * not called out: it stays on disk untouched (V-1) and is simply not this
139
+ * line's subject. Under `--dry-run` (I3), a `would write:` header and the
140
+ * exact fragment a real run would splice in follow, so the preview
141
+ * exercises — and shows — the same edit the write path would make.
5
142
  *
6
143
  * @public
7
144
  */
8
- declare const rootCommand: Command.Command<"okfit", {}, {}, never, never>;
145
+ export declare const humanVerify: (input: VerifyLines) => ReadonlyArray<string>;
9
146
  //#endregion
10
- //#region src/index.d.ts
147
+ //#region src/version.d.ts
11
148
  /**
12
- * The version string reported by `okfit --version`.
149
+ * The version string reported by `okfit --version`. `@savvy-web/bundler`
150
+ * replaces `process.env.__PACKAGE_VERSION__` with this package's own
151
+ * version at build time (K-32), so a release can never desync from the
152
+ * printed version. `"0.0.0"` is the unbuilt-source fallback and reads as
153
+ * dev mode.
13
154
  *
14
155
  * @public
15
156
  */
16
- declare const CLI_VERSION = "0.0.0";
157
+ export declare const CLI_VERSION: string;
17
158
  //#endregion
18
- export { CLI_VERSION, rootCommand };
159
+ export type { Counts, VerifyLines };
19
160
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,12 +1,8 @@
1
+ import { humanContext } from "./render/context.js";
2
+ import { CLI_VERSION } from "./version.js";
3
+ import { human, line, summary } from "./render/human.js";
4
+ import { humanVerify } from "./render/verify.js";
1
5
  import { rootCommand } from "./commands/root.js";
6
+ import { renderFailure } from "./errors.js";
2
7
 
3
- //#region src/index.ts
4
- /**
5
- * The version string reported by `okfit --version`.
6
- *
7
- * @public
8
- */
9
- const CLI_VERSION = "0.0.0";
10
-
11
- //#endregion
12
- export { CLI_VERSION, rootCommand };
8
+ export { CLI_VERSION, human, humanContext, humanVerify, line, renderFailure, rootCommand, summary };
@@ -0,0 +1,14 @@
1
+ //#region src/internal/exit.ts
2
+ /**
3
+ * K-7: exits `1` and `2` are a SUCCESSFUL run that found diagnostics, never
4
+ * an Effect failure. The handler writes the code here and returns `void`.
5
+ * The only writer of `process.exitCode` in this package.
6
+ *
7
+ * @public
8
+ */
9
+ const setExitCode = (code) => {
10
+ process.exitCode = code;
11
+ };
12
+
13
+ //#endregion
14
+ export { setExitCode };
@@ -0,0 +1,13 @@
1
+ //#region src/internal/tty.ts
2
+ /**
3
+ * K-19: exactly the framework's own colour rule. Evaluated once per run at
4
+ * the command boundary and passed to the renderers as a boolean, so every
5
+ * renderer stays pure. The only reader of `process.stdout.isTTY`/`NO_COLOR`
6
+ * in this package.
7
+ *
8
+ * @public
9
+ */
10
+ const useColor = () => process.stdout.isTTY === true && process.env.NO_COLOR !== "1";
11
+
12
+ //#endregion
13
+ export { useColor };
package/main.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ //#region src/main.d.ts
2
+ /**
3
+ * The assembled okfit CLI program.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ /**
8
+ * Run the okfit CLI. Owns the process: installs the runtime teardown and
9
+ * sets the exit code. `NodeRuntime.runMain` does not return a promise.
10
+ *
11
+ * @public
12
+ */
13
+ export declare const main: () => void;
14
+ //#endregion
15
+ //# sourceMappingURL=main.d.ts.map
package/main.js ADDED
@@ -0,0 +1,44 @@
1
+ import { CLI_VERSION } from "./version.js";
2
+ import { rootCommand } from "./commands/root.js";
3
+ import { renderFailure } from "./errors.js";
4
+ import { Command } from "effect/unstable/cli";
5
+ import { Now, OkfitPlatform } from "@okfit/engine";
6
+ import { DateTime, Effect, Option } from "effect";
7
+ import { CliLogger, CliRuntime } from "@effected/cli";
8
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
9
+
10
+ //#region src/main.ts
11
+ /**
12
+ * The assembled okfit CLI program.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /**
17
+ * K-47: an ISO-8601 `OKFIT_NOW` when set, else the wall clock. A documented
18
+ * test hook, not user-facing. Resolved exactly once, here, and provided to
19
+ * the whole command tree through the `Now` tag so no command handler ever
20
+ * reads `process.env["OKFIT_NOW"]` itself.
21
+ */
22
+ const nowEffect = Option.fromNullishOr(process.env.OKFIT_NOW).pipe(Option.flatMap((iso) => DateTime.make(iso)), Option.match({
23
+ onNone: () => DateTime.now,
24
+ onSome: Effect.succeed
25
+ }));
26
+ /**
27
+ * Run the okfit CLI. Owns the process: installs the runtime teardown and
28
+ * sets the exit code. `NodeRuntime.runMain` does not return a promise.
29
+ *
30
+ * @public
31
+ */
32
+ const main = () => {
33
+ const program = Effect.gen(function* () {
34
+ const now = yield* nowEffect;
35
+ return yield* Command.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(Now, now), Effect.catchTag("ShowHelp", (help) => Effect.fail(CliRuntime.reported(help, help.errors.length > 0 ? 64 : 0))));
36
+ }).pipe(Effect.provide(OkfitPlatform), CliRuntime.reportFailures({
37
+ exitCode: 3,
38
+ render: renderFailure
39
+ }));
40
+ NodeRuntime.runMain(program.pipe(Effect.provide(CliLogger.layer())));
41
+ };
42
+
43
+ //#endregion
44
+ export { main };