@okfit/cli 0.6.12 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -3
- package/commands/context.js +0 -2
- package/commands/graph.js +0 -2
- package/commands/init.js +56 -23
- package/commands/lint.js +4 -5
- package/commands/query.js +172 -0
- package/commands/root.js +8 -4
- package/commands/stale.js +0 -2
- package/commands/sync.js +0 -2
- package/commands/validate.js +4 -5
- package/commands/verify-picker.js +75 -0
- package/commands/verify.js +48 -11
- package/errors.js +13 -2
- package/index.d.ts +61 -7
- package/index.js +2 -1
- package/internal/initWizard.js +121 -0
- package/internal/versionFormatter.js +1 -1
- package/main.js +4 -4
- package/package.json +20 -17
- package/render/human.js +2 -13
- package/render/query.js +58 -0
- package/render/verify.js +10 -6
- package/version.js +1 -1
- package/internal/exit.js +0 -14
|
@@ -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 };
|
package/commands/verify.js
CHANGED
|
@@ -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>]... [--
|
|
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 (
|
|
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 {
|
|
3
|
-
import {
|
|
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`,
|
|
14
|
-
* `stale
|
|
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", {
|
|
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 `
|
|
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
|
|
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
|
|
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 {
|
|
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*
|
|
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.
|
|
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
|
|
46
|
-
"@effected/cli": "^0.
|
|
47
|
-
"@effected/config-file": "^0.
|
|
48
|
-
"@effected/engine": "^0.
|
|
49
|
-
"@effected/
|
|
50
|
-
"@effected/
|
|
51
|
-
"@effected/
|
|
52
|
-
"@effected/
|
|
53
|
-
"@effected/
|
|
54
|
-
"@effected/
|
|
55
|
-
"@effected/
|
|
56
|
-
"@effected/
|
|
57
|
-
"@
|
|
58
|
-
"@okfit/
|
|
59
|
-
"@okfit/
|
|
60
|
-
"
|
|
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"
|