@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 +25 -1
- 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 +0 -2
- package/commands/root.js +2 -1
- 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 +31 -7
- package/errors.js +6 -1
- package/index.d.ts +48 -5
- 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/version.js +1 -1
- package/internal/exit.js +0 -14
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
|
|
package/commands/context.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { humanContext } from "../render/context.js";
|
|
3
2
|
import { CLI_VERSION } from "../version.js";
|
|
4
3
|
import { Argument, Command, Flag } from "effect/cli";
|
|
@@ -64,7 +63,6 @@ const contextCommand = Command.make("context", {
|
|
|
64
63
|
});
|
|
65
64
|
if (input.format === "json") yield* Console.log(JSON.stringify(Schema.encodeSync(ContextEnvelope)(envelope)));
|
|
66
65
|
else for (const contextLine of humanContext(envelope)) yield* Console.log(contextLine);
|
|
67
|
-
setExitCode(0);
|
|
68
66
|
}).pipe(provideConfig({
|
|
69
67
|
explicitConfigPath: input.config,
|
|
70
68
|
discoveryCwd
|
package/commands/graph.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { Argument, Command, Flag } from "effect/cli";
|
|
4
3
|
import { CurrentDistribution } from "@effected/engine";
|
|
@@ -65,7 +64,6 @@ const graphCommand = Command.make("graph", {
|
|
|
65
64
|
yield* Console.log(JSON.stringify(Schema.encodeSync(GraphEnvelope)(envelope)));
|
|
66
65
|
} else if (input.format === "dot") yield* Console.log(result.graph.toGraphViz());
|
|
67
66
|
else yield* Console.log(result.graph.toMermaid());
|
|
68
|
-
setExitCode(0);
|
|
69
67
|
}).pipe(provideConfig({
|
|
70
68
|
explicitConfigPath: input.config,
|
|
71
69
|
discoveryCwd
|
package/commands/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { INIT_CONFIG_LOCATIONS, initWizard } from "../internal/initWizard.js";
|
|
2
2
|
import { displayRoot, human, summary } from "../render/human.js";
|
|
3
|
+
import { CliExit, CliTheme } from "@effected/cli";
|
|
3
4
|
import { Argument, Command, Flag } from "effect/cli";
|
|
4
5
|
import { CONFIG_RELATIVE_PATH, InitOverwriteError, Now, collect, configValue, files, forDiagnostics, provideConfig, resolveBundleRoot, resolveProjectRoot, run, targetPaths } from "@okfit/engine";
|
|
5
6
|
import { Console, DateTime, Effect, FileSystem, Layer, Option, Path } from "effect";
|
|
6
7
|
import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile, SCHEMA_DIRECTIVE } from "@okfit/core";
|
|
7
|
-
import { CliColor } from "@effected/cli";
|
|
8
8
|
import { Git } from "@effected/git";
|
|
9
9
|
import { GitHistory, Profiles } from "@okfit/profiles";
|
|
10
10
|
|
|
@@ -23,7 +23,11 @@ const configFlag = Flag.File("config").pipe(Flag.optional, Flag.withDescription(
|
|
|
23
23
|
* CLI-rendered warning (K-4), matching `Profiles.get`'s own `Option.none`
|
|
24
24
|
* contract (P-38), not a parser-level `CliError.InvalidValue`.
|
|
25
25
|
*/
|
|
26
|
-
const profileFlag = Flag.String("profile").pipe(Flag.optional, Flag.withDescription("profile to scaffold with (default: the config's bundle.profile or software-project)"));
|
|
26
|
+
const profileFlag = Flag.String("profile").pipe(Flag.optional, Flag.withDescription("profile to scaffold with (default: the config's bundle.profile or software-project; prompted for when interactive)"));
|
|
27
|
+
/** `--bundle <dir>`: project-relative; validated by the wizard before anything else (usage error, exit 64). */
|
|
28
|
+
const bundleFlag = Flag.String("bundle").pipe(Flag.optional, Flag.withDescription("bundle directory, relative to the project root (default: the config's bundle.path or okf; prompted for when interactive)"));
|
|
29
|
+
/** `--config-location`: where the config file is written, one of the three discovered names (C-1). */
|
|
30
|
+
const configLocationFlag = Flag.Literals("config-location", INIT_CONFIG_LOCATIONS).pipe(Flag.optional, Flag.withDescription("where to write the config file (default: .config/okfit.toml; prompted for when interactive)"));
|
|
27
31
|
/**
|
|
28
32
|
* Stands in for `profile.config` when `Profiles.get` returns `None`.
|
|
29
33
|
* `OkfitConfig.merge` only visits `Object.keys(override)`
|
|
@@ -48,8 +52,13 @@ const countsOf = (diagnostics, concepts) => ({
|
|
|
48
52
|
concepts
|
|
49
53
|
});
|
|
50
54
|
/**
|
|
51
|
-
* `okfit init [path] [--profile <name>] [--
|
|
52
|
-
* `--format`, human output only, K-6).
|
|
55
|
+
* `okfit init [path] [--profile <name>] [--bundle <dir>] [--config-location <name>]
|
|
56
|
+
* [--config <file>]` (K-2, K-3; no `--format`, human output only, K-6).
|
|
57
|
+
*
|
|
58
|
+
* Interactive runs prompt (the kit's Ink screens, `initWizard`) for each of
|
|
59
|
+
* profile, bundle directory and config location whose flag was not given;
|
|
60
|
+
* every prompt happens after discovery and before any write (step 4), so a
|
|
61
|
+
* cancel exits 130 with nothing written.
|
|
53
62
|
*
|
|
54
63
|
* Handler order, fixed by the contract:
|
|
55
64
|
*
|
|
@@ -61,7 +70,8 @@ const countsOf = (diagnostics, concepts) => ({
|
|
|
61
70
|
* `sources[0]`.
|
|
62
71
|
* 4. `profileName = Option.getOrElse(input.profile, () =>
|
|
63
72
|
* fileConfig.bundle?.profile ?? DEFAULTS.bundle.profile)` (K-3, the one
|
|
64
|
-
* difference from `validate`'s step 4)
|
|
73
|
+
* difference from `validate`'s step 4), then `--bundle`/`--config-location`
|
|
74
|
+
* likewise, all three through `initWizard`. `Profiles.get(profileName)`.
|
|
65
75
|
* `None` and `profileName !== "none"` warns (K-4); `"none"` is silent.
|
|
66
76
|
* 5. `merged = OkfitConfig.merge(OkfitConfig.merge(DEFAULTS, profileConfig),
|
|
67
77
|
* fileConfig)`, `profileConfig` falling back to `NO_PROFILE_CONFIG` when
|
|
@@ -81,19 +91,15 @@ const countsOf = (diagnostics, concepts) => ({
|
|
|
81
91
|
* 10. `Console.log` the K-51 success line.
|
|
82
92
|
* 11. self-validate (K-29): `run({ root: bundleRoot, config: merged,
|
|
83
93
|
* profile, now })` over the bundle just written, rendered exactly as
|
|
84
|
-
* `validate --format human` does, `
|
|
94
|
+
* `validate --format human` does, `CliExit.set` to its
|
|
85
95
|
* `forDiagnostics` result. The handler SUCCEEDS (K-7); the failure path
|
|
86
96
|
* is only `InitOverwriteError` at step 8, or an infrastructure error
|
|
87
97
|
* that already carries its own `[Runtime.errorExitCode]`.
|
|
88
98
|
*
|
|
89
99
|
* @public
|
|
90
100
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
config: configFlag,
|
|
94
|
-
profile: profileFlag
|
|
95
|
-
}, (input) => Effect.gen(function* () {
|
|
96
|
-
const cwd = process.cwd();
|
|
101
|
+
/** The `init` handler body, parameterised on `cwd` so tests can drive it against a temp project. */
|
|
102
|
+
const initProgram = (input, cwd) => Effect.gen(function* () {
|
|
97
103
|
const discoveryCwd = Option.getOrElse(input.path, () => cwd);
|
|
98
104
|
yield* provideConfig({
|
|
99
105
|
explicitConfigPath: input.config,
|
|
@@ -102,14 +108,31 @@ const initCommand = Command.make("init", {
|
|
|
102
108
|
const configFile = yield* OkfitConfigFile;
|
|
103
109
|
const winner = (yield* configFile.discover)[0];
|
|
104
110
|
const fileConfig = winner === void 0 ? { extensions: {} } : winner.value;
|
|
105
|
-
const
|
|
111
|
+
const defaultBundle = fileConfig.bundle?.path ?? DEFAULT_BUNDLE_PATH;
|
|
112
|
+
const answers = yield* initWizard({
|
|
113
|
+
profile: input.profile,
|
|
114
|
+
bundle: input.bundle,
|
|
115
|
+
location: input.configLocation
|
|
116
|
+
}, {
|
|
117
|
+
profile: fileConfig.bundle?.profile ?? DEFAULT_PROFILE_NAME,
|
|
118
|
+
bundle: defaultBundle,
|
|
119
|
+
location: CONFIG_RELATIVE_PATH
|
|
120
|
+
});
|
|
121
|
+
const profileName = answers.profile;
|
|
106
122
|
const profile = Profiles.get(profileName);
|
|
107
123
|
if (Option.isNone(profile) && profileName !== "none") yield* Console.error(`warning: unknown profile "${profileName}"; continuing with defaults`);
|
|
108
124
|
const profileConfig = Option.match(profile, {
|
|
109
125
|
onNone: () => NO_PROFILE_CONFIG,
|
|
110
126
|
onSome: (resolved) => resolved.config
|
|
111
127
|
});
|
|
112
|
-
const
|
|
128
|
+
const mergedBase = OkfitConfig.merge(OkfitConfig.merge(OkfitConfig.DEFAULTS, profileConfig), fileConfig);
|
|
129
|
+
const merged = {
|
|
130
|
+
...mergedBase,
|
|
131
|
+
bundle: {
|
|
132
|
+
...mergedBase.bundle,
|
|
133
|
+
path: answers.bundle
|
|
134
|
+
}
|
|
135
|
+
};
|
|
113
136
|
if (merged.okf_version !== void 0 && merged.okf_version !== OKF_SPEC_VERSION) yield* Console.error(`warning: okf_version "${merged.okf_version}" does not match this okfit's spec version "${OKF_SPEC_VERSION}"; continuing`);
|
|
114
137
|
const path = yield* Path.Path;
|
|
115
138
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -143,12 +166,14 @@ const initCommand = Command.make("init", {
|
|
|
143
166
|
paths: existing,
|
|
144
167
|
cwd
|
|
145
168
|
});
|
|
146
|
-
|
|
147
|
-
const
|
|
169
|
+
const configTargets = new Set(INIT_CONFIG_LOCATIONS.map((location) => `${projectRoot}/${location}`));
|
|
170
|
+
const written = [...paths.filter((target) => !configTargets.has(target)), `${projectRoot}/${answers.location}`];
|
|
171
|
+
for (const target of written) yield* fs.makeDirectory(path.dirname(target), { recursive: true });
|
|
172
|
+
const bundlePath = answers.bundle;
|
|
148
173
|
yield* configFile.write(configValue({
|
|
149
174
|
...scaffoldOptions,
|
|
150
175
|
bundlePath
|
|
151
|
-
}), `${projectRoot}/${
|
|
176
|
+
}), `${projectRoot}/${answers.location}`, { header: SCHEMA_DIRECTIVE });
|
|
152
177
|
const scaffoldFiles = yield* files(scaffoldOptions);
|
|
153
178
|
for (const file of scaffoldFiles) yield* fs.writeFileString(file.path, file.contents);
|
|
154
179
|
yield* Console.log(`Initialized ${displayRoot(cwd, bundleRoot, path)} with the ${profileName} profile`);
|
|
@@ -159,12 +184,20 @@ const initCommand = Command.make("init", {
|
|
|
159
184
|
now
|
|
160
185
|
}).pipe(Effect.provide(Layer.mergeAll(Git.layer, GitHistory.layer)));
|
|
161
186
|
const diagnostics = collect(result.report.conformance, result.report.lint, result.profileDiagnostics);
|
|
162
|
-
const
|
|
163
|
-
for (const line of human(diagnostics, {
|
|
187
|
+
const theme = yield* CliTheme;
|
|
188
|
+
for (const line of human(diagnostics, { paint: theme.paint })) yield* Console.log(line);
|
|
164
189
|
yield* Console.error(summary(countsOf(diagnostics, result.bundle.concepts.size), displayRoot(cwd, bundleRoot, path)));
|
|
165
|
-
|
|
190
|
+
yield* CliExit.set(forDiagnostics(diagnostics));
|
|
166
191
|
}));
|
|
167
|
-
})
|
|
192
|
+
});
|
|
193
|
+
/** @public */
|
|
194
|
+
const initCommand = Command.make("init", {
|
|
195
|
+
path: pathArg,
|
|
196
|
+
config: configFlag,
|
|
197
|
+
profile: profileFlag,
|
|
198
|
+
bundle: bundleFlag,
|
|
199
|
+
configLocation: configLocationFlag
|
|
200
|
+
}, (input) => initProgram(input, process.cwd())).pipe(Command.withDescription("Scaffold a new OKF bundle: a config file, the bundle's root and per-directory index files, a project stub, and an initial log entry."));
|
|
168
201
|
|
|
169
202
|
//#endregion
|
|
170
|
-
export { initCommand };
|
|
203
|
+
export { initCommand, initProgram };
|
package/commands/lint.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { displayRoot, human, summary } from "../render/human.js";
|
|
3
|
+
import { CliExit, CliTheme } from "@effected/cli";
|
|
4
4
|
import { Argument, Command, Flag } from "effect/cli";
|
|
5
5
|
import { CurrentDistribution } from "@effected/engine";
|
|
6
6
|
import { JsonEnvelope, Now, collect, forDiagnostics, json, jsonError, provideConfig, resolveProjectConfig, run } from "@okfit/engine";
|
|
7
7
|
import { Console, Effect, Layer, Option, Path, Schema } from "effect";
|
|
8
8
|
import { OKF_SPEC_VERSION } from "@okfit/core";
|
|
9
|
-
import { CliColor } from "@effected/cli";
|
|
10
9
|
import { Git } from "@effected/git";
|
|
11
10
|
import { GitHistory } from "@okfit/profiles";
|
|
12
11
|
|
|
@@ -76,8 +75,8 @@ const lintCommand = Command.make("lint", {
|
|
|
76
75
|
});
|
|
77
76
|
yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
|
|
78
77
|
} else {
|
|
79
|
-
const
|
|
80
|
-
for (const diagnosticLine of human(diagnostics, {
|
|
78
|
+
const theme = yield* CliTheme;
|
|
79
|
+
for (const diagnosticLine of human(diagnostics, { paint: theme.paint })) yield* Console.log(diagnosticLine);
|
|
81
80
|
const counts = {
|
|
82
81
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
83
82
|
warnings: diagnostics.filter((d) => d.severity === "warning").length,
|
|
@@ -86,7 +85,7 @@ const lintCommand = Command.make("lint", {
|
|
|
86
85
|
};
|
|
87
86
|
yield* Console.error(summary(counts, displayRoot(cwd, bundleRoot, path)));
|
|
88
87
|
}
|
|
89
|
-
|
|
88
|
+
yield* CliExit.set(code);
|
|
90
89
|
}).pipe(provideConfig({
|
|
91
90
|
explicitConfigPath: input.config,
|
|
92
91
|
discoveryCwd
|
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
|
package/commands/validate.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { setExitCode } from "../internal/exit.js";
|
|
2
1
|
import { CLI_VERSION } from "../version.js";
|
|
3
2
|
import { displayRoot, human, summary } from "../render/human.js";
|
|
4
3
|
import { readDocumentText } from "../internal/stdin.js";
|
|
4
|
+
import { CliExit, CliTheme } from "@effected/cli";
|
|
5
5
|
import { Argument, Command, Flag } from "effect/cli";
|
|
6
6
|
import { CurrentDistribution } from "@effected/engine";
|
|
7
7
|
import { JsonEnvelope, Now, collect, forDiagnostics, json, jsonError, provideConfig, provideDocuments, resolveProjectConfig, run } from "@okfit/engine";
|
|
8
8
|
import { Console, Effect, Layer, Option, Path, Schema } from "effect";
|
|
9
9
|
import { OKF_SPEC_VERSION } from "@okfit/core";
|
|
10
|
-
import { CliColor } from "@effected/cli";
|
|
11
10
|
import { Git } from "@effected/git";
|
|
12
11
|
import { GitHistory } from "@okfit/profiles";
|
|
13
12
|
|
|
@@ -83,8 +82,8 @@ const validateCommand = Command.make("validate", {
|
|
|
83
82
|
});
|
|
84
83
|
yield* Console.log(JSON.stringify(Schema.encodeSync(JsonEnvelope)(envelope)));
|
|
85
84
|
} else {
|
|
86
|
-
const
|
|
87
|
-
for (const diagnosticLine of human(diagnostics, {
|
|
85
|
+
const theme = yield* CliTheme;
|
|
86
|
+
for (const diagnosticLine of human(diagnostics, { paint: theme.paint })) yield* Console.log(diagnosticLine);
|
|
88
87
|
const counts = {
|
|
89
88
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
90
89
|
warnings: diagnostics.filter((d) => d.severity === "warning").length,
|
|
@@ -93,7 +92,7 @@ const validateCommand = Command.make("validate", {
|
|
|
93
92
|
};
|
|
94
93
|
yield* Console.error(summary(counts, displayRoot(cwd, bundleRoot, path)));
|
|
95
94
|
}
|
|
96
|
-
|
|
95
|
+
yield* CliExit.set(code);
|
|
97
96
|
}).pipe(provideConfig({
|
|
98
97
|
explicitConfigPath: input.config,
|
|
99
98
|
discoveryCwd
|
|
@@ -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";
|
|
@@ -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", {
|
|
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 `
|
|
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
|
|
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
|
|
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 {
|
|
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"
|
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 `
|
|
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 =
|
|
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
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 };
|