@okfit/cli 0.6.12 → 0.8.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 +77 -3
- package/commands/context.js +0 -2
- package/commands/graph.js +0 -2
- package/commands/init.js +56 -23
- package/commands/lint.js +4 -5
- package/commands/query.js +172 -0
- package/commands/root.js +8 -4
- package/commands/stale.js +0 -2
- package/commands/sync.js +0 -2
- package/commands/validate.js +4 -5
- package/commands/verify-picker.js +75 -0
- package/commands/verify.js +48 -11
- package/errors.js +13 -2
- package/index.d.ts +61 -7
- package/index.js +2 -1
- package/internal/initWizard.js +121 -0
- package/internal/versionFormatter.js +1 -1
- package/main.js +4 -4
- package/package.json +20 -17
- package/render/human.js +2 -13
- package/render/query.js +58 -0
- package/render/verify.js +10 -6
- package/version.js +1 -1
- package/internal/exit.js +0 -14
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @okfit/cli
|
|
2
2
|
|
|
3
|
-
The `okfit` command line for [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) v0.2 bundles: `okfit validate`, `okfit init`, `okfit context`, `okfit verify`, and `okfit sync`. The full subcommand list is `okf/interfaces/cli-commands.md`'s to keep, not this sentence's to count.
|
|
3
|
+
The `okfit` command line for [Open Knowledge Format (OKF)](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md) v0.2 bundles: `okfit validate`, `okfit init`, `okfit context`, `okfit verify`, `okfit query`, and `okfit sync`. The full subcommand list is `okf/interfaces/cli-commands.md`'s to keep, not this sentence's to count.
|
|
4
4
|
|
|
5
5
|
> **Part of the okfit kit.** Most users want **[@okfit/plugin](https://www.npmjs.com/package/@okfit/plugin)**, which pulls this package in automatically.
|
|
6
6
|
|
|
@@ -9,9 +9,12 @@ The `okfit` command line for [Open Knowledge Format (OKF)](https://github.com/Go
|
|
|
9
9
|
```text
|
|
10
10
|
okfit [--help] [--version]
|
|
11
11
|
okfit validate [path] [--config <file>] [--format human|json] [--skip-provenance] [--document <bundle-path>] [--help]
|
|
12
|
-
okfit init [path] [--profile <name>] [--config <file>] [--help]
|
|
12
|
+
okfit init [path] [--profile <name>] [--bundle <dir>] [--config-location <name>] [--config <file>] [--help]
|
|
13
13
|
okfit context [path] [--config <file>] [--format human|json] [--help]
|
|
14
|
-
okfit verify <id> [path] [--config <file>] [--at <iso>] [--dry-run] [--format human|json] [--help]
|
|
14
|
+
okfit verify [<id>] [path] [--all] [--type <Type>]... [--stable|--draft] [--config <file>] [--at <iso>] [--dry-run] [--format human|json] [--help]
|
|
15
|
+
okfit query list [path] [--type <Type>]... [--tag <tag>]... [--status draft|stable|deprecated]... [--verified|--unverified] [--config <file>] [--format human|json]
|
|
16
|
+
okfit query get <id> [path] [--config <file>] [--format human|json]
|
|
17
|
+
okfit query neighbors <id> [path] [--config <file>] [--format human|json]
|
|
15
18
|
```
|
|
16
19
|
|
|
17
20
|
`okfit --version` prints `okfit <cli> (engine <engine>, okf <okf>,
|
|
@@ -161,6 +164,21 @@ config's `bundle.profile`, itself defaulting to `software-project`); an
|
|
|
161
164
|
unrecognised name is a warning, not a failure — `init` continues with the
|
|
162
165
|
default profile.
|
|
163
166
|
|
|
167
|
+
`--bundle <dir>` sets the bundle directory, relative to the project root
|
|
168
|
+
(default: the config's `bundle.path`, itself defaulting to `okf`). It is
|
|
169
|
+
written to the config's `bundle.path` and is where the scaffold goes. An
|
|
170
|
+
empty, absolute, or root-escaping value (`..`) is a usage error (exit `64`).
|
|
171
|
+
|
|
172
|
+
`--config-location <.config/okfit.toml|okfit.toml|.okfit.toml>` sets where
|
|
173
|
+
the config file is written (default `.config/okfit.toml`); all three are
|
|
174
|
+
locations discovery already reads.
|
|
175
|
+
|
|
176
|
+
When the run is interactive, `init` asks for each of the profile, the bundle
|
|
177
|
+
directory, and the config location that was not given as a flag, in that
|
|
178
|
+
order, with the default pre-selected. Esc or Ctrl-C at any prompt exits `130`
|
|
179
|
+
with `cancelled; nothing written`; every prompt happens before the first
|
|
180
|
+
write. Piped or scripted runs never prompt and behave exactly as before.
|
|
181
|
+
|
|
164
182
|
### `okfit context`
|
|
165
183
|
|
|
166
184
|
Prints the resolved project root, bundle root, config path, profile, and
|
|
@@ -220,9 +238,52 @@ file and an atomic rename, and the target's mode is preserved on the
|
|
|
220
238
|
replacement, but the file is not skipped just because it is `chmod`-ed
|
|
221
239
|
read-only.
|
|
222
240
|
|
|
241
|
+
`--stable` or `--draft` sets the concept's `status` in the same write as the
|
|
242
|
+
attestation, so settling a reviewed draft is one command:
|
|
243
|
+
`okfit verify <id> --stable`. Passing both is a usage error (exit `64`), as
|
|
244
|
+
is passing either with `--all` or `--type`: promotion is a per-concept
|
|
245
|
+
decision. A concept already at the requested status gets no status edit. The
|
|
246
|
+
human line ends `; status <from> -> <to>`, `; status already <to>` or
|
|
247
|
+
`; status (absent) -> <to>`, and a dry run adds a `would set status:` line
|
|
248
|
+
with the exact fragment.
|
|
249
|
+
|
|
250
|
+
`--all` and `--type <Type>` (repeatable) attest in batch: every concept whose
|
|
251
|
+
type sets `require_verified` (or of the named types) that you have not
|
|
252
|
+
already verified. A batch skips a concept and reports why: `draft`,
|
|
253
|
+
`deprecated`, or `already-verified`. A batch never re-attests a
|
|
254
|
+
`deprecated` concept; verifying one by id is still allowed.
|
|
255
|
+
|
|
256
|
+
Bare `okfit verify` in a terminal opens a picker: every concept of a
|
|
257
|
+
`require_verified` type you have not attested, drafts included and
|
|
258
|
+
deprecated excluded, grouped by type (↑/↓ move, space toggles, `a` toggles a
|
|
259
|
+
section, Enter continues). A confirm step follows, with a "promote K drafts
|
|
260
|
+
to stable" toggle that is on by default. The picked concepts are attested in
|
|
261
|
+
one all-or-nothing write. Esc, `q`, Ctrl-C or answering no exits `130` with
|
|
262
|
+
nothing written. Without a terminal (a pipe, an agent or CI audience, or
|
|
263
|
+
`--format json`) bare `verify` is a usage error (exit `64`).
|
|
264
|
+
|
|
223
265
|
This is a human-run command: it records **your** attestation that you
|
|
224
266
|
reviewed the concept, so no agent, hook, or MCP tool ever invokes it.
|
|
225
267
|
|
|
268
|
+
### `okfit query`
|
|
269
|
+
|
|
270
|
+
Read-only lookups over the bundle, backed by the engine's `ConceptQuery`
|
|
271
|
+
layer (the same one the MCP `list_concepts`, `get_concept` and
|
|
272
|
+
`concept_neighbors` tools use). Bare `okfit query` prints help.
|
|
273
|
+
|
|
274
|
+
- `okfit query list [path]` lists concepts as summaries (id, type, title,
|
|
275
|
+
description, status, tags, path). Filters: `--type <Type>` and
|
|
276
|
+
`--tag <tag>` (repeatable, each must be declared in the config),
|
|
277
|
+
`--status draft|stable|deprecated` (repeatable), and `--verified` or
|
|
278
|
+
`--unverified`.
|
|
279
|
+
- `okfit query get <id> [path]` prints one concept: the summary fields and its
|
|
280
|
+
outgoing links. `--format json` also includes the raw frontmatter.
|
|
281
|
+
- `okfit query neighbors <id> [path]` prints a concept's outgoing and
|
|
282
|
+
incoming links.
|
|
283
|
+
|
|
284
|
+
Exit `0` on success, `3` for an unknown id, `64` for an undeclared type or
|
|
285
|
+
tag or for `--verified` together with `--unverified`.
|
|
286
|
+
|
|
226
287
|
## Config discovery
|
|
227
288
|
|
|
228
289
|
With no `--config` flag, `okfit` walks upward from `[path]` (default:
|
|
@@ -347,6 +408,19 @@ envelope to stdout and exits `3`:
|
|
|
347
408
|
|
|
348
409
|
`init` has no `--format`; it is human output only.
|
|
349
410
|
|
|
411
|
+
`okfit verify --format json` prints `VerifyEnvelope` (`schema`, `okfit_version`,
|
|
412
|
+
`engine_version`, `distribution`, `id`, `path`, `verified`, `status`,
|
|
413
|
+
`dry_run`, `exit_code`), where `status` is `{ "from": ..., "to": ... }` when
|
|
414
|
+
`--stable` or `--draft` was given and `null` otherwise. With `--all` or
|
|
415
|
+
`--type` it prints `VerifyBatchEnvelope`, whose `skipped[].reason` is one of
|
|
416
|
+
`draft`, `deprecated` or `already-verified`.
|
|
417
|
+
|
|
418
|
+
`okfit query list --format json` prints `QueryListEnvelope` (`total`, `items`);
|
|
419
|
+
`query get` prints `QueryGetEnvelope` (`concept`: the summary fields plus
|
|
420
|
+
`frontmatter` and `links`); `query neighbors` prints `QueryNeighborsEnvelope`
|
|
421
|
+
(`id`, `outgoing`, `incoming`). Each carries `schema`, `okfit_version`,
|
|
422
|
+
`engine_version` and `distribution`.
|
|
423
|
+
|
|
350
424
|
`okfit context --format json` prints its own envelope, distinct from the
|
|
351
425
|
one above:
|
|
352
426
|
|
package/commands/context.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { humanContext } from "../render/context.js";
|
|
3
2
|
import { CLI_VERSION } from "../version.js";
|
|
4
3
|
import { Argument, Command, Flag } from "effect/cli";
|
|
@@ -64,7 +63,6 @@ const contextCommand = Command.make("context", {
|
|
|
64
63
|
});
|
|
65
64
|
if (input.format === "json") yield* Console.log(JSON.stringify(Schema.encodeSync(ContextEnvelope)(envelope)));
|
|
66
65
|
else for (const contextLine of humanContext(envelope)) yield* Console.log(contextLine);
|
|
67
|
-
setExitCode(0);
|
|
68
66
|
}).pipe(provideConfig({
|
|
69
67
|
explicitConfigPath: input.config,
|
|
70
68
|
discoveryCwd
|
package/commands/graph.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { Argument, Command, Flag } from "effect/cli";
|
|
4
3
|
import { CurrentDistribution } from "@effected/engine";
|
|
@@ -65,7 +64,6 @@ const graphCommand = Command.make("graph", {
|
|
|
65
64
|
yield* Console.log(JSON.stringify(Schema.encodeSync(GraphEnvelope)(envelope)));
|
|
66
65
|
} else if (input.format === "dot") yield* Console.log(result.graph.toGraphViz());
|
|
67
66
|
else yield* Console.log(result.graph.toMermaid());
|
|
68
|
-
setExitCode(0);
|
|
69
67
|
}).pipe(provideConfig({
|
|
70
68
|
explicitConfigPath: input.config,
|
|
71
69
|
discoveryCwd
|
package/commands/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { INIT_CONFIG_LOCATIONS, initWizard } from "../internal/initWizard.js";
|
|
2
2
|
import { displayRoot, human, summary } from "../render/human.js";
|
|
3
|
+
import { CliExit, CliTheme } from "@effected/cli";
|
|
3
4
|
import { Argument, Command, Flag } from "effect/cli";
|
|
4
5
|
import { CONFIG_RELATIVE_PATH, InitOverwriteError, Now, collect, configValue, files, forDiagnostics, provideConfig, resolveBundleRoot, resolveProjectRoot, run, targetPaths } from "@okfit/engine";
|
|
5
6
|
import { Console, DateTime, Effect, FileSystem, Layer, Option, Path } from "effect";
|
|
6
7
|
import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile, SCHEMA_DIRECTIVE } from "@okfit/core";
|
|
7
|
-
import { CliColor } from "@effected/cli";
|
|
8
8
|
import { Git } from "@effected/git";
|
|
9
9
|
import { GitHistory, Profiles } from "@okfit/profiles";
|
|
10
10
|
|
|
@@ -23,7 +23,11 @@ const configFlag = Flag.File("config").pipe(Flag.optional, Flag.withDescription(
|
|
|
23
23
|
* CLI-rendered warning (K-4), matching `Profiles.get`'s own `Option.none`
|
|
24
24
|
* contract (P-38), not a parser-level `CliError.InvalidValue`.
|
|
25
25
|
*/
|
|
26
|
-
const profileFlag = Flag.String("profile").pipe(Flag.optional, Flag.withDescription("profile to scaffold with (default: the config's bundle.profile or software-project)"));
|
|
26
|
+
const profileFlag = Flag.String("profile").pipe(Flag.optional, Flag.withDescription("profile to scaffold with (default: the config's bundle.profile or software-project; prompted for when interactive)"));
|
|
27
|
+
/** `--bundle <dir>`: project-relative; validated by the wizard before anything else (usage error, exit 64). */
|
|
28
|
+
const bundleFlag = Flag.String("bundle").pipe(Flag.optional, Flag.withDescription("bundle directory, relative to the project root (default: the config's bundle.path or okf; prompted for when interactive)"));
|
|
29
|
+
/** `--config-location`: where the config file is written, one of the three discovered names (C-1). */
|
|
30
|
+
const configLocationFlag = Flag.Literals("config-location", INIT_CONFIG_LOCATIONS).pipe(Flag.optional, Flag.withDescription("where to write the config file (default: .config/okfit.toml; prompted for when interactive)"));
|
|
27
31
|
/**
|
|
28
32
|
* Stands in for `profile.config` when `Profiles.get` returns `None`.
|
|
29
33
|
* `OkfitConfig.merge` only visits `Object.keys(override)`
|
|
@@ -48,8 +52,13 @@ const countsOf = (diagnostics, concepts) => ({
|
|
|
48
52
|
concepts
|
|
49
53
|
});
|
|
50
54
|
/**
|
|
51
|
-
* `okfit init [path] [--profile <name>] [--
|
|
52
|
-
* `--format`, human output only, K-6).
|
|
55
|
+
* `okfit init [path] [--profile <name>] [--bundle <dir>] [--config-location <name>]
|
|
56
|
+
* [--config <file>]` (K-2, K-3; no `--format`, human output only, K-6).
|
|
57
|
+
*
|
|
58
|
+
* Interactive runs prompt (the kit's Ink screens, `initWizard`) for each of
|
|
59
|
+
* profile, bundle directory and config location whose flag was not given;
|
|
60
|
+
* every prompt happens after discovery and before any write (step 4), so a
|
|
61
|
+
* cancel exits 130 with nothing written.
|
|
53
62
|
*
|
|
54
63
|
* Handler order, fixed by the contract:
|
|
55
64
|
*
|
|
@@ -61,7 +70,8 @@ const countsOf = (diagnostics, concepts) => ({
|
|
|
61
70
|
* `sources[0]`.
|
|
62
71
|
* 4. `profileName = Option.getOrElse(input.profile, () =>
|
|
63
72
|
* fileConfig.bundle?.profile ?? DEFAULTS.bundle.profile)` (K-3, the one
|
|
64
|
-
* difference from `validate`'s step 4)
|
|
73
|
+
* difference from `validate`'s step 4), then `--bundle`/`--config-location`
|
|
74
|
+
* likewise, all three through `initWizard`. `Profiles.get(profileName)`.
|
|
65
75
|
* `None` and `profileName !== "none"` warns (K-4); `"none"` is silent.
|
|
66
76
|
* 5. `merged = OkfitConfig.merge(OkfitConfig.merge(DEFAULTS, profileConfig),
|
|
67
77
|
* fileConfig)`, `profileConfig` falling back to `NO_PROFILE_CONFIG` when
|
|
@@ -81,19 +91,15 @@ const countsOf = (diagnostics, concepts) => ({
|
|
|
81
91
|
* 10. `Console.log` the K-51 success line.
|
|
82
92
|
* 11. self-validate (K-29): `run({ root: bundleRoot, config: merged,
|
|
83
93
|
* profile, now })` over the bundle just written, rendered exactly as
|
|
84
|
-
* `validate --format human` does, `
|
|
94
|
+
* `validate --format human` does, `CliExit.set` to its
|
|
85
95
|
* `forDiagnostics` result. The handler SUCCEEDS (K-7); the failure path
|
|
86
96
|
* is only `InitOverwriteError` at step 8, or an infrastructure error
|
|
87
97
|
* that already carries its own `[Runtime.errorExitCode]`.
|
|
88
98
|
*
|
|
89
99
|
* @public
|
|
90
100
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
config: configFlag,
|
|
94
|
-
profile: profileFlag
|
|
95
|
-
}, (input) => Effect.gen(function* () {
|
|
96
|
-
const cwd = process.cwd();
|
|
101
|
+
/** The `init` handler body, parameterised on `cwd` so tests can drive it against a temp project. */
|
|
102
|
+
const initProgram = (input, cwd) => Effect.gen(function* () {
|
|
97
103
|
const discoveryCwd = Option.getOrElse(input.path, () => cwd);
|
|
98
104
|
yield* provideConfig({
|
|
99
105
|
explicitConfigPath: input.config,
|
|
@@ -102,14 +108,31 @@ const initCommand = Command.make("init", {
|
|
|
102
108
|
const configFile = yield* OkfitConfigFile;
|
|
103
109
|
const winner = (yield* configFile.discover)[0];
|
|
104
110
|
const fileConfig = winner === void 0 ? { extensions: {} } : winner.value;
|
|
105
|
-
const
|
|
111
|
+
const defaultBundle = fileConfig.bundle?.path ?? DEFAULT_BUNDLE_PATH;
|
|
112
|
+
const answers = yield* initWizard({
|
|
113
|
+
profile: input.profile,
|
|
114
|
+
bundle: input.bundle,
|
|
115
|
+
location: input.configLocation
|
|
116
|
+
}, {
|
|
117
|
+
profile: fileConfig.bundle?.profile ?? DEFAULT_PROFILE_NAME,
|
|
118
|
+
bundle: defaultBundle,
|
|
119
|
+
location: CONFIG_RELATIVE_PATH
|
|
120
|
+
});
|
|
121
|
+
const profileName = answers.profile;
|
|
106
122
|
const profile = Profiles.get(profileName);
|
|
107
123
|
if (Option.isNone(profile) && profileName !== "none") yield* Console.error(`warning: unknown profile "${profileName}"; continuing with defaults`);
|
|
108
124
|
const profileConfig = Option.match(profile, {
|
|
109
125
|
onNone: () => NO_PROFILE_CONFIG,
|
|
110
126
|
onSome: (resolved) => resolved.config
|
|
111
127
|
});
|
|
112
|
-
const
|
|
128
|
+
const mergedBase = OkfitConfig.merge(OkfitConfig.merge(OkfitConfig.DEFAULTS, profileConfig), fileConfig);
|
|
129
|
+
const merged = {
|
|
130
|
+
...mergedBase,
|
|
131
|
+
bundle: {
|
|
132
|
+
...mergedBase.bundle,
|
|
133
|
+
path: answers.bundle
|
|
134
|
+
}
|
|
135
|
+
};
|
|
113
136
|
if (merged.okf_version !== void 0 && merged.okf_version !== OKF_SPEC_VERSION) yield* Console.error(`warning: okf_version "${merged.okf_version}" does not match this okfit's spec version "${OKF_SPEC_VERSION}"; continuing`);
|
|
114
137
|
const path = yield* Path.Path;
|
|
115
138
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -143,12 +166,14 @@ const initCommand = Command.make("init", {
|
|
|
143
166
|
paths: existing,
|
|
144
167
|
cwd
|
|
145
168
|
});
|
|
146
|
-
|
|
147
|
-
const
|
|
169
|
+
const configTargets = new Set(INIT_CONFIG_LOCATIONS.map((location) => `${projectRoot}/${location}`));
|
|
170
|
+
const written = [...paths.filter((target) => !configTargets.has(target)), `${projectRoot}/${answers.location}`];
|
|
171
|
+
for (const target of written) yield* fs.makeDirectory(path.dirname(target), { recursive: true });
|
|
172
|
+
const bundlePath = answers.bundle;
|
|
148
173
|
yield* configFile.write(configValue({
|
|
149
174
|
...scaffoldOptions,
|
|
150
175
|
bundlePath
|
|
151
|
-
}), `${projectRoot}/${
|
|
176
|
+
}), `${projectRoot}/${answers.location}`, { header: SCHEMA_DIRECTIVE });
|
|
152
177
|
const scaffoldFiles = yield* files(scaffoldOptions);
|
|
153
178
|
for (const file of scaffoldFiles) yield* fs.writeFileString(file.path, file.contents);
|
|
154
179
|
yield* Console.log(`Initialized ${displayRoot(cwd, bundleRoot, path)} with the ${profileName} profile`);
|
|
@@ -159,12 +184,20 @@ const initCommand = Command.make("init", {
|
|
|
159
184
|
now
|
|
160
185
|
}).pipe(Effect.provide(Layer.mergeAll(Git.layer, GitHistory.layer)));
|
|
161
186
|
const diagnostics = collect(result.report.conformance, result.report.lint, result.profileDiagnostics);
|
|
162
|
-
const
|
|
163
|
-
for (const line of human(diagnostics, {
|
|
187
|
+
const theme = yield* CliTheme;
|
|
188
|
+
for (const line of human(diagnostics, { paint: theme.paint })) yield* Console.log(line);
|
|
164
189
|
yield* Console.error(summary(countsOf(diagnostics, result.bundle.concepts.size), displayRoot(cwd, bundleRoot, path)));
|
|
165
|
-
|
|
190
|
+
yield* CliExit.set(forDiagnostics(diagnostics));
|
|
166
191
|
}));
|
|
167
|
-
})
|
|
192
|
+
});
|
|
193
|
+
/** @public */
|
|
194
|
+
const initCommand = Command.make("init", {
|
|
195
|
+
path: pathArg,
|
|
196
|
+
config: configFlag,
|
|
197
|
+
profile: profileFlag,
|
|
198
|
+
bundle: bundleFlag,
|
|
199
|
+
configLocation: configLocationFlag
|
|
200
|
+
}, (input) => initProgram(input, process.cwd())).pipe(Command.withDescription("Scaffold a new OKF bundle: a config file, the bundle's root and per-directory index files, a project stub, and an initial log entry."));
|
|
168
201
|
|
|
169
202
|
//#endregion
|
|
170
|
-
export { initCommand };
|
|
203
|
+
export { initCommand, initProgram };
|
package/commands/lint.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { displayRoot, human, summary } from "../render/human.js";
|
|
3
|
+
import { CliExit, CliTheme } from "@effected/cli";
|
|
4
4
|
import { Argument, Command, Flag } from "effect/cli";
|
|
5
5
|
import { CurrentDistribution } from "@effected/engine";
|
|
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
8
|
import { OKF_SPEC_VERSION } from "@okfit/core";
|
|
9
|
-
import { CliColor } from "@effected/cli";
|
|
10
9
|
import { Git } from "@effected/git";
|
|
11
10
|
import { GitHistory } from "@okfit/profiles";
|
|
12
11
|
|
|
@@ -76,8 +75,8 @@ const lintCommand = Command.make("lint", {
|
|
|
76
75
|
});
|
|
77
76
|
yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
|
|
78
77
|
} else {
|
|
79
|
-
const
|
|
80
|
-
for (const diagnosticLine of human(diagnostics, {
|
|
78
|
+
const theme = yield* CliTheme;
|
|
79
|
+
for (const diagnosticLine of human(diagnostics, { paint: theme.paint })) yield* Console.log(diagnosticLine);
|
|
81
80
|
const counts = {
|
|
82
81
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
83
82
|
warnings: diagnostics.filter((d) => d.severity === "warning").length,
|
|
@@ -86,7 +85,7 @@ const lintCommand = Command.make("lint", {
|
|
|
86
85
|
};
|
|
87
86
|
yield* Console.error(summary(counts, displayRoot(cwd, bundleRoot, path)));
|
|
88
87
|
}
|
|
89
|
-
|
|
88
|
+
yield* CliExit.set(code);
|
|
90
89
|
}).pipe(provideConfig({
|
|
91
90
|
explicitConfigPath: input.config,
|
|
92
91
|
discoveryCwd
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { CLI_VERSION } from "../version.js";
|
|
2
|
+
import { displayRoot } from "../render/human.js";
|
|
3
|
+
import { humanQueryGet, humanQueryList, humanQueryNeighbors, queryListSummary } from "../render/query.js";
|
|
4
|
+
import { Argument, Command, Flag } from "effect/cli";
|
|
5
|
+
import { CurrentDistribution } from "@effected/engine";
|
|
6
|
+
import { ConceptQuery, QueryGetEnvelope, QueryListEnvelope, QueryNeighborsEnvelope, QuerySelectionError, jsonError, provideConfig, queryGetEnvelope, queryListEnvelope, queryNeighborsEnvelope, resolveProjectConfig, toConceptSummary } from "@okfit/engine";
|
|
7
|
+
import { Console, Effect, Option, Path, Schema } from "effect";
|
|
8
|
+
import { Bundle } from "@okfit/core";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/query.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
|
+
const idArg = Argument.String("id").pipe(Argument.withDescription("concept id, with or without a leading slash or trailing .md"));
|
|
14
|
+
/** K-1: no `mustExist` — the handler stats the path itself, before building any layer. */
|
|
15
|
+
const configFlag = Flag.File("config").pipe(Flag.optional, Flag.withDescription("explicit config file; skips discovery"));
|
|
16
|
+
const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
|
|
17
|
+
const typeFlag = Flag.String("type").pipe(Flag.atLeast(0), Flag.withDescription("only concepts of this type (repeatable; any match)"));
|
|
18
|
+
const tagFlag = Flag.String("tag").pipe(Flag.atLeast(0), Flag.withDescription("only concepts carrying this tag (repeatable; all must match)"));
|
|
19
|
+
const statusFlag = Flag.Literals("status", [
|
|
20
|
+
"draft",
|
|
21
|
+
"stable",
|
|
22
|
+
"deprecated"
|
|
23
|
+
]).pipe(Flag.atLeast(0), Flag.withDescription("only concepts with this status (repeatable; any match)"));
|
|
24
|
+
const verifiedFlag = Flag.Boolean("verified").pipe(Flag.withDefault(false), Flag.withDescription("only concepts with at least one verified entry"));
|
|
25
|
+
const unverifiedFlag = Flag.Boolean("unverified").pipe(Flag.withDefault(false), Flag.withDescription("only concepts with no verified entry"));
|
|
26
|
+
/**
|
|
27
|
+
* Shared skeleton: discover config, load the bundle, run `use`, and — under
|
|
28
|
+
* `--format json` — apply the K-22 stdout error envelope. `precheck` runs
|
|
29
|
+
* first, before any config discovery, so an invalid flag combination fails fast.
|
|
30
|
+
*/
|
|
31
|
+
const run$1 = (input, use, precheck) => Effect.gen(function* () {
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const discoveryCwd = Option.getOrElse(input.path, () => cwd);
|
|
34
|
+
const path = yield* Path.Path;
|
|
35
|
+
const distribution = yield* CurrentDistribution;
|
|
36
|
+
const body = Effect.gen(function* () {
|
|
37
|
+
if (precheck !== void 0) yield* precheck;
|
|
38
|
+
const resolved = yield* resolveProjectConfig({
|
|
39
|
+
pathArg: input.path,
|
|
40
|
+
explicitConfigPath: input.config,
|
|
41
|
+
cwd
|
|
42
|
+
});
|
|
43
|
+
yield* use({
|
|
44
|
+
bundle: yield* Bundle.load({ root: resolved.bundleRoot }),
|
|
45
|
+
config: resolved.config,
|
|
46
|
+
root: displayRoot(cwd, resolved.bundleRoot, path),
|
|
47
|
+
distribution
|
|
48
|
+
});
|
|
49
|
+
}).pipe(provideConfig({
|
|
50
|
+
explicitConfigPath: input.config,
|
|
51
|
+
discoveryCwd
|
|
52
|
+
}));
|
|
53
|
+
if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
|
|
54
|
+
return yield* body;
|
|
55
|
+
});
|
|
56
|
+
const distributionOf = (distribution) => Option.isSome(distribution) ? { distribution: distribution.value } : {};
|
|
57
|
+
/**
|
|
58
|
+
* `okfit query list [path] [--type <T>]... [--tag <t>]... [--status <s>]...
|
|
59
|
+
* [--verified|--unverified] [--config <file>] [--format human|json]`.
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
const queryListCommand = Command.make("list", {
|
|
64
|
+
path: pathArg,
|
|
65
|
+
config: configFlag,
|
|
66
|
+
type: typeFlag,
|
|
67
|
+
tag: tagFlag,
|
|
68
|
+
status: statusFlag,
|
|
69
|
+
verified: verifiedFlag,
|
|
70
|
+
unverified: unverifiedFlag,
|
|
71
|
+
format: formatFlag
|
|
72
|
+
}, (input) => {
|
|
73
|
+
const filter = {
|
|
74
|
+
...input.type.length > 0 ? { types: input.type } : {},
|
|
75
|
+
...input.tag.length > 0 ? { tags: input.tag } : {},
|
|
76
|
+
...input.status.length > 0 ? { statuses: input.status } : {},
|
|
77
|
+
...input.verified ? { verified: true } : {},
|
|
78
|
+
...input.unverified ? { verified: false } : {}
|
|
79
|
+
};
|
|
80
|
+
return run$1(input, ({ bundle, config, root, distribution }) => Effect.gen(function* () {
|
|
81
|
+
const items = (yield* ConceptQuery.list(bundle, config, filter)).map(toConceptSummary);
|
|
82
|
+
if (input.format === "json") {
|
|
83
|
+
const envelope = queryListEnvelope({
|
|
84
|
+
okfitVersion: CLI_VERSION,
|
|
85
|
+
total: items.length,
|
|
86
|
+
items,
|
|
87
|
+
...distributionOf(distribution)
|
|
88
|
+
});
|
|
89
|
+
yield* Console.log(JSON.stringify(Schema.encodeSync(QueryListEnvelope)(envelope)));
|
|
90
|
+
} else {
|
|
91
|
+
for (const line of humanQueryList(items)) yield* Console.log(line);
|
|
92
|
+
yield* Console.error(queryListSummary(items.length, root));
|
|
93
|
+
}
|
|
94
|
+
}), input.verified && input.unverified ? Effect.fail(new QuerySelectionError({ reason: "verified-conflict" })) : void 0);
|
|
95
|
+
}).pipe(Command.withDescription("List concepts, sorted by id, optionally filtered by type, tag, status, or verified state."));
|
|
96
|
+
/**
|
|
97
|
+
* `okfit query get <id> [path] [--config <file>] [--format human|json]`.
|
|
98
|
+
*
|
|
99
|
+
* @public
|
|
100
|
+
*/
|
|
101
|
+
const queryGetCommand = Command.make("get", {
|
|
102
|
+
id: idArg,
|
|
103
|
+
path: pathArg,
|
|
104
|
+
config: configFlag,
|
|
105
|
+
format: formatFlag
|
|
106
|
+
}, (input) => run$1(input, ({ bundle, distribution }) => Effect.gen(function* () {
|
|
107
|
+
const { concept, links } = yield* ConceptQuery.get(bundle, input.id);
|
|
108
|
+
const summary = toConceptSummary(concept);
|
|
109
|
+
if (input.format === "json") {
|
|
110
|
+
const envelope = queryGetEnvelope({
|
|
111
|
+
okfitVersion: CLI_VERSION,
|
|
112
|
+
concept: summary,
|
|
113
|
+
frontmatter: concept.frontmatter.raw,
|
|
114
|
+
links,
|
|
115
|
+
...distributionOf(distribution)
|
|
116
|
+
});
|
|
117
|
+
yield* Console.log(JSON.stringify(Schema.encodeSync(QueryGetEnvelope)(envelope)));
|
|
118
|
+
} else for (const line of humanQueryGet({
|
|
119
|
+
summary,
|
|
120
|
+
verified: concept.frontmatter.verified ?? [],
|
|
121
|
+
links
|
|
122
|
+
})) yield* Console.log(line);
|
|
123
|
+
}))).pipe(Command.withDescription("Show one concept: its fields, verified entries, and outgoing links."));
|
|
124
|
+
/**
|
|
125
|
+
* `okfit query neighbors <id> [path] [--config <file>] [--format human|json]`.
|
|
126
|
+
*
|
|
127
|
+
* @public
|
|
128
|
+
*/
|
|
129
|
+
const queryNeighborsCommand = Command.make("neighbors", {
|
|
130
|
+
id: idArg,
|
|
131
|
+
path: pathArg,
|
|
132
|
+
config: configFlag,
|
|
133
|
+
format: formatFlag
|
|
134
|
+
}, (input) => run$1(input, ({ bundle, distribution }) => Effect.gen(function* () {
|
|
135
|
+
const result = yield* ConceptQuery.neighbors(bundle, input.id);
|
|
136
|
+
const project = (n) => ({
|
|
137
|
+
id: n.id,
|
|
138
|
+
kind: n.kind,
|
|
139
|
+
summary: n.concept === null ? null : toConceptSummary(n.concept)
|
|
140
|
+
});
|
|
141
|
+
const outgoing = result.outgoing.map(project);
|
|
142
|
+
const incoming = result.incoming.map(project);
|
|
143
|
+
if (input.format === "json") {
|
|
144
|
+
const envelope = queryNeighborsEnvelope({
|
|
145
|
+
okfitVersion: CLI_VERSION,
|
|
146
|
+
id: result.id,
|
|
147
|
+
outgoing,
|
|
148
|
+
incoming,
|
|
149
|
+
...distributionOf(distribution)
|
|
150
|
+
});
|
|
151
|
+
yield* Console.log(JSON.stringify(Schema.encodeSync(QueryNeighborsEnvelope)(envelope)));
|
|
152
|
+
} else for (const line of humanQueryNeighbors({
|
|
153
|
+
outgoing,
|
|
154
|
+
incoming
|
|
155
|
+
})) yield* Console.log(line);
|
|
156
|
+
}))).pipe(Command.withDescription("Show the concepts a concept links to and the ones that link to it."));
|
|
157
|
+
/**
|
|
158
|
+
* `okfit query`: read-only questions about the bundle, one subcommand per
|
|
159
|
+
* MCP query tool (`list_concepts`, `get_concept`, `concept_neighbors`) over
|
|
160
|
+
* the same `@okfit/engine` `ConceptQuery`. No handler: bare `okfit query`
|
|
161
|
+
* prints help and exits 0, the root command's own K-5 behaviour.
|
|
162
|
+
*
|
|
163
|
+
* @public
|
|
164
|
+
*/
|
|
165
|
+
const queryCommand = Command.make("query", {}).pipe(Command.withDescription("Read-only queries over the bundle: list, get, neighbors."), Command.withSubcommands([
|
|
166
|
+
queryListCommand,
|
|
167
|
+
queryGetCommand,
|
|
168
|
+
queryNeighborsCommand
|
|
169
|
+
]));
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { queryCommand, queryGetCommand, queryListCommand, queryNeighborsCommand };
|
package/commands/root.js
CHANGED
|
@@ -2,10 +2,12 @@ import { contextCommand } from "./context.js";
|
|
|
2
2
|
import { graphCommand } from "./graph.js";
|
|
3
3
|
import { initCommand } from "./init.js";
|
|
4
4
|
import { lintCommand } from "./lint.js";
|
|
5
|
+
import { queryCommand } from "./query.js";
|
|
5
6
|
import { staleCommand } from "./stale.js";
|
|
6
7
|
import { syncCommand } from "./sync.js";
|
|
7
8
|
import { validateCommand } from "./validate.js";
|
|
8
9
|
import { verifyCommand } from "./verify.js";
|
|
10
|
+
import { CliAudience } from "@effected/cli";
|
|
9
11
|
import { Command } from "effect/cli";
|
|
10
12
|
|
|
11
13
|
//#region src/commands/root.ts
|
|
@@ -17,8 +19,9 @@ import { Command } from "effect/cli";
|
|
|
17
19
|
* `okfit` therefore prints the root help and exits `0` with no code in this
|
|
18
20
|
* package at all.
|
|
19
21
|
*
|
|
20
|
-
* `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`,
|
|
21
|
-
* `stale
|
|
22
|
+
* `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`,
|
|
23
|
+
* `stale`, and `query` (with its `list`/`get`/`neighbors`) are the whole
|
|
24
|
+
* command tree; nothing else is registered here.
|
|
22
25
|
* Each is appended in introduction order, never reordered in, so
|
|
23
26
|
* `--help`'s subcommand list reads that way too (contract §4.2). The
|
|
24
27
|
* top-level description is left unchanged: neither `context` nor `verify`
|
|
@@ -27,7 +30,7 @@ import { Command } from "effect/cli";
|
|
|
27
30
|
*
|
|
28
31
|
* @public
|
|
29
32
|
*/
|
|
30
|
-
const rootCommand = Command.make("okfit", {}).pipe(Command.withDescription("Open Knowledge Format (OKF) v0.2 tooling: validate and scaffold bundles."), Command.withSubcommands([
|
|
33
|
+
const rootCommand = Command.make("okfit", {}).pipe(Command.withDescription("Open Knowledge Format (OKF) v0.2 tooling: validate and scaffold bundles."), Command.withSharedFlags(CliAudience.flags()), Command.withSubcommands([
|
|
31
34
|
validateCommand,
|
|
32
35
|
initCommand,
|
|
33
36
|
contextCommand,
|
|
@@ -35,7 +38,8 @@ const rootCommand = Command.make("okfit", {}).pipe(Command.withDescription("Open
|
|
|
35
38
|
syncCommand,
|
|
36
39
|
lintCommand,
|
|
37
40
|
graphCommand,
|
|
38
|
-
staleCommand
|
|
41
|
+
staleCommand,
|
|
42
|
+
queryCommand
|
|
39
43
|
]));
|
|
40
44
|
|
|
41
45
|
//#endregion
|
package/commands/stale.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { displayRoot } from "../render/human.js";
|
|
4
3
|
import { humanStale, staleSummary } from "../render/stale.js";
|
|
@@ -66,7 +65,6 @@ const staleCommand = Command.make("stale", {
|
|
|
66
65
|
for (const line of humanStale(envelope.items)) yield* Console.log(line);
|
|
67
66
|
yield* Console.error(staleSummary(envelope.summary.stale, envelope.summary.concepts, displayRoot(cwd, bundleRoot, path)));
|
|
68
67
|
}
|
|
69
|
-
setExitCode(0);
|
|
70
68
|
}).pipe(provideConfig({
|
|
71
69
|
explicitConfigPath: input.config,
|
|
72
70
|
discoveryCwd
|
package/commands/sync.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { displayRoot } from "../render/human.js";
|
|
4
3
|
import { humanSync } from "../render/sync.js";
|
|
@@ -110,7 +109,6 @@ const syncCommand = Command.make("sync", {
|
|
|
110
109
|
});
|
|
111
110
|
yield* Console.log(JSON.stringify(Schema.encodeSync(SyncEnvelope)(envelope)));
|
|
112
111
|
} else for (const line of humanSync(result)) yield* Console.log(line);
|
|
113
|
-
setExitCode(0);
|
|
114
112
|
}).pipe(provideConfig({
|
|
115
113
|
explicitConfigPath: input.config,
|
|
116
114
|
discoveryCwd
|
package/commands/validate.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { displayRoot, human, summary } from "../render/human.js";
|
|
4
3
|
import { readDocumentText } from "../internal/stdin.js";
|
|
4
|
+
import { CliExit, CliTheme } from "@effected/cli";
|
|
5
5
|
import { Argument, Command, Flag } from "effect/cli";
|
|
6
6
|
import { CurrentDistribution } from "@effected/engine";
|
|
7
7
|
import { JsonEnvelope, Now, collect, forDiagnostics, json, jsonError, provideConfig, provideDocuments, resolveProjectConfig, run } from "@okfit/engine";
|
|
8
8
|
import { Console, Effect, Layer, Option, Path, Schema } from "effect";
|
|
9
9
|
import { OKF_SPEC_VERSION } from "@okfit/core";
|
|
10
|
-
import { CliColor } from "@effected/cli";
|
|
11
10
|
import { Git } from "@effected/git";
|
|
12
11
|
import { GitHistory } from "@okfit/profiles";
|
|
13
12
|
|
|
@@ -83,8 +82,8 @@ const validateCommand = Command.make("validate", {
|
|
|
83
82
|
});
|
|
84
83
|
yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
|
|
85
84
|
} else {
|
|
86
|
-
const
|
|
87
|
-
for (const diagnosticLine of human(diagnostics, {
|
|
85
|
+
const theme = yield* CliTheme;
|
|
86
|
+
for (const diagnosticLine of human(diagnostics, { paint: theme.paint })) yield* Console.log(diagnosticLine);
|
|
88
87
|
const counts = {
|
|
89
88
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
90
89
|
warnings: diagnostics.filter((d) => d.severity === "warning").length,
|
|
@@ -93,7 +92,7 @@ const validateCommand = Command.make("validate", {
|
|
|
93
92
|
};
|
|
94
93
|
yield* Console.error(summary(counts, displayRoot(cwd, bundleRoot, path)));
|
|
95
94
|
}
|
|
96
|
-
|
|
95
|
+
yield* CliExit.set(code);
|
|
97
96
|
}).pipe(provideConfig({
|
|
98
97
|
explicitConfigPath: input.config,
|
|
99
98
|
discoveryCwd
|