@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.
@@ -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";
@@ -20,6 +21,10 @@ const idArg = Argument.String("id").pipe(Argument.optional, Argument.withDescrip
20
21
  const allFlag = Flag.Boolean("all").pipe(Flag.withDefault(false), Flag.withDescription("verify every concept whose type sets require_verified and that you have not verified yet; drafts are skipped"));
21
22
  /** Issue #138: narrow (or, without --all, define) the batch to these types. */
22
23
  const typeFlag = Flag.String("type").pipe(Flag.atLeast(0), Flag.withDescription("verify every unverified concept of this type (repeatable); implies --all's selection rule for the named types"));
24
+ /** Issue #185: settle the concept in the same write as the attestation. */
25
+ const stableFlag = Flag.Boolean("stable").pipe(Flag.withDefault(false), Flag.withDescription("also set status: stable in the same write; needs a concept id"));
26
+ /** Issue #185: record a review without settling. */
27
+ const draftFlag = Flag.Boolean("draft").pipe(Flag.withDefault(false), Flag.withDescription("also set (or keep) status: draft in the same write; needs a concept id"));
23
28
  /** K-2: `[path]` is the PROJECT root, byte-identical to validate/init/context's. */
24
29
  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"));
25
30
  /** K-1: no `mustExist`; the handler stats it via `provideConfig`. */
@@ -30,8 +35,8 @@ const atFlag = Flag.String("at").pipe(Flag.optional, Flag.withDescription("ISO 8
30
35
  const dryRunFlag = Flag.Boolean("dry-run").pipe(Flag.withDefault(false), Flag.withDescription("print the exact fragment a real run would splice in; write nothing"));
31
36
  const formatFlag = Flag.Literals("format", ["human", "json"]).pipe(Flag.withDefault("human"), Flag.withDescription("output format: human (default) or json"));
32
37
  /**
33
- * `okfit verify [<id>] [path] [--all] [--type <Type>]... [--config <file>]
34
- * [--at <iso>] [--dry-run] [--format human|json]`.
38
+ * `okfit verify [<id>] [path] [--all] [--type <Type>]... [--stable|--draft]
39
+ * [--config <file>] [--at <iso>] [--dry-run] [--format human|json]`.
35
40
  *
36
41
  * Handler order fixed by contract §2.3. Steps 1–3 are `context`'s handler
37
42
  * in substance — stat `--config` (K-1) via `provideConfig`, discover,
@@ -51,6 +56,8 @@ const verifyCommand = Command.make("verify", {
51
56
  config: configFlag,
52
57
  all: allFlag,
53
58
  type: typeFlag,
59
+ stable: stableFlag,
60
+ draft: draftFlag,
54
61
  at: atFlag,
55
62
  dryRun: dryRunFlag,
56
63
  format: formatFlag
@@ -69,7 +76,35 @@ const verifyCommand = Command.make("verify", {
69
76
  });
70
77
  const at = Option.isNone(input.at) ? DateTime.startOf(yield* Now, "second") : yield* Schema.decodeUnknownEffect(Timestamp)(input.at.value);
71
78
  if (Option.isSome(input.id) && Option.isSome(input.path) && batch) return yield* new VerifySelectionError({ reason: "id-and-batch" });
72
- if (Option.isNone(input.id) && !batch) return yield* new VerifySelectionError({ reason: "no-selection" });
79
+ if (input.stable && input.draft) return yield* new VerifySelectionError({ reason: "status-conflict" });
80
+ const status = input.stable ? "stable" : input.draft ? "draft" : void 0;
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
+ }
73
108
  if (batch) {
74
109
  const result = yield* runVerifyBatch({
75
110
  bundleRoot: resolved.bundleRoot,
@@ -101,7 +136,6 @@ const verifyCommand = Command.make("verify", {
101
136
  verified: result.verified,
102
137
  skipped: result.skipped
103
138
  })) yield* Console.log(line);
104
- setExitCode(0);
105
139
  return;
106
140
  }
107
141
  if (Option.isNone(input.id)) return yield* new VerifySelectionError({ reason: "no-selection" });
@@ -111,7 +145,8 @@ const verifyCommand = Command.make("verify", {
111
145
  projectRoot: resolved.projectRoot,
112
146
  config: resolved.config,
113
147
  at,
114
- dryRun: input.dryRun
148
+ dryRun: input.dryRun,
149
+ ...status === void 0 ? {} : { status }
115
150
  });
116
151
  const displayPath = `${displayRoot(cwd, result.bundleRoot, path)}/${result.conceptPath}`;
117
152
  if (input.format === "json") {
@@ -122,6 +157,7 @@ const verifyCommand = Command.make("verify", {
122
157
  by: result.by,
123
158
  at: result.at,
124
159
  dryRun: result.dryRun,
160
+ status: result.status,
125
161
  ...Option.isSome(distribution) ? { distribution: distribution.value } : {}
126
162
  });
127
163
  yield* Console.log(JSON.stringify(Schema.encodeSync(VerifyEnvelope)(envelope)));
@@ -131,16 +167,17 @@ const verifyCommand = Command.make("verify", {
131
167
  at: result.at,
132
168
  priorAt: result.priorAt,
133
169
  dryRun: result.dryRun,
134
- fragment: result.fragment
170
+ fragment: result.fragment,
171
+ status: result.status,
172
+ statusFragment: result.statusFragment
135
173
  })) yield* Console.log(line);
136
- setExitCode(0);
137
174
  }).pipe(Effect.provide(Git.layer), provideConfig({
138
175
  explicitConfigPath: input.config,
139
176
  discoveryCwd
140
- }));
177
+ }), CliInteractive.unless(input.format === "json"));
141
178
  if (input.format === "json") return yield* body.pipe(Effect.tapError((error) => Console.log(JSON.stringify(jsonError(error, CLI_VERSION, Option.getOrUndefined(distribution))))));
142
179
  return yield* body;
143
- })).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. This is a human-run command: no agent, hook, or MCP tool ever invokes it."));
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."));
144
181
 
145
182
  //#endregion
146
183
  export { verifyCommand };
package/errors.js CHANGED
@@ -1,6 +1,7 @@
1
+ import { InitBundleDirError } from "./internal/initWizard.js";
1
2
  import { DocumentStdinIsTerminalError } from "./internal/stdin.js";
2
- import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
3
- import { ConfigIssueRenderer } from "@effected/cli";
3
+ import { Cancelled, ConfigIssueRenderer, NotInteractive } from "@effected/cli";
4
+ import { ConfigMalformedError, ConfigPathNotFoundError, DocumentPathError, InitOverwriteError, QueryConceptNotFoundError, QuerySelectionError, QueryUnknownVocabularyError, SyncStagedLogError, VerifyConceptNotFoundError, VerifySelectionError, VerifyUnsupportedFrontmatterError } from "@okfit/engine";
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
@@ -50,6 +53,9 @@ const relativeToCwd = (path, cwd) => {
50
53
  * contradictory, empty, or named an undeclared type.
51
54
  * 5c. `DocumentPathError` and `DocumentStdinIsTerminalError` (`--document`)
52
55
  * each render as one `error: <message>` line.
56
+ * 5d. `QueryUnknownVocabularyError`, `QueryConceptNotFoundError` and
57
+ * `QuerySelectionError` (`okfit query`) each render as one
58
+ * `error: <message>` line.
53
59
  * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
54
60
  * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
55
61
  * (the K-13 `HOME`-unset case) — renders as the single line
@@ -61,6 +67,7 @@ const relativeToCwd = (path, cwd) => {
61
67
  */
62
68
  const renderFailure = (error) => {
63
69
  if (hasTag(error, "ShowHelp")) return [];
70
+ if (error instanceof Cancelled || error instanceof NotInteractive) return [error.message];
64
71
  if (error instanceof ConfigPathNotFoundError) return [`error: ${error.message}`];
65
72
  if (error instanceof ConfigMalformedError) return [`error: ${error.message}`];
66
73
  if (error instanceof InitOverwriteError) return [
@@ -68,10 +75,14 @@ const renderFailure = (error) => {
68
75
  ...error.paths.map((path) => ` ${relativeToCwd(path, error.cwd)}`),
69
76
  "Nothing was written."
70
77
  ];
78
+ if (error instanceof InitBundleDirError) return [`error: ${error.message}`];
71
79
  if (error instanceof VerifyConceptNotFoundError) return [`error: ${error.message}`];
72
80
  if (error instanceof VerifyUnsupportedFrontmatterError) return [`error: ${error.message}`];
73
81
  if (error instanceof SyncStagedLogError) return [`error: ${error.message}`];
74
82
  if (error instanceof VerifySelectionError) return [`error: ${error.message}`];
83
+ if (error instanceof QueryUnknownVocabularyError) return [`error: ${error.message}`];
84
+ if (error instanceof QueryConceptNotFoundError) return [`error: ${error.message}`];
85
+ if (error instanceof QuerySelectionError) return [`error: ${error.message}`];
75
86
  if (error instanceof DocumentPathError) return [`error: ${error.message}`];
76
87
  if (error instanceof DocumentStdinIsTerminalError) return [`error: ${error.message}`];
77
88
  if (hasTag(error, "ConfigValidationError")) {
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
@@ -10,8 +11,9 @@ import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
10
11
  * `okfit` therefore prints the root help and exits `0` with no code in this
11
12
  * package at all.
12
13
  *
13
- * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`, and
14
- * `stale` are the whole command tree; nothing else is registered here.
14
+ * `validate`, `init`, `context`, `verify`, `sync`, `lint`, `graph`,
15
+ * `stale`, and `query` (with its `list`/`get`/`neighbors`) are the whole
16
+ * command tree; nothing else is registered here.
15
17
  * Each is appended in introduction order, never reordered in, so
16
18
  * `--help`'s subcommand list reads that way too (contract §4.2). The
17
19
  * top-level description is left unchanged: neither `context` nor `verify`
@@ -20,7 +22,22 @@ import { ContextEnvelope, RenderedDiagnostic } from "@okfit/engine";
20
22
  *
21
23
  * @public
22
24
  */
23
- export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("@okfit/profiles").AgentActorUnconfiguredError | import("@okfit/core").BundleDepthExceededError | import("@okfit/core").BundleReadError | import("@okfit/core").BundleRootNotFoundError | import("@effected/config-file").ConfigCodecError | import("@effected/config-file").ConfigFileReadError | import("@effected/config-file").ConfigFileWriteError | import("@okfit/engine").ConfigMalformedError | import("@okfit/engine").ConfigPathNotFoundError | import("@effected/config-file").ConfigValidationError | import("@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("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>;
24
41
  //#endregion
25
42
  //#region src/errors.d.ts
26
43
  /**
@@ -51,6 +68,8 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
51
68
  * type-only import — this is the "path unknown" case, K-46) renders as
52
69
  * `error: ${String(error)}` followed by one two-space-indented
53
70
  * `ConfigIssueRenderer.render(error)` line per entry.
71
+ * 4a. `InitBundleDirError` (`okfit init --bundle`, exit 64) renders as one
72
+ * `error: <message>` line.
54
73
  * 5. `VerifyConceptNotFoundError` and `VerifyUnsupportedFrontmatterError`
55
74
  * each render as their own `error: <message>` line; both messages
56
75
  * already name the concept id and what to do about it, and neither
@@ -63,6 +82,9 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
63
82
  * contradictory, empty, or named an undeclared type.
64
83
  * 5c. `DocumentPathError` and `DocumentStdinIsTerminalError` (`--document`)
65
84
  * each render as one `error: <message>` line.
85
+ * 5d. `QueryUnknownVocabularyError`, `QueryConceptNotFoundError` and
86
+ * `QuerySelectionError` (`okfit query`) each render as one
87
+ * `error: <message>` line.
66
88
  * 6. Everything else — core's `BundleRootNotFoundError`/`BundleReadError`/
67
89
  * `BundleDepthExceededError`, config-file's other errors, `XdgEnvError`
68
90
  * (the K-13 `HOME`-unset case) — renders as the single line
@@ -74,6 +96,23 @@ export declare const rootCommand: Command.Command<"okfit", {} | {}, {}, import("
74
96
  */
75
97
  export declare const renderFailure: (error: unknown) => ReadonlyArray<string>;
76
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
77
116
  //#region src/internal/stdin.d.ts
78
117
  declare const DocumentStdinIsTerminalError_base: Schema.Class<DocumentStdinIsTerminalError, Schema.TaggedStruct<"DocumentStdinIsTerminalError", {
79
118
  readonly path: Schema.String;
@@ -112,19 +151,27 @@ interface Counts {
112
151
  readonly info: number;
113
152
  readonly concepts: number;
114
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;
115
162
  /**
116
163
  * K-16. With a range:
117
164
  * `<file>:<range.line + 1>:<range.character + 1> <severity> <code> <message>`.
118
165
  * Without one: `<file> <severity> <code> <message>`. `file: ""` renders as
119
166
  * the literal `(bundle)`. Core's range is zero-based (D-32,
120
167
  * `CORE/Diagnostic.ts:49-57`); the `+ 1`s here are the only place it becomes
121
- * 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
122
169
  * (K-19) — never the code, the path, or the message.
123
170
  *
124
171
  * @public
125
172
  */
126
173
  export declare const line: (diagnostic: RenderedDiagnostic, options?: {
127
- readonly color?: boolean;
174
+ readonly paint?: SeverityPaint;
128
175
  }) => string;
129
176
  /**
130
177
  * `sort` then `line` over the whole set: the exact stdout body of
@@ -133,7 +180,7 @@ export declare const line: (diagnostic: RenderedDiagnostic, options?: {
133
180
  * @public
134
181
  */
135
182
  export declare const human: (diagnostics: ReadonlyArray<RenderedDiagnostic>, options?: {
136
- readonly color?: boolean;
183
+ readonly paint?: SeverityPaint;
137
184
  }) => ReadonlyArray<string>;
138
185
  /**
139
186
  * K-20, verbatim and unpluralised —
@@ -156,6 +203,13 @@ interface VerifyLines {
156
203
  readonly dryRun: boolean;
157
204
  /** The exact bytes a real run would splice in; printed only when `dryRun`. */
158
205
  readonly fragment: string;
206
+ /** Issue #185: the status change requested, or `null` when no status flag was given. */
207
+ readonly status: {
208
+ readonly from: string | null;
209
+ readonly to: string;
210
+ } | null;
211
+ /** The exact status bytes a real run would splice in; printed only when `dryRun`. */
212
+ readonly statusFragment: string | null;
159
213
  }
160
214
  /**
161
215
  * V-11's human output: one line per fact. One `already verified` line per
@@ -185,5 +239,5 @@ export declare const humanVerify: (input: VerifyLines) => ReadonlyArray<string>;
185
239
  */
186
240
  export declare const CLI_VERSION: string;
187
241
  //#endregion
188
- export type { Counts, VerifyLines };
242
+ export type { Counts, SeverityPaint, VerifyLines };
189
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.6.12",
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.0",
56
- "@effected/yaml": "^0.18.0",
57
- "@okfit/core": "0.8.3",
58
- "@okfit/engine": "0.9.4",
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"