@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 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
 
@@ -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 { setExitCode } from "../internal/exit.js";
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>] [--config <file>]` (K-2, K-3; no
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). `Profiles.get(profileName)`.
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, `setExitCode` to its
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
- const initCommand = Command.make("init", {
92
- path: pathArg,
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 profileName = Option.getOrElse(input.profile, () => fileConfig.bundle?.profile ?? DEFAULT_PROFILE_NAME);
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 merged = OkfitConfig.merge(OkfitConfig.merge(OkfitConfig.DEFAULTS, profileConfig), fileConfig);
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
- for (const target of paths) yield* fs.makeDirectory(path.dirname(target), { recursive: true });
147
- const bundlePath = merged.bundle?.path ?? DEFAULT_BUNDLE_PATH;
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}/${CONFIG_RELATIVE_PATH}`, { header: SCHEMA_DIRECTIVE });
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 color = yield* CliColor.enabled;
163
- for (const line of human(diagnostics, { color })) yield* Console.log(line);
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
- setExitCode(forDiagnostics(diagnostics));
190
+ yield* CliExit.set(forDiagnostics(diagnostics));
166
191
  }));
167
- })).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."));
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 color = yield* CliColor.enabled;
80
- for (const diagnosticLine of human(diagnostics, { color })) yield* Console.log(diagnosticLine);
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
- setExitCode(code);
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`, and
21
- * `stale` are the whole command tree; nothing else is registered here.
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
@@ -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 color = yield* CliColor.enabled;
87
- for (const diagnosticLine of human(diagnostics, { color })) yield* Console.log(diagnosticLine);
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
- setExitCode(code);
95
+ yield* CliExit.set(code);
97
96
  }).pipe(provideConfig({
98
97
  explicitConfigPath: input.config,
99
98
  discoveryCwd