@okfit/cli 0.4.1 → 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 +29 -0
- package/commands/graph.js +75 -0
- package/commands/init.js +6 -8
- package/commands/lint.js +95 -0
- package/commands/root.js +14 -8
- package/commands/stale.js +76 -0
- package/commands/validate.js +2 -1
- package/commands/verify.js +1 -1
- package/index.d.ts +11 -10
- package/package.json +9 -9
- package/render/context.js +16 -3
- package/render/stale.js +18 -0
- package/version.js +1 -1
package/README.md
CHANGED
|
@@ -113,6 +113,34 @@ $ echo $?
|
|
|
113
113
|
and names every colliding file in one error. An ancestor directory's config
|
|
114
114
|
is a legitimate discovery hit, not a collision, and is never probed.
|
|
115
115
|
|
|
116
|
+
If the repository lints markdown, the bundle needs two exemptions from
|
|
117
|
+
markdownlint's defaults, and neither can be satisfied at the source: MD025
|
|
118
|
+
(one top-level heading) counts a concept's frontmatter `title:` as its H1,
|
|
119
|
+
so the body H1 the spec requires reads as a second one, and the spec shapes
|
|
120
|
+
every `index.md` as `# Section` groups, one H1 per concept type. `log.md`
|
|
121
|
+
is fine: `init` and `sync` both start it with `# Log`. Add this to a
|
|
122
|
+
`.markdownlint-cli2.jsonc` at the repository root, adjusting the bundle
|
|
123
|
+
path if `bundle.path` is not `okf`:
|
|
124
|
+
|
|
125
|
+
```jsonc
|
|
126
|
+
"overrides": [
|
|
127
|
+
{
|
|
128
|
+
"combine": "merge",
|
|
129
|
+
"config": { "MD025": { "front_matter_title": "" } },
|
|
130
|
+
"filter": ["okf/**/*.md", "!okf/**/index.md"]
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"combine": "merge",
|
|
134
|
+
"config": { "MD025": false },
|
|
135
|
+
"filter": ["okf/**/index.md"]
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A repository whose lint config cannot express overrides can drop a nested
|
|
141
|
+
`okf/.markdownlint-cli2.jsonc` carrying `{ "config": { "MD025": false } }`
|
|
142
|
+
instead; the bundle loader ignores non-markdown files.
|
|
143
|
+
|
|
116
144
|
`--profile <name>` picks the profile `init` scaffolds for (default: the
|
|
117
145
|
config's `bundle.profile`, itself defaulting to `software-project`); an
|
|
118
146
|
unrecognised name is a warning, not a failure — `init` continues with the
|
|
@@ -250,6 +278,7 @@ nothing else (no summary line, no warnings — those still go to stderr):
|
|
|
250
278
|
{
|
|
251
279
|
"schema": 1,
|
|
252
280
|
"okfit_version": "0.1.0",
|
|
281
|
+
"producer": "okfit",
|
|
253
282
|
"okf_version": "0.2",
|
|
254
283
|
"root": "/abs/path/to/my-repo/okf",
|
|
255
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
|
@@ -3,10 +3,9 @@ import { useColor } from "../internal/tty.js";
|
|
|
3
3
|
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
|
-
import { Console, DateTime, Effect, FileSystem, Layer, Option, Path
|
|
7
|
-
import { TomlCodec } from "@effected/config-file";
|
|
8
|
-
import { Git } from "@effected/git";
|
|
6
|
+
import { Console, DateTime, Effect, FileSystem, Layer, Option, Path } from "effect";
|
|
9
7
|
import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile } from "@okfit/core";
|
|
8
|
+
import { Git } from "@effected/git";
|
|
10
9
|
import { GitHistory, Profiles } from "@okfit/profiles";
|
|
11
10
|
|
|
12
11
|
//#region src/commands/init.ts
|
|
@@ -100,7 +99,8 @@ const initCommand = Command.make("init", {
|
|
|
100
99
|
explicitConfigPath: input.config,
|
|
101
100
|
discoveryCwd
|
|
102
101
|
})(Effect.gen(function* () {
|
|
103
|
-
const
|
|
102
|
+
const configFile = yield* OkfitConfigFile;
|
|
103
|
+
const winner = (yield* configFile.discover)[0];
|
|
104
104
|
const fileConfig = winner === void 0 ? { extensions: {} } : winner.value;
|
|
105
105
|
const profileName = Option.getOrElse(input.profile, () => fileConfig.bundle?.profile ?? DEFAULT_PROFILE_NAME);
|
|
106
106
|
const profile = Profiles.get(profileName);
|
|
@@ -145,12 +145,10 @@ const initCommand = Command.make("init", {
|
|
|
145
145
|
});
|
|
146
146
|
for (const target of paths) yield* fs.makeDirectory(path.dirname(target), { recursive: true });
|
|
147
147
|
const bundlePath = merged.bundle?.path ?? DEFAULT_BUNDLE_PATH;
|
|
148
|
-
|
|
148
|
+
yield* configFile.write(configValue({
|
|
149
149
|
...scaffoldOptions,
|
|
150
150
|
bundlePath
|
|
151
|
-
}));
|
|
152
|
-
const toml = yield* TomlCodec.stringify(encoded);
|
|
153
|
-
yield* fs.writeFileString(`${projectRoot}/${CONFIG_RELATIVE_PATH}`, `${SCHEMA_DIRECTIVE}${toml}`);
|
|
151
|
+
}), `${projectRoot}/${CONFIG_RELATIVE_PATH}`, { header: SCHEMA_DIRECTIVE });
|
|
154
152
|
const scaffoldFiles = yield* files(scaffoldOptions);
|
|
155
153
|
for (const file of scaffoldFiles) yield* fs.writeFileString(file.path, file.contents);
|
|
156
154
|
yield* Console.log(`Initialized ${displayRoot(cwd, bundleRoot, path)} with the ${profileName} profile`);
|
package/commands/lint.js
ADDED
|
@@ -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`,
|
|
18
|
-
* nothing else is registered here.
|
|
19
|
-
*
|
|
20
|
-
* (contract §4.2). The
|
|
21
|
-
* `context` nor `verify`
|
|
22
|
-
*
|
|
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 };
|
package/commands/validate.js
CHANGED
|
@@ -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, {
|
package/commands/verify.js
CHANGED
|
@@ -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,17 +10,17 @@ 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`,
|
|
14
|
-
* nothing else is registered here.
|
|
15
|
-
*
|
|
16
|
-
* (contract §4.2). The
|
|
17
|
-
* `context` nor `verify`
|
|
18
|
-
*
|
|
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
|
*/
|
|
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/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("@effected/xdg").Xdg>;
|
|
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("@effected/config-file").ConfigFileWriteError | 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/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("@effected/xdg").Xdg>;
|
|
24
24
|
//#endregion
|
|
25
25
|
//#region src/errors.d.ts
|
|
26
26
|
/**
|
|
@@ -66,8 +66,9 @@ export declare const renderFailure: (error: unknown) => ReadonlyArray<string>;
|
|
|
66
66
|
//#endregion
|
|
67
67
|
//#region src/render/context.d.ts
|
|
68
68
|
/**
|
|
69
|
-
* The `human` format: a short header block, then one line per type
|
|
70
|
-
* per tag. Pure; the caller
|
|
69
|
+
* The `human` format: a short header block, then one line per type (plus
|
|
70
|
+
* its constraint lines, when it has any) and one per tag. Pure; the caller
|
|
71
|
+
* pipes each line through `Console.log`.
|
|
71
72
|
*
|
|
72
73
|
* The `profile:` line reads `profile: (none) (requested NAME, unknown)`
|
|
73
74
|
* when `profile` and `profile_requested` disagree over an actually-unknown
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okfit/cli",
|
|
3
|
-
"version": "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": [
|
|
@@ -43,18 +43,18 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@effect/platform-node": "4.0.0-rc.115",
|
|
46
|
-
"@effected/cli": "^0.4.
|
|
47
|
-
"@effected/config-file": "^0.
|
|
48
|
-
"@effected/git": "^0.15.
|
|
46
|
+
"@effected/cli": "^0.4.1",
|
|
47
|
+
"@effected/config-file": "^0.9.0",
|
|
48
|
+
"@effected/git": "^0.15.1",
|
|
49
49
|
"@effected/glob": "^0.6.0",
|
|
50
50
|
"@effected/jsonc": "^0.11.0",
|
|
51
51
|
"@effected/markdown": "^0.10.0",
|
|
52
52
|
"@effected/toml": "^0.7.0",
|
|
53
|
-
"@effected/walker": "^0.
|
|
54
|
-
"@effected/yaml": "^0.15.
|
|
55
|
-
"@okfit/core": "0.
|
|
56
|
-
"@okfit/engine": "0.
|
|
57
|
-
"@okfit/profiles": "0.
|
|
53
|
+
"@effected/walker": "^0.9.0",
|
|
54
|
+
"@effected/yaml": "^0.15.1",
|
|
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": {
|
package/render/context.js
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
//#region src/render/context.ts
|
|
2
|
+
/** `kind (a | b)` for an enum field, `resource (path)` for a path field, bare `layer` for free text. */
|
|
3
|
+
const renderField = (field) => field.values !== null ? `${field.name} (${field.values.map((v) => v.name).join(" | ")})` : field.kind !== null ? `${field.name} (${field.kind})` : field.name;
|
|
2
4
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
5
|
+
* The constraint lines under one type bullet (issue #33): what `validate`
|
|
6
|
+
* will require of a concept of this type, so an agent learns it here and
|
|
7
|
+
* not from a lint error. Empty for a type that declares none.
|
|
8
|
+
*/
|
|
9
|
+
const constraintLines = (type) => [
|
|
10
|
+
...type.required !== null && type.required.length > 0 ? [` required: ${type.required.join(", ")}`] : [],
|
|
11
|
+
...type.require_verified === true ? [" verified: required"] : [],
|
|
12
|
+
...type.fields.length > 0 ? [` fields: ${type.fields.map(renderField).join(", ")}`] : []
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* The `human` format: a short header block, then one line per type (plus
|
|
16
|
+
* its constraint lines, when it has any) and one per tag. Pure; the caller
|
|
17
|
+
* pipes each line through `Console.log`.
|
|
5
18
|
*
|
|
6
19
|
* The `profile:` line reads `profile: (none) (requested NAME, unknown)`
|
|
7
20
|
* when `profile` and `profile_requested` disagree over an actually-unknown
|
|
@@ -19,7 +32,7 @@ const humanContext = (envelope) => [
|
|
|
19
32
|
`agent: ${envelope.actors.agent ?? "(unset)"}`,
|
|
20
33
|
"",
|
|
21
34
|
"types:",
|
|
22
|
-
...envelope.types.
|
|
35
|
+
...envelope.types.flatMap((t) => [` ${t.name} ${t.description ?? ""}`, ...constraintLines(t)]),
|
|
23
36
|
"",
|
|
24
37
|
"tags:",
|
|
25
38
|
...envelope.tags.map((t) => ` ${t.name} ${t.description ?? ""}`)
|
package/render/stale.js
ADDED
|
@@ -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 };
|