@okfit/cli 0.5.0 → 0.5.1

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 CHANGED
@@ -278,6 +278,7 @@ nothing else (no summary line, no warnings — those still go to stderr):
278
278
  {
279
279
  "schema": 1,
280
280
  "okfit_version": "0.1.0",
281
+ "producer": "okfit",
281
282
  "okf_version": "0.2",
282
283
  "root": "/abs/path/to/my-repo/okf",
283
284
  "profile": "software-project",
@@ -0,0 +1,75 @@
1
+ import { setExitCode } from "../internal/exit.js";
2
+ import { CLI_VERSION } from "../version.js";
3
+ import { Argument, Command, Flag } from "effect/unstable/cli";
4
+ import { GraphEnvelope, graphEnvelope, jsonError, provideConfig, resolveProjectConfig, runGraph } from "@okfit/engine";
5
+ import { Console, Effect, Option, Schema } from "effect";
6
+ import { OKF_SPEC_VERSION } from "@okfit/core";
7
+
8
+ //#region src/commands/graph.ts
9
+ /** `[path]` is the PROJECT root (K-2), never the bundle root. Absolute at parse time (K-50). */
10
+ 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"));
11
+ /** K-1: no `mustExist` — the handler stats the path itself, before building any layer. */
12
+ const configFlag = Flag.File("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
13
+ /** Default `mermaid`, unlike every other command's `--format` (which defaults `human`). */
14
+ const formatFlag = Flag.Literals("format", [
15
+ "mermaid",
16
+ "dot",
17
+ "json"
18
+ ]).pipe(Flag.withDefault("mermaid"), Flag.withDescription("output format: mermaid (default), dot, or json"));
19
+ /**
20
+ * `okfit graph [path] [--config <file>] [--format mermaid|dot|json]`.
21
+ *
22
+ * Same handler skeleton as `commands/validate.ts`/`commands/context.ts` —
23
+ * stat `--config` (K-1) via `provideConfig`, resolve the project and
24
+ * bundle roots through `resolveProjectConfig` — then it diverges:
25
+ * `@okfit/engine`'s `runGraph` (`Bundle.load` then `Graph.fromBundle`),
26
+ * render, and always exit `0`. `mermaid`/`dot` print the graph's own
27
+ * `toMermaid()`/`toGraphViz()` text raw to stdout — nothing else on
28
+ * stdout, no summary line — since either is meant to be piped straight
29
+ * into a renderer. Only `--format json` failures get the K-22 `jsonError`
30
+ * envelope; a `mermaid`/`dot` failure renders the usual way through
31
+ * `bin.ts`'s `CliRuntime.reportFailures` and carries no stdout envelope.
32
+ *
33
+ * @public
34
+ */
35
+ const graphCommand = Command.make("graph", {
36
+ path: pathArg,
37
+ config: configFlag,
38
+ format: formatFlag
39
+ }, (input) => Effect.gen(function* () {
40
+ const cwd = process.cwd();
41
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
42
+ const body = Effect.gen(function* () {
43
+ const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
44
+ pathArg: input.path,
45
+ explicitConfigPath: input.config,
46
+ cwd
47
+ });
48
+ const result = yield* runGraph({ root: bundleRoot });
49
+ if (input.format === "json") {
50
+ const envelope = graphEnvelope({
51
+ okfitVersion: CLI_VERSION,
52
+ producer: "okfit",
53
+ okfVersion: merged.okf_version ?? OKF_SPEC_VERSION,
54
+ root: bundleRoot,
55
+ profile: Option.match(profile, {
56
+ onNone: () => null,
57
+ onSome: (p) => p.name
58
+ }),
59
+ nodes: result.graph.nodes,
60
+ edges: result.graph.edges
61
+ });
62
+ yield* Console.log(JSON.stringify(Schema.encodeSync(GraphEnvelope)(envelope)));
63
+ } else if (input.format === "dot") yield* Console.log(result.graph.toGraphViz());
64
+ else yield* Console.log(result.graph.toMermaid());
65
+ setExitCode(0);
66
+ }).pipe(provideConfig({
67
+ explicitConfigPath: input.config,
68
+ discoveryCwd
69
+ }));
70
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
71
+ return yield* body;
72
+ })).pipe(Command.withDescription("Render the bundle's link graph: frontmatter path fields and body links, as Mermaid, DOT, or JSON."));
73
+
74
+ //#endregion
75
+ export { graphCommand };
package/commands/init.js CHANGED
@@ -4,8 +4,8 @@ import { displayRoot, human, summary } from "../render/human.js";
4
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
5
5
  import { CONFIG_RELATIVE_PATH, InitOverwriteError, Now, SCHEMA_DIRECTIVE, collect, configValue, files, forDiagnostics, provideConfig, resolveBundleRoot, resolveProjectRoot, run, targetPaths } from "@okfit/engine";
6
6
  import { Console, DateTime, Effect, FileSystem, Layer, Option, Path } from "effect";
7
- import { Git } from "@effected/git";
8
7
  import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile } from "@okfit/core";
8
+ import { Git } from "@effected/git";
9
9
  import { GitHistory, Profiles } from "@okfit/profiles";
10
10
 
11
11
  //#region src/commands/init.ts
@@ -0,0 +1,95 @@
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 { OKF_SPEC_VERSION } from "@okfit/core";
9
+ import { Git } from "@effected/git";
10
+ import { GitHistory } from "@okfit/profiles";
11
+
12
+ //#region src/commands/lint.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
+ const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
18
+ /** S-31: skips `Provenance.lint`'s git tier for this invocation, without touching `[lint]`. */
19
+ const skipProvenanceFlag = Flag.Boolean("skip-provenance").pipe(Flag.withDefault(false), Flag.withDescription("skip the generated-at-drift lint's git tier for this run"));
20
+ /**
21
+ * `okfit lint [path] [--config <file>] [--format human|json] [--skip-provenance]`.
22
+ *
23
+ * Same handler skeleton as `commands/validate.ts` — stat `--config` (K-1)
24
+ * via `provideConfig`, discover, resolve the profile and roots through
25
+ * `resolveProjectConfig`, run the engine's `validate/run.ts#run` (both
26
+ * conformance AND lint tiers still run — core's `Validate.all` does not
27
+ * separate them), render, and set the exit code — except the CONFORMANCE
28
+ * tier is dropped from what this command collects and reports:
29
+ * `collect([], result.report.lint, result.profileDiagnostics)`, never
30
+ * `result.report.conformance`. `conformance_errors` in the JSON envelope's
31
+ * `summary` is therefore always `0`.
32
+ *
33
+ * @public
34
+ */
35
+ const lintCommand = Command.make("lint", {
36
+ path: pathArg,
37
+ config: configFlag,
38
+ format: formatFlag,
39
+ skipProvenance: skipProvenanceFlag
40
+ }, (input) => Effect.gen(function* () {
41
+ const cwd = process.cwd();
42
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
43
+ const now = yield* Now;
44
+ const path = yield* Path.Path;
45
+ const body = Effect.gen(function* () {
46
+ const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
47
+ pathArg: input.path,
48
+ explicitConfigPath: input.config,
49
+ cwd
50
+ });
51
+ const result = yield* run({
52
+ root: bundleRoot,
53
+ config: merged,
54
+ profile,
55
+ now,
56
+ skipProvenance: input.skipProvenance
57
+ }).pipe(Effect.provide(Layer.mergeAll(Git.layer, GitHistory.layer)));
58
+ const diagnostics = collect([], result.report.lint, result.profileDiagnostics);
59
+ const code = forDiagnostics(diagnostics);
60
+ if (input.format === "json") {
61
+ const envelope = json({
62
+ okfitVersion: CLI_VERSION,
63
+ producer: "okfit",
64
+ okfVersion: merged.okf_version ?? OKF_SPEC_VERSION,
65
+ root: bundleRoot,
66
+ profile: Option.match(profile, {
67
+ onNone: () => null,
68
+ onSome: (p) => p.name
69
+ }),
70
+ exitCode: code,
71
+ concepts: result.bundle.concepts.size,
72
+ diagnostics
73
+ });
74
+ yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
75
+ } else {
76
+ for (const diagnosticLine of human(diagnostics, { color: useColor() })) yield* Console.log(diagnosticLine);
77
+ const counts = {
78
+ errors: diagnostics.filter((d) => d.severity === "error").length,
79
+ warnings: diagnostics.filter((d) => d.severity === "warning").length,
80
+ info: diagnostics.filter((d) => d.severity === "info").length,
81
+ concepts: result.bundle.concepts.size
82
+ };
83
+ yield* Console.error(summary(counts, displayRoot(cwd, bundleRoot, path)));
84
+ }
85
+ setExitCode(code);
86
+ }).pipe(provideConfig({
87
+ explicitConfigPath: input.config,
88
+ discoveryCwd
89
+ }));
90
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
91
+ return yield* body;
92
+ })).pipe(Command.withDescription("Load config and bundle, run lint checks (and the profile check), and render the diagnostics; never conformance."));
93
+
94
+ //#endregion
95
+ export { lintCommand };
package/commands/root.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { contextCommand } from "./context.js";
2
+ import { graphCommand } from "./graph.js";
2
3
  import { initCommand } from "./init.js";
4
+ import { lintCommand } from "./lint.js";
5
+ import { staleCommand } from "./stale.js";
3
6
  import { syncCommand } from "./sync.js";
4
7
  import { validateCommand } from "./validate.js";
5
8
  import { verifyCommand } from "./verify.js";
@@ -14,13 +17,13 @@ import { Command } from "effect/unstable/cli";
14
17
  * `okfit` therefore prints the root help and exits `0` with no code in this
15
18
  * package at all.
16
19
  *
17
- * `validate`, `init`, `context`, `verify`, and `sync` are the whole command tree;
18
- * nothing else is registered here. Each is appended in introduction order,
19
- * never reordered in, so `--help`'s subcommand list reads that way too
20
- * (contract §4.2). The top-level description is left unchanged: neither
21
- * `context` nor `verify` validates or scaffolds, but widening the sentence
22
- * for the orientation- and attestation-only commands buys nothing
23
- * (contract §4.2).
20
+ * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`, and
21
+ * `stale` are the whole command tree; nothing else is registered here.
22
+ * Each is appended in introduction order, never reordered in, so
23
+ * `--help`'s subcommand list reads that way too (contract §4.2). The
24
+ * top-level description is left unchanged: neither `context` nor `verify`
25
+ * validates or scaffolds, but widening the sentence for the orientation-
26
+ * and attestation-only commands buys nothing (contract §4.2).
24
27
  *
25
28
  * @public
26
29
  */
@@ -29,7 +32,10 @@ const rootCommand = Command.make("okfit", {}).pipe(Command.withDescription("Open
29
32
  initCommand,
30
33
  contextCommand,
31
34
  verifyCommand,
32
- syncCommand
35
+ syncCommand,
36
+ lintCommand,
37
+ graphCommand,
38
+ staleCommand
33
39
  ]));
34
40
 
35
41
  //#endregion
@@ -0,0 +1,76 @@
1
+ import { setExitCode } from "../internal/exit.js";
2
+ import { CLI_VERSION } from "../version.js";
3
+ import { displayRoot } from "../render/human.js";
4
+ import { humanStale, staleSummary } from "../render/stale.js";
5
+ import { Argument, Command, Flag } from "effect/unstable/cli";
6
+ import { Now, StaleEnvelope, jsonError, provideConfig, resolveProjectConfig, runStale, staleEnvelope } from "@okfit/engine";
7
+ import { Console, Effect, Option, Path, Schema } from "effect";
8
+ import { OKF_SPEC_VERSION } from "@okfit/core";
9
+
10
+ //#region src/commands/stale.ts
11
+ /** `[path]` is the PROJECT root (K-2), never the bundle root. Absolute at parse time (K-50). */
12
+ 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"));
13
+ /** K-1: no `mustExist` — the handler stats the path itself, before building any layer. */
14
+ const configFlag = Flag.File("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
15
+ const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
16
+ /**
17
+ * `okfit stale [path] [--config <file>] [--format human|json]`.
18
+ *
19
+ * Same handler skeleton as `commands/validate.ts`/`commands/context.ts` —
20
+ * stat `--config` (K-1) via `provideConfig`, resolve the project and
21
+ * bundle roots through `resolveProjectConfig` — then it diverges:
22
+ * `@okfit/engine`'s `runStale` (`Bundle.load` then
23
+ * `Derive.staleReport(bundle, now)`), render, and always exit `0`: this is
24
+ * a report, not a check, and the `stale` lint rule (part of `okfit
25
+ * validate`/`okfit lint`) is where staleness fails a run.
26
+ *
27
+ * @public
28
+ */
29
+ const staleCommand = Command.make("stale", {
30
+ path: pathArg,
31
+ config: configFlag,
32
+ format: formatFlag
33
+ }, (input) => Effect.gen(function* () {
34
+ const cwd = process.cwd();
35
+ const discoveryCwd = Option.getOrElse(input.path, () => cwd);
36
+ const now = yield* Now;
37
+ const path = yield* Path.Path;
38
+ const body = Effect.gen(function* () {
39
+ const { bundleRoot, config: merged, profile } = yield* resolveProjectConfig({
40
+ pathArg: input.path,
41
+ explicitConfigPath: input.config,
42
+ cwd
43
+ });
44
+ const result = yield* runStale({
45
+ root: bundleRoot,
46
+ now
47
+ });
48
+ const envelope = staleEnvelope({
49
+ okfitVersion: CLI_VERSION,
50
+ producer: "okfit",
51
+ okfVersion: merged.okf_version ?? OKF_SPEC_VERSION,
52
+ root: bundleRoot,
53
+ profile: Option.match(profile, {
54
+ onNone: () => null,
55
+ onSome: (p) => p.name
56
+ }),
57
+ now,
58
+ concepts: result.bundle.concepts.size,
59
+ items: result.items
60
+ });
61
+ if (input.format === "json") yield* Console.log(JSON.stringify(Schema.encodeSync(StaleEnvelope)(envelope)));
62
+ else {
63
+ for (const line of humanStale(envelope.items)) yield* Console.log(line);
64
+ yield* Console.error(staleSummary(envelope.summary.stale, envelope.summary.concepts, displayRoot(cwd, bundleRoot, path)));
65
+ }
66
+ setExitCode(0);
67
+ }).pipe(provideConfig({
68
+ explicitConfigPath: input.config,
69
+ discoveryCwd
70
+ }));
71
+ if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION)))));
72
+ return yield* body;
73
+ })).pipe(Command.withDescription("List every concept whose stale_after instant has passed as of now, with how many whole days past it."));
74
+
75
+ //#endregion
76
+ export { staleCommand };
@@ -5,8 +5,8 @@ import { displayRoot, human, summary } from "../render/human.js";
5
5
  import { Argument, Command, Flag } from "effect/unstable/cli";
6
6
  import { JsonEnvelope, Now, collect, forDiagnostics, json, jsonError, provideConfig, resolveProjectConfig, run } from "@okfit/engine";
7
7
  import { Console, Effect, Layer, Option, Path, Schema } from "effect";
8
- import { Git } from "@effected/git";
9
8
  import { OKF_SPEC_VERSION } from "@okfit/core";
9
+ import { Git } from "@effected/git";
10
10
  import { GitHistory } from "@okfit/profiles";
11
11
 
12
12
  //#region src/commands/validate.ts
@@ -59,6 +59,7 @@ const validateCommand = Command.make("validate", {
59
59
  if (input.format === "json") {
60
60
  const envelope = json({
61
61
  okfitVersion: CLI_VERSION,
62
+ producer: "okfit",
62
63
  okfVersion: merged.okf_version ?? OKF_SPEC_VERSION,
63
64
  root: bundleRoot,
64
65
  profile: Option.match(profile, {
@@ -5,8 +5,8 @@ import { humanVerify } from "../render/verify.js";
5
5
  import { Argument, Command, Flag } from "effect/unstable/cli";
6
6
  import { Now, VerifyEnvelope, jsonError, provideConfig, resolveProjectConfig, runVerify, verifyEnvelope } from "@okfit/engine";
7
7
  import { Console, DateTime, Effect, Option, Path, Schema } from "effect";
8
- import { Git } from "@effected/git";
9
8
  import { Timestamp } from "@okfit/core";
9
+ import { Git } from "@effected/git";
10
10
 
11
11
  //#region src/commands/verify.ts
12
12
  /** V-6: tolerant id, normalised through `ConceptId.normalize`; never `Argument.Path`. */
package/index.d.ts CHANGED
@@ -10,13 +10,13 @@ import "effect";
10
10
  * `okfit` therefore prints the root help and exits `0` with no code in this
11
11
  * package at all.
12
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).
13
+ * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`, and
14
+ * `stale` are the whole command tree; nothing else is registered here.
15
+ * Each is appended in introduction order, never reordered in, so
16
+ * `--help`'s subcommand list reads that way too (contract §4.2). The
17
+ * top-level description is left unchanged: neither `context` nor `verify`
18
+ * validates or scaffolds, but widening the sentence for the orientation-
19
+ * and attestation-only commands buys nothing (contract §4.2).
20
20
  *
21
21
  * @public
22
22
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "private": false,
5
5
  "description": "The okfit command line: validate, lint, index, and inspect Open Knowledge Format (OKF) bundles.",
6
6
  "keywords": [
@@ -52,9 +52,9 @@
52
52
  "@effected/toml": "^0.7.0",
53
53
  "@effected/walker": "^0.9.0",
54
54
  "@effected/yaml": "^0.15.1",
55
- "@okfit/core": "0.4.0",
56
- "@okfit/engine": "0.3.0",
57
- "@okfit/profiles": "0.4.0",
55
+ "@okfit/core": "0.4.1",
56
+ "@okfit/engine": "0.4.0",
57
+ "@okfit/profiles": "0.5.0",
58
58
  "effect": "4.0.0-rc.115"
59
59
  },
60
60
  "engines": {
@@ -0,0 +1,18 @@
1
+ //#region src/render/stale.ts
2
+ /**
3
+ * One line per stale concept: `<id> <stale_after ISO> (<N> days past)`.
4
+ * `items` is `StaleEnvelope`'s own `items` array — already sorted by id
5
+ * (`Derive.staleReport`) — so this renderer never re-sorts.
6
+ *
7
+ * @public
8
+ */
9
+ const humanStale = (items) => items.map((item) => `${item.id} ${item.stale_after} (${item.days_past} days past)`);
10
+ /**
11
+ * `<N> stale concepts of <M> in <root>` — the one-line stderr summary.
12
+ *
13
+ * @public
14
+ */
15
+ const staleSummary = (stale, concepts, root) => `${stale} stale concepts of ${concepts} in ${root}`;
16
+
17
+ //#endregion
18
+ export { humanStale, staleSummary };
package/version.js CHANGED
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * @public
10
10
  */
11
- const CLI_VERSION = "0.5.0";
11
+ const CLI_VERSION = "0.5.1";
12
12
 
13
13
  //#endregion
14
14
  export { CLI_VERSION };