@okfit/cli 0.7.0 → 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
@@ -9,7 +9,7 @@ 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
14
  okfit verify [<id>] [path] [--all] [--type <Type>]... [--stable|--draft] [--config <file>] [--at <iso>] [--dry-run] [--format human|json] [--help]
15
15
  okfit query list [path] [--type <Type>]... [--tag <tag>]... [--status draft|stable|deprecated]... [--verified|--unverified] [--config <file>] [--format human|json]
@@ -164,6 +164,21 @@ config's `bundle.profile`, itself defaulting to `software-project`); an
164
164
  unrecognised name is a warning, not a failure — `init` continues with the
165
165
  default profile.
166
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
+
167
182
  ### `okfit context`
168
183
 
169
184
  Prints the resolved project root, bundle root, config path, profile, and
@@ -238,6 +253,15 @@ already verified. A batch skips a concept and reports why: `draft`,
238
253
  `deprecated`, or `already-verified`. A batch never re-attests a
239
254
  `deprecated` concept; verifying one by id is still allowed.
240
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
+
241
265
  This is a human-run command: it records **your** attestation that you
242
266
  reviewed the concept, so no agent, hook, or MCP tool ever invokes it.
243
267
 
@@ -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
package/commands/query.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 { humanQueryGet, humanQueryList, humanQueryNeighbors, queryListSummary } from "../render/query.js";
@@ -47,7 +46,6 @@ const run$1 = (input, use, precheck) => Effect.gen(function* () {
47
46
  root: displayRoot(cwd, resolved.bundleRoot, path),
48
47
  distribution
49
48
  });
50
- setExitCode(0);
51
49
  }).pipe(provideConfig({
52
50
  explicitConfigPath: input.config,
53
51
  discoveryCwd
package/commands/root.js CHANGED
@@ -7,6 +7,7 @@ import { staleCommand } from "./stale.js";
7
7
  import { syncCommand } from "./sync.js";
8
8
  import { validateCommand } from "./validate.js";
9
9
  import { verifyCommand } from "./verify.js";
10
+ import { CliAudience } from "@effected/cli";
10
11
  import { Command } from "effect/cli";
11
12
 
12
13
  //#region src/commands/root.ts
@@ -29,7 +30,7 @@ import { Command } from "effect/cli";
29
30
  *
30
31
  * @public
31
32
  */
32
- 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([
33
34
  validateCommand,
34
35
  initCommand,
35
36
  contextCommand,
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
@@ -0,0 +1,75 @@
1
+ import { Cancelled } from "@effected/cli";
2
+ import { loadPickerCandidates } from "@okfit/engine";
3
+ import { Console, Effect } from "effect";
4
+ import { CliUi, Confirm, MultiSelect } from "@effected/cli/ui";
5
+
6
+ //#region src/commands/verify-picker.ts
7
+ /** One compact row: id, status and how many other actors already attested it. */
8
+ const pickerLabel = (candidate) => candidate.otherAttestations === 0 ? `${candidate.id} ${candidate.status}` : `${candidate.id} ${candidate.status} ${candidate.otherAttestations} other attestation(s)`;
9
+ const detailOf = (candidate) => {
10
+ const { title, description } = candidate;
11
+ if (title !== null && description !== null) return `${title} — ${description}`;
12
+ return title ?? description ?? void 0;
13
+ };
14
+ /** Candidates grouped into one section per type, in candidate order. */
15
+ const pickerSections = (candidates) => {
16
+ const byType = /* @__PURE__ */ new Map();
17
+ for (const candidate of candidates) {
18
+ const rows = byType.get(candidate.type);
19
+ if (rows === void 0) byType.set(candidate.type, [candidate]);
20
+ else rows.push(candidate);
21
+ }
22
+ return [...byType].map(([type, rows]) => ({
23
+ title: type,
24
+ items: rows.map((candidate) => {
25
+ const detail = detailOf(candidate);
26
+ return {
27
+ key: candidate.id,
28
+ label: pickerLabel(candidate),
29
+ value: candidate.id,
30
+ ...detail === void 0 ? {} : { detail }
31
+ };
32
+ })
33
+ }));
34
+ };
35
+ /**
36
+ * Bare `okfit verify` on a terminal: list what `by` has not attested, let the
37
+ * person pick, confirm. Resolves to `undefined` (after printing why) when there
38
+ * is nothing to do; fails `Cancelled` on Esc, `q`, Ctrl-C or a "no" answer, and
39
+ * `NotInteractive` when no screen can mount. Writes nothing itself.
40
+ */
41
+ const pickConcepts = (options) => Effect.gen(function* () {
42
+ const { by, candidates } = yield* loadPickerCandidates(options);
43
+ if (candidates.length === 0) {
44
+ yield* Console.log(`nothing to verify: every require_verified concept is already attested by ${by}`);
45
+ return;
46
+ }
47
+ const ids = yield* CliUi.prompt(MultiSelect.screen({
48
+ message: "Attest which concepts?",
49
+ sections: pickerSections(candidates)
50
+ }));
51
+ if (ids.length === 0) {
52
+ yield* Console.log("nothing selected; nothing written");
53
+ return;
54
+ }
55
+ const chosen = new Set(ids);
56
+ const drafts = candidates.filter((candidate) => chosen.has(candidate.id) && candidate.status === "draft").length;
57
+ const answer = yield* CliUi.prompt(Confirm.screen({
58
+ message: `Attest ${ids.length} concept(s) as ${by}?`,
59
+ initial: true,
60
+ toggles: drafts > 0 ? [{
61
+ key: "promote",
62
+ label: `promote ${drafts} draft(s) to stable`,
63
+ value: true
64
+ }] : []
65
+ }));
66
+ if (!answer.confirmed) return yield* new Cancelled({ reason: "escape" });
67
+ return {
68
+ by,
69
+ ids,
70
+ promote: answer.toggles.promote ?? false
71
+ };
72
+ });
73
+
74
+ //#endregion
75
+ export { pickConcepts, pickerLabel, pickerSections };
@@ -1,10 +1,11 @@
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 { humanVerify, humanVerifyBatch } from "../render/verify.js";
4
+ import { pickConcepts } from "./verify-picker.js";
5
+ import { CliInteractive } from "@effected/cli";
5
6
  import { Argument, Command, Flag } from "effect/cli";
6
7
  import { CurrentDistribution } from "@effected/engine";
7
- import { Now, VerifyBatchEnvelope, VerifyEnvelope, VerifySelectionError, jsonError, provideConfig, resolveProjectConfig, runVerify, runVerifyBatch, verifyBatchEnvelope, verifyEnvelope } from "@okfit/engine";
8
+ import { Now, VerifyBatchEnvelope, VerifyEnvelope, VerifySelectionError, jsonError, provideConfig, resolveProjectConfig, runVerify, runVerifyBatch, runVerifyIds, verifyBatchEnvelope, verifyEnvelope } from "@okfit/engine";
8
9
  import { Console, DateTime, Effect, Option, Path, Schema } from "effect";
9
10
  import { Timestamp } from "@okfit/core";
10
11
  import { Git } from "@effected/git";
@@ -75,10 +76,35 @@ const verifyCommand = Command.make("verify", {
75
76
  });
76
77
  const at = Option.isNone(input.at) ? DateTime.startOf(yield* Now, "second") : yield* Schema.decodeUnknownEffect(Timestamp)(input.at.value);
77
78
  if (Option.isSome(input.id) && Option.isSome(input.path) && batch) return yield* new VerifySelectionError({ reason: "id-and-batch" });
78
- if (Option.isNone(input.id) && !batch) return yield* new VerifySelectionError({ reason: "no-selection" });
79
79
  if (input.stable && input.draft) return yield* new VerifySelectionError({ reason: "status-conflict" });
80
80
  const status = input.stable ? "stable" : input.draft ? "draft" : void 0;
81
- if (status !== void 0 && batch) return yield* new VerifySelectionError({ reason: "status-and-batch" });
81
+ if (status !== void 0 && (batch || Option.isNone(input.id))) return yield* new VerifySelectionError({ reason: "status-and-batch" });
82
+ if (Option.isNone(input.id) && !batch) {
83
+ if (!(yield* CliInteractive)) return yield* new VerifySelectionError({ reason: "no-selection" });
84
+ const picked = yield* pickConcepts({
85
+ bundleRoot: resolved.bundleRoot,
86
+ projectRoot: resolved.projectRoot,
87
+ config: resolved.config
88
+ }).pipe(Effect.catchTag("NotInteractive", () => Effect.fail(new VerifySelectionError({ reason: "no-selection" }))));
89
+ if (picked === void 0) return;
90
+ const result = yield* runVerifyIds({
91
+ bundleRoot: resolved.bundleRoot,
92
+ projectRoot: resolved.projectRoot,
93
+ config: resolved.config,
94
+ at,
95
+ dryRun: input.dryRun,
96
+ ids: picked.ids,
97
+ promote: picked.promote
98
+ });
99
+ for (const line of humanVerifyBatch({
100
+ by: result.by,
101
+ at: result.at,
102
+ dryRun: result.dryRun,
103
+ verified: result.verified,
104
+ skipped: result.skipped
105
+ })) yield* Console.log(line);
106
+ return;
107
+ }
82
108
  if (batch) {
83
109
  const result = yield* runVerifyBatch({
84
110
  bundleRoot: resolved.bundleRoot,
@@ -110,7 +136,6 @@ const verifyCommand = Command.make("verify", {
110
136
  verified: result.verified,
111
137
  skipped: result.skipped
112
138
  })) yield* Console.log(line);
113
- setExitCode(0);
114
139
  return;
115
140
  }
116
141
  if (Option.isNone(input.id)) return yield* new VerifySelectionError({ reason: "no-selection" });
@@ -146,11 +171,10 @@ const verifyCommand = Command.make("verify", {
146
171
  status: result.status,
147
172
  statusFragment: result.statusFragment
148
173
  })) yield* Console.log(line);
149
- setExitCode(0);
150
174
  }).pipe(Effect.provide(Git.layer), provideConfig({
151
175
  explicitConfigPath: input.config,
152
176
  discoveryCwd
153
- }));
177
+ }), CliInteractive.unless(input.format === "json"));
154
178
  if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
155
179
  return yield* body;
156
180
  })).pipe(Command.withDescription("Record a human's attestation that a concept has been reviewed: append one verified entry, { by: human:<id>, at: <now> }, to its frontmatter. The actor is always your own git identity; there is no --by. Give a concept id, or --all/--type <Type> (repeatable) to attest a batch of unverified concepts at once; exactly one of the two selections is required. With a concept id, --stable or --draft also sets its status in the same write. This is a human-run command: no agent, hook, or MCP tool ever invokes it."));
package/errors.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { InitBundleDirError } from "./internal/initWizard.js";
1
2
  import { DocumentStdinIsTerminalError } from "./internal/stdin.js";
3
+ import { Cancelled, ConfigIssueRenderer, NotInteractive } from "@effected/cli";
2
4
  import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
3
- import { ConfigIssueRenderer } from "@effected/cli";
4
5
 
5
6
  //#region src/errors.ts
6
7
  const hasTag = (error, tag) => typeof error === "object" && error !== null && "_tag" in error && error._tag === tag;
@@ -38,6 +39,8 @@ const relativeToCwd = (path, cwd) => {
38
39
  * type-only import — this is the "path unknown" case, K-46) renders as
39
40
  * `error: ${String(error)}` followed by one two-space-indented
40
41
  * `ConfigIssueRenderer.render(error)` line per entry.
42
+ * 4a. `InitBundleDirError` (`okfit init --bundle`, exit 64) renders as one
43
+ * `error: <message>` line.
41
44
  * 5. `VerifyConceptNotFoundError` and `VerifyUnsupportedFrontmatterError`
42
45
  * each render as their own `error: <message>` line; both messages
43
46
  * already name the concept id and what to do about it, and neither
@@ -64,6 +67,7 @@ const relativeToCwd = (path, cwd) => {
64
67
  */
65
68
  const renderFailure = (error) => {
66
69
  if (hasTag(error, "ShowHelp")) return [];
70
+ if (error instanceof Cancelled || error instanceof NotInteractive) return [error.message];
67
71
  if (error instanceof ConfigPathNotFoundError) return [`error: ${error.message}`];
68
72
  if (error instanceof ConfigMalformedError) return [`error: ${error.message}`];
69
73
  if (error instanceof InitOverwriteError) return [
@@ -71,6 +75,7 @@ const renderFailure = (error) => {
71
75
  ...error.paths.map((path) => ` ${relativeToCwd(path, error.cwd)}`),
72
76
  "Nothing was written."
73
77
  ];
78
+ if (error instanceof InitBundleDirError) return [`error: ${error.message}`];
74
79
  if (error instanceof VerifyConceptNotFoundError) return [`error: ${error.message}`];
75
80
  if (error instanceof VerifyUnsupportedFrontmatterError) return [`error: ${error.message}`];
76
81
  if (error instanceof SyncStagedLogError) return [`error: ${error.message}`];
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Command } from "effect/cli";
2
+ import "@effected/cli";
2
3
  import { Runtime, Schema } from "effect";
3
4
  import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
4
5
  //#region src/commands/root.d.ts
@@ -21,7 +22,22 @@ import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
21
22
  *
22
23
  * @public
23
24
  */
24
- 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("@okfit/engine").DocumentPathError | DocumentStdinIsTerminalError | 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("@okfit/engine").QueryConceptNotFoundError | import("@okfit/engine").QuerySelectionError | import("@okfit/engine").QueryUnknownVocabularyError | import("effect/Schema").SchemaError | import("@okfit/engine").SyncStagedLogError | import("@effected/git").UnknownRefError | import("@okfit/engine").VerifyConceptNotFoundError | import("@okfit/engine").VerifySelectionError | import("@okfit/engine").VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError, import("@effected/xdg").AppDirs | import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("effect/Stdio").Stdio | import("@effected/xdg").Xdg>;
25
+ export declare const rootCommand: Command.Command<"okfit", {
26
+ readonly audience: readonly import("@effected/env").AudienceKind[];
27
+ readonly human: readonly boolean[];
28
+ readonly agent: readonly boolean[];
29
+ readonly ci: readonly boolean[];
30
+ } | {
31
+ readonly audience: readonly import("@effected/env").AudienceKind[];
32
+ readonly human: readonly boolean[];
33
+ readonly agent: readonly boolean[];
34
+ readonly ci: readonly boolean[];
35
+ }, {
36
+ readonly audience: readonly import("@effected/env").AudienceKind[];
37
+ readonly human: readonly boolean[];
38
+ readonly agent: readonly boolean[];
39
+ readonly ci: readonly boolean[];
40
+ }, import("@okfit/profiles").AgentActorUnconfiguredError | import("@okfit/core").BundleDepthExceededError | import("@okfit/core").BundleReadError | import("@okfit/core").BundleRootNotFoundError | import("@effected/cli").Cancelled | 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("@okfit/engine").DocumentPathError | DocumentStdinIsTerminalError | import("@effected/markdown").FrontmatterEncodeError | import("@effected/markdown").FrontmatterFormatMismatchError | import("@effected/markdown").FrontmatterValidationError | import("@effected/git").GitCommandError | import("@okfit/profiles").GitHistoryError | import("@okfit/profiles").HumanActorUnresolvedError | InitBundleDirError | import("@okfit/engine").InitOverwriteError | import("@effected/markdown").MarkdownParseError | import("@effected/git").NotARepositoryError | import("@effected/cli").NotInteractive | import("effect/PlatformError").PlatformError | import("@okfit/engine").QueryConceptNotFoundError | import("@okfit/engine").QuerySelectionError | import("@okfit/engine").QueryUnknownVocabularyError | import("effect/Schema").SchemaError | import("@okfit/engine").SyncStagedLogError | import("@effected/git").UnknownRefError | import("@okfit/engine").VerifyConceptNotFoundError | import("@okfit/engine").VerifySelectionError | import("@okfit/engine").VerifyUnsupportedFrontmatterError | import("@effected/yaml").YamlParseError, import("@effected/xdg").AppDirs | import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("@effected/cli").CliExit | import("@effected/cli").CliTheme | import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("@okfit/engine").Now | import("effect/Path").Path | import("effect/Stdio").Stdio | import("@effected/xdg").Xdg>;
25
41
  //#endregion
26
42
  //#region src/errors.d.ts
27
43
  /**
@@ -52,6 +68,8 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
52
68
  * type-only import — this is the "path unknown" case, K-46) renders as
53
69
  * `error: ${String(error)}` followed by one two-space-indented
54
70
  * `ConfigIssueRenderer.render(error)` line per entry.
71
+ * 4a. `InitBundleDirError` (`okfit init --bundle`, exit 64) renders as one
72
+ * `error: <message>` line.
55
73
  * 5. `VerifyConceptNotFoundError` and `VerifyUnsupportedFrontmatterError`
56
74
  * each render as their own `error: <message>` line; both messages
57
75
  * already name the concept id and what to do about it, and neither
@@ -78,6 +96,23 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
78
96
  */
79
97
  export declare const renderFailure: (error: unknown) => ReadonlyArray<string>;
80
98
  //#endregion
99
+ //#region src/internal/initWizard.d.ts
100
+ declare const InitBundleDirError_base: Schema.Class<InitBundleDirError, Schema.TaggedStruct<"InitBundleDirError", {
101
+ readonly value: Schema.String;
102
+ readonly reason: Schema.String;
103
+ }>, import("effect/Cause").YieldableError>;
104
+ /**
105
+ * `okfit init --bundle <dir>` named a directory that is not a usable
106
+ * project-relative path: empty, absolute, the project root itself, or
107
+ * escaping it. A usage error (exit 64).
108
+ *
109
+ * @public
110
+ */
111
+ export declare class InitBundleDirError extends InitBundleDirError_base {
112
+ readonly [Runtime.errorExitCode] = 64;
113
+ get message(): string;
114
+ }
115
+ //#endregion
81
116
  //#region src/internal/stdin.d.ts
82
117
  declare const DocumentStdinIsTerminalError_base: Schema.Class<DocumentStdinIsTerminalError, Schema.TaggedStruct<"DocumentStdinIsTerminalError", {
83
118
  readonly path: Schema.String;
@@ -116,19 +151,27 @@ interface Counts {
116
151
  readonly info: number;
117
152
  readonly concepts: number;
118
153
  }
154
+ /**
155
+ * Paints one severity word. `CliTheme`'s `paint` fits as-is: severities are
156
+ * a subset of its token names, and it is the identity when colour is `none`
157
+ * (#217 retired this module's hand-rolled ANSI table onto the theme).
158
+ *
159
+ * @public
160
+ */
161
+ type SeverityPaint = (severity: RenderedDiagnostic["severity"], text: string) => string;
119
162
  /**
120
163
  * K-16. With a range:
121
164
  * `<file>:<range.line + 1>:<range.character + 1> <severity> <code> <message>`.
122
165
  * Without one: `<file> <severity> <code> <message>`. `file: ""` renders as
123
166
  * the literal `(bundle)`. Core's range is zero-based (D-32,
124
167
  * `CORE/Diagnostic.ts:49-57`); the `+ 1`s here are the only place it becomes
125
- * one-based. Colour, when `color` is `true`, wraps ONLY the severity word
168
+ * one-based. Colour, when a `paint` is given, wraps ONLY the severity word
126
169
  * (K-19) — never the code, the path, or the message.
127
170
  *
128
171
  * @public
129
172
  */
130
173
  export declare const line: (diagnostic: RenderedDiagnostic, options?: {
131
- readonly color?: boolean;
174
+ readonly paint?: SeverityPaint;
132
175
  }) => string;
133
176
  /**
134
177
  * `sort` then `line` over the whole set: the exact stdout body of
@@ -137,7 +180,7 @@ export declare const line: (diagnostic: RenderedDiagnostic, options?: {
137
180
  * @public
138
181
  */
139
182
  export declare const human: (diagnostics: ReadonlyArray<RenderedDiagnostic>, options?: {
140
- readonly color?: boolean;
183
+ readonly paint?: SeverityPaint;
141
184
  }) => ReadonlyArray<string>;
142
185
  /**
143
186
  * K-20, verbatim and unpluralised —
@@ -196,5 +239,5 @@ export declare const humanVerify: (input: VerifyLines) => ReadonlyArray<string>;
196
239
  */
197
240
  export declare const CLI_VERSION: string;
198
241
  //#endregion
199
- export type { Counts, VerifyLines };
242
+ export type { Counts, SeverityPaint, VerifyLines };
200
243
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { humanContext } from "./render/context.js";
2
2
  import { CLI_VERSION } from "./version.js";
3
+ import { InitBundleDirError } from "./internal/initWizard.js";
3
4
  import { human, line, summary } from "./render/human.js";
4
5
  import { DocumentStdinIsTerminalError } from "./internal/stdin.js";
5
6
  import { humanVerify } from "./render/verify.js";
6
7
  import { rootCommand } from "./commands/root.js";
7
8
  import { renderFailure } from "./errors.js";
8
9
 
9
- export { CLI_VERSION, DocumentStdinIsTerminalError, human, humanContext, humanVerify, line, renderFailure, rootCommand, summary };
10
+ export { CLI_VERSION, DocumentStdinIsTerminalError, InitBundleDirError, human, humanContext, humanVerify, line, renderFailure, rootCommand, summary };
@@ -0,0 +1,121 @@
1
+ import { Effect, Option, Runtime, Schema } from "effect";
2
+ import { PROFILE_NAMES } from "@okfit/profiles";
3
+ import { posix } from "node:path";
4
+ import { CliUi, Select, TextInput } from "@effected/cli/ui";
5
+
6
+ //#region src/internal/initWizard.ts
7
+ /**
8
+ * The three project-level config names `okfit` discovers (C-1), in the order
9
+ * the wizard offers them; the first is the default (`CONFIG_RELATIVE_PATH`).
10
+ *
11
+ * @public
12
+ */
13
+ const INIT_CONFIG_LOCATIONS = [
14
+ ".config/okfit.toml",
15
+ "okfit.toml",
16
+ ".okfit.toml"
17
+ ];
18
+ /**
19
+ * `okfit init --bundle <dir>` named a directory that is not a usable
20
+ * project-relative path: empty, absolute, the project root itself, or
21
+ * escaping it. A usage error (exit 64).
22
+ *
23
+ * @public
24
+ */
25
+ var InitBundleDirError = class extends Schema.TaggedError()("InitBundleDirError", {
26
+ value: Schema.String,
27
+ reason: Schema.String
28
+ }) {
29
+ [Runtime.errorExitCode] = 64;
30
+ get message() {
31
+ return `invalid bundle directory "${this.value}": ${this.reason}`;
32
+ }
33
+ };
34
+ /**
35
+ * Validate and normalise a project-relative bundle directory. `Right`-like
36
+ * result: the normalised directory (`./docs//kb/` becomes `docs/kb`), or the
37
+ * reason it is unusable.
38
+ */
39
+ const checkBundleDir = (value) => {
40
+ const trimmed = value.trim();
41
+ if (trimmed === "") return {
42
+ ok: false,
43
+ reason: "must not be empty"
44
+ };
45
+ if (posix.isAbsolute(trimmed) || /^[A-Za-z]:[\\/]/.test(trimmed)) return {
46
+ ok: false,
47
+ reason: "must be relative to the project root"
48
+ };
49
+ const normal = posix.normalize(trimmed.replaceAll("\\", "/")).replace(/\/+$/, "");
50
+ if (normal === "" || normal === ".") return {
51
+ ok: false,
52
+ reason: "must name a subdirectory of the project root"
53
+ };
54
+ if (normal === ".." || normal.startsWith("../")) return {
55
+ ok: false,
56
+ reason: "must not escape the project root"
57
+ };
58
+ return {
59
+ ok: true,
60
+ dir: normal
61
+ };
62
+ };
63
+ /**
64
+ * The `okfit init` wizard: profile, bundle directory, config location, in
65
+ * that order. A setting is prompted for only when its flag was not given and
66
+ * the run is interactive; otherwise its flag value or default is used
67
+ * (`CliUi.prompt`'s `otherwise`, so a non-interactive run never loads Ink).
68
+ *
69
+ * A bad `--bundle` flag fails with {@link InitBundleDirError} before any
70
+ * screen mounts. In the prompt, a bad entry is rejected inline by the text
71
+ * input and the question stays open. Esc, `q` or Ctrl-C on any screen fails
72
+ * with the kit's `Cancelled`; the caller performs every write only after this
73
+ * returns, so a cancel writes nothing.
74
+ *
75
+ * Answered screens stay in scrollback (no `clear`): three short screens read
76
+ * as a record of the answers, and erasing them leaves the terminal looking
77
+ * as if nothing had been asked.
78
+ */
79
+ const initWizard = (given, defaults) => Effect.gen(function* () {
80
+ let flagBundle;
81
+ if (Option.isSome(given.bundle)) {
82
+ const checked = checkBundleDir(given.bundle.value);
83
+ if (!checked.ok) return yield* new InitBundleDirError({
84
+ value: given.bundle.value,
85
+ reason: checked.reason
86
+ });
87
+ flagBundle = checked.dir;
88
+ }
89
+ return {
90
+ profile: Option.isSome(given.profile) ? given.profile.value : yield* CliUi.prompt(Select.screen({
91
+ message: "Profile to scaffold with",
92
+ choices: PROFILE_NAMES.map((name) => ({
93
+ label: name,
94
+ value: name
95
+ })),
96
+ initial: Math.max(0, PROFILE_NAMES.indexOf(defaults.profile))
97
+ }), { otherwise: defaults.profile }),
98
+ bundle: flagBundle ?? (yield* CliUi.prompt(TextInput.screen({
99
+ message: "Bundle directory (relative to the project root)",
100
+ initial: defaults.bundle,
101
+ validate: (entered) => {
102
+ const checked = checkBundleDir(entered);
103
+ return checked.ok ? void 0 : checked.reason;
104
+ }
105
+ }), { otherwise: defaults.bundle }).pipe(Effect.map((entered) => {
106
+ const checked = checkBundleDir(entered);
107
+ return checked.ok ? checked.dir : defaults.bundle;
108
+ }))),
109
+ location: Option.isSome(given.location) ? given.location.value : yield* CliUi.prompt(Select.screen({
110
+ message: "Config file location",
111
+ choices: INIT_CONFIG_LOCATIONS.map((name) => ({
112
+ label: name,
113
+ value: name
114
+ })),
115
+ initial: INIT_CONFIG_LOCATIONS.indexOf(defaults.location)
116
+ }), { otherwise: defaults.location })
117
+ };
118
+ });
119
+
120
+ //#endregion
121
+ export { INIT_CONFIG_LOCATIONS, InitBundleDirError, checkBundleDir, initWizard };
@@ -1,8 +1,8 @@
1
+ import { CliColor } from "@effected/cli";
1
2
  import { CurrentDistribution, distributionSuffix } from "@effected/engine";
2
3
  import { ENGINE_VERSION } from "@okfit/engine";
3
4
  import { Effect, Layer } from "effect";
4
5
  import { CONFIG_SCHEMA_VERSION, OKF_SPEC_VERSION } from "@okfit/core";
5
- import { CliColor } from "@effected/cli";
6
6
 
7
7
  //#region src/internal/versionFormatter.ts
8
8
  /**
package/main.js CHANGED
@@ -2,11 +2,10 @@ import { CLI_VERSION } from "./version.js";
2
2
  import { rootCommand } from "./commands/root.js";
3
3
  import { renderFailure } from "./errors.js";
4
4
  import { versionFormatterLayer } from "./internal/versionFormatter.js";
5
- import { Command } from "effect/cli";
5
+ import { CliAudience, CliRuntime } from "@effected/cli";
6
6
  import { CurrentDistribution } from "@effected/engine";
7
7
  import { Now, OkfitPlatform } from "@okfit/engine";
8
8
  import { DateTime, Effect, Option } from "effect";
9
- import { CliRuntime } from "@effected/cli";
10
9
  import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
11
10
 
12
11
  //#region src/main.ts
@@ -46,12 +45,13 @@ const main = (options = {}) => {
46
45
  const distribution = Option.fromNullishOr(options.distribution);
47
46
  const program = Effect.gen(function* () {
48
47
  const now = yield* nowEffect;
49
- return yield* Command.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(Now, now));
48
+ return yield* CliAudience.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(Now, now));
50
49
  }).pipe(Effect.provide(versionFormatterLayer), Effect.provideService(CurrentDistribution, distribution));
51
50
  NodeRuntime.runMain(CliRuntime.main(program, {
52
51
  platform: OkfitPlatform,
53
52
  exitCode: 3,
54
- render: renderFailure
53
+ render: renderFailure,
54
+ env: { audienceEnvVar: "OKFIT_AUDIENCE" }
55
55
  }));
56
56
  };
57
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okfit/cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "private": false,
5
5
  "description": "The okfit command line: validate, lint, index, and inspect Open Knowledge Format (OKF) bundles.",
6
6
  "keywords": [
@@ -42,22 +42,25 @@
42
42
  "okfit": "bin/okfit.js"
43
43
  },
44
44
  "dependencies": {
45
- "@effect/platform-node": "4.0.0-rc.118",
46
- "@effected/cli": "^0.10.0",
47
- "@effected/config-file": "^0.13.1",
48
- "@effected/engine": "^0.2.0",
49
- "@effected/git": "^0.19.0",
50
- "@effected/glob": "^0.9.0",
51
- "@effected/jsonc": "^0.14.0",
52
- "@effected/markdown": "^0.14.0",
53
- "@effected/schemastore": "^0.17.0",
54
- "@effected/toml": "^0.10.0",
55
- "@effected/walker": "^0.14.1",
56
- "@effected/yaml": "^0.18.0",
57
- "@okfit/core": "0.8.3",
58
- "@okfit/engine": "0.10.0",
59
- "@okfit/profiles": "0.8.2",
60
- "effect": "4.0.0-rc.118"
45
+ "@effect/platform-node": "^4.0.0",
46
+ "@effected/cli": "^0.11.0",
47
+ "@effected/config-file": "^0.14.0",
48
+ "@effected/engine": "^0.3.0",
49
+ "@effected/env": "^0.1.0",
50
+ "@effected/git": "^0.20.0",
51
+ "@effected/glob": "^0.10.0",
52
+ "@effected/jsonc": "^0.15.0",
53
+ "@effected/markdown": "^0.15.0",
54
+ "@effected/schemastore": "^0.18.0",
55
+ "@effected/toml": "^0.11.0",
56
+ "@effected/walker": "^0.15.0",
57
+ "@effected/yaml": "^0.19.0",
58
+ "@okfit/core": "0.8.4",
59
+ "@okfit/engine": "0.11.0",
60
+ "@okfit/profiles": "0.8.3",
61
+ "effect": "^4.0.0",
62
+ "ink": "^7.1.1",
63
+ "react": "^19.2.0"
61
64
  },
62
65
  "engines": {
63
66
  "node": ">=24.11.0"
package/render/human.js CHANGED
@@ -1,31 +1,20 @@
1
1
  import { sort } from "@okfit/engine";
2
2
 
3
3
  //#region src/render/human.ts
4
- /** The ANSI escape character, built from its code point so the source never carries a raw control byte. */
5
- const ESC = String.fromCharCode(27);
6
- /** ANSI SGR codes for the severity word only (K-19); reset after, never applied elsewhere. */
7
- const SEVERITY_COLOR = {
8
- error: `${ESC}[31m`,
9
- warning: `${ESC}[33m`,
10
- info: `${ESC}[36m`
11
- };
12
- const RESET = `${ESC}[0m`;
13
- const colorize = (severity, color) => color ? `${SEVERITY_COLOR[severity]}${severity}${RESET}` : severity;
14
4
  /**
15
5
  * K-16. With a range:
16
6
  * `<file>:<range.line + 1>:<range.character + 1> <severity> <code> <message>`.
17
7
  * Without one: `<file> <severity> <code> <message>`. `file: ""` renders as
18
8
  * the literal `(bundle)`. Core's range is zero-based (D-32,
19
9
  * `CORE/Diagnostic.ts:49-57`); the `+ 1`s here are the only place it becomes
20
- * one-based. Colour, when `color` is `true`, wraps ONLY the severity word
10
+ * one-based. Colour, when a `paint` is given, wraps ONLY the severity word
21
11
  * (K-19) — never the code, the path, or the message.
22
12
  *
23
13
  * @public
24
14
  */
25
15
  const line = (diagnostic, options) => {
26
- const color = options?.color ?? false;
27
16
  const file = diagnostic.file === "" ? "(bundle)" : diagnostic.file;
28
- const severity = colorize(diagnostic.severity, color);
17
+ const severity = options?.paint === void 0 ? diagnostic.severity : options.paint(diagnostic.severity, diagnostic.severity);
29
18
  return `${diagnostic.range === void 0 ? file : `${file}:${diagnostic.range.line + 1}:${diagnostic.range.character + 1}`} ${severity} ${diagnostic.code} ${diagnostic.message}`;
30
19
  };
31
20
  /**
package/version.js CHANGED
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @public
13
13
  */
14
- const CLI_VERSION = "0.7.0";
14
+ const CLI_VERSION = "0.8.0";
15
15
 
16
16
  //#endregion
17
17
  export { CLI_VERSION };
package/internal/exit.js DELETED
@@ -1,14 +0,0 @@
1
- //#region src/internal/exit.ts
2
- /**
3
- * K-7: exits `1` and `2` are a SUCCESSFUL run that found diagnostics, never
4
- * an Effect failure. The handler writes the code here and returns `void`.
5
- * The only writer of `process.exitCode` in this package.
6
- *
7
- * @public
8
- */
9
- const setExitCode = (code) => {
10
- process.exitCode = code;
11
- };
12
-
13
- //#endregion
14
- export { setExitCode };