@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.
- package/README.md +332 -3
- package/bin/okfit.js +2 -8
- package/commands/context.js +75 -0
- package/commands/init.js +171 -0
- package/commands/root.js +26 -3
- package/commands/sync.js +108 -0
- package/commands/validate.js +93 -0
- package/commands/verify.js +94 -0
- package/errors.js +70 -0
- package/index.d.ts +147 -6
- package/index.js +6 -10
- package/internal/exit.js +14 -0
- package/internal/tty.js +13 -0
- package/main.d.ts +15 -0
- package/main.js +44 -0
- package/package.json +20 -5
- package/render/context.js +29 -0
- package/render/human.js +68 -0
- package/render/sync.js +41 -0
- package/render/verify.js +29 -0
- package/version.js +14 -0
package/commands/sync.js
ADDED
|
@@ -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
|
-
*
|
|
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
|
|
145
|
+
export declare const humanVerify: (input: VerifyLines) => ReadonlyArray<string>;
|
|
9
146
|
//#endregion
|
|
10
|
-
//#region src/
|
|
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
|
|
157
|
+
export declare const CLI_VERSION: string;
|
|
17
158
|
//#endregion
|
|
18
|
-
export {
|
|
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
|
-
|
|
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 };
|
package/internal/exit.js
ADDED
|
@@ -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 };
|
package/internal/tty.js
ADDED
|
@@ -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 };
|