@okfit/cli 0.1.0 → 0.3.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 +332 -3
- package/bin/okfit.js +2 -8
- package/commands/context.js +75 -0
- package/commands/init.js +171 -0
- package/commands/root.js +26 -3
- package/commands/sync.js +108 -0
- package/commands/validate.js +93 -0
- package/commands/verify.js +94 -0
- package/errors.js +70 -0
- package/index.d.ts +147 -6
- package/index.js +6 -10
- package/internal/exit.js +14 -0
- package/internal/tty.js +13 -0
- package/main.d.ts +15 -0
- package/main.js +44 -0
- package/package.json +20 -5
- package/render/context.js +29 -0
- package/render/human.js +68 -0
- package/render/sync.js +41 -0
- package/render/verify.js +29 -0
- package/version.js +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okfit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": [
|
|
@@ -31,16 +31,31 @@
|
|
|
31
31
|
"import": "./index.js",
|
|
32
32
|
"default": "./index.js"
|
|
33
33
|
},
|
|
34
|
+
"./main": {
|
|
35
|
+
"types": "./main.d.ts",
|
|
36
|
+
"import": "./main.js",
|
|
37
|
+
"default": "./main.js"
|
|
38
|
+
},
|
|
34
39
|
"./package.json": "./package.json"
|
|
35
40
|
},
|
|
36
41
|
"bin": {
|
|
37
42
|
"okfit": "bin/okfit.js"
|
|
38
43
|
},
|
|
39
44
|
"dependencies": {
|
|
40
|
-
"@effect/platform-node": "4.0.0-rc.
|
|
41
|
-
"@
|
|
42
|
-
"@
|
|
43
|
-
"
|
|
45
|
+
"@effect/platform-node": "4.0.0-rc.112",
|
|
46
|
+
"@effected/cli": "^0.3.1",
|
|
47
|
+
"@effected/config-file": "^0.7.0",
|
|
48
|
+
"@effected/git": "^0.12.0",
|
|
49
|
+
"@effected/glob": "^0.5.0",
|
|
50
|
+
"@effected/jsonc": "^0.9.0",
|
|
51
|
+
"@effected/markdown": "^0.9.1",
|
|
52
|
+
"@effected/toml": "^0.6.0",
|
|
53
|
+
"@effected/walker": "^0.7.0",
|
|
54
|
+
"@effected/yaml": "^0.14.0",
|
|
55
|
+
"@okfit/core": "0.2.0",
|
|
56
|
+
"@okfit/engine": "0.1.0",
|
|
57
|
+
"@okfit/profiles": "0.2.0",
|
|
58
|
+
"effect": "4.0.0-rc.112"
|
|
44
59
|
},
|
|
45
60
|
"engines": {
|
|
46
61
|
"node": ">=24.11.0"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/render/context.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `human` format: a short header block, then one line per type and one
|
|
4
|
+
* per tag. Pure; the caller pipes each line through `Console.log`.
|
|
5
|
+
*
|
|
6
|
+
* The `profile:` line reads `profile: (none) (requested NAME, unknown)`
|
|
7
|
+
* when `profile` and `profile_requested` disagree over an actually-unknown
|
|
8
|
+
* profile — never for the `"none"` or no-config cases, where a `null`
|
|
9
|
+
* `profile` is expected, not an error.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
const humanContext = (envelope) => [
|
|
14
|
+
`project root: ${envelope.project_root}`,
|
|
15
|
+
`bundle root: ${envelope.bundle_root}`,
|
|
16
|
+
`config: ${envelope.config_path ?? "(none)"}`,
|
|
17
|
+
envelope.profile === null && envelope.profile_requested !== null && envelope.profile_requested !== "none" ? `profile: (none) (requested ${envelope.profile_requested}, unknown)` : `profile: ${envelope.profile ?? "(none)"}`,
|
|
18
|
+
`index.md: ${envelope.index_path} (${envelope.index_exists ? "exists" : "missing"})`,
|
|
19
|
+
`agent: ${envelope.actors.agent ?? "(unset)"}`,
|
|
20
|
+
"",
|
|
21
|
+
"types:",
|
|
22
|
+
...envelope.types.map((t) => ` ${t.name} ${t.description ?? ""}`),
|
|
23
|
+
"",
|
|
24
|
+
"tags:",
|
|
25
|
+
...envelope.tags.map((t) => ` ${t.name} ${t.description ?? ""}`)
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { humanContext };
|
package/render/human.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { sort } from "@okfit/engine";
|
|
2
|
+
|
|
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
|
+
/**
|
|
15
|
+
* K-16. With a range:
|
|
16
|
+
* `<file>:<range.line + 1>:<range.character + 1> <severity> <code> <message>`.
|
|
17
|
+
* Without one: `<file> <severity> <code> <message>`. `file: ""` renders as
|
|
18
|
+
* the literal `(bundle)`. Core's range is zero-based (D-32,
|
|
19
|
+
* `CORE/Diagnostic.ts:49-57`); the `+ 1`s here are the only place it becomes
|
|
20
|
+
* one-based. Colour, when `color` is `true`, wraps ONLY the severity word
|
|
21
|
+
* (K-19) — never the code, the path, or the message.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
const line = (diagnostic, options) => {
|
|
26
|
+
const color = options?.color ?? false;
|
|
27
|
+
const file = diagnostic.file === "" ? "(bundle)" : diagnostic.file;
|
|
28
|
+
const severity = colorize(diagnostic.severity, color);
|
|
29
|
+
return `${diagnostic.range === void 0 ? file : `${file}:${diagnostic.range.line + 1}:${diagnostic.range.character + 1}`} ${severity} ${diagnostic.code} ${diagnostic.message}`;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* `sort` then `line` over the whole set: the exact stdout body of
|
|
33
|
+
* `--format human`, one array element per stdout line.
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
const human = (diagnostics, options) => sort(diagnostics).map((d) => line(d, options));
|
|
38
|
+
/**
|
|
39
|
+
* K-20, verbatim and unpluralised —
|
|
40
|
+
* `<E> errors, <W> warnings, <I> info in <N> concepts (<root>)`. `root` is
|
|
41
|
+
* pre-rendered by the caller: relative to cwd when under it, absolute
|
|
42
|
+
* otherwise (K-51).
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
const summary = (counts, root) => `${counts.errors} errors, ${counts.warnings} warnings, ${counts.info} info in ${counts.concepts} concepts (${root})`;
|
|
47
|
+
/**
|
|
48
|
+
* The one K-51 display-path rule, shared by both `commands/validate.ts`'s
|
|
49
|
+
* `summary` root and `commands/init.ts`'s success line: `target` relative
|
|
50
|
+
* to `cwd` when it is under it, absolute otherwise. Three cases, in order:
|
|
51
|
+
*
|
|
52
|
+
* 1. `target === cwd` (`path.relative` returns `""`): the literal `.`.
|
|
53
|
+
* 2. The relative form starts with `..`, or is itself absolute (a target on
|
|
54
|
+
* a different root than `cwd`, where `Path.relative` can return an
|
|
55
|
+
* absolute path unchanged depending on the platform): `target`
|
|
56
|
+
* unchanged, absolute.
|
|
57
|
+
* 3. Otherwise: the relative form.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
const displayRoot = (cwd, target, path) => {
|
|
62
|
+
const relative = path.relative(cwd, target);
|
|
63
|
+
if (relative === "") return ".";
|
|
64
|
+
return relative.startsWith("..") || path.isAbsolute(relative) ? target : relative;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { displayRoot, human, line, summary };
|
package/render/sync.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/render/sync.ts
|
|
2
|
+
/** Contract §9.2's fixed reason-sentence table, closed over `SkipReason`. */
|
|
3
|
+
const REASON_SENTENCE = {
|
|
4
|
+
untracked: "not tracked by git",
|
|
5
|
+
dirty: "has uncommitted changes",
|
|
6
|
+
unborn: "the repository has no commits yet",
|
|
7
|
+
"generated-missing": "has no generated block",
|
|
8
|
+
"generated-unsupported": "generated.at is a shape sync cannot edit; edit it by hand",
|
|
9
|
+
"log-unparseable": "log.md could not be parsed; see okfit validate"
|
|
10
|
+
};
|
|
11
|
+
const humanMode = (name, mode) => {
|
|
12
|
+
if (!mode.selected) return [`${name}: not selected`];
|
|
13
|
+
const lines = [`${name}:`];
|
|
14
|
+
for (const id of mode.written) lines.push(` wrote ${id}`);
|
|
15
|
+
for (const id of mode.unchanged) lines.push(` unchanged ${id}`);
|
|
16
|
+
for (const entry of mode.skipped) lines.push(` skipped ${entry.id}: ${REASON_SENTENCE[entry.reason]}`);
|
|
17
|
+
return lines;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Contract §9.2 (design §2): per mode, three lists, then a one-line
|
|
21
|
+
* summary. A mode `--only` excluded from renders as `not selected` rather
|
|
22
|
+
* than three empty lists — an implementer choice within the fixed JSON
|
|
23
|
+
* shape (§14 makes no ruling on the human half; this is cosmetic).
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
const humanSync = (result) => {
|
|
28
|
+
const lines = [
|
|
29
|
+
...humanMode("generated", result.generated),
|
|
30
|
+
...humanMode("index", result.index),
|
|
31
|
+
...humanMode("log", result.log)
|
|
32
|
+
];
|
|
33
|
+
const totalWritten = result.generated.written.length + result.index.written.length + result.log.written.length;
|
|
34
|
+
const totalUnchanged = result.generated.unchanged.length + result.index.unchanged.length + result.log.unchanged.length;
|
|
35
|
+
const totalSkipped = result.generated.skipped.length + result.index.skipped.length + result.log.skipped.length;
|
|
36
|
+
lines.push(result.dryRun ? `would write ${totalWritten}, unchanged ${totalUnchanged}, skipped ${totalSkipped} (dry run, nothing written)` : `wrote ${totalWritten}, unchanged ${totalUnchanged}, skipped ${totalSkipped}`);
|
|
37
|
+
return lines;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { humanSync };
|
package/render/verify.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/render/verify.ts
|
|
2
|
+
/**
|
|
3
|
+
* Indent every line of `fragment` two spaces for display under a
|
|
4
|
+
* `would write:` header, dropping the single trailing empty line a
|
|
5
|
+
* newline-terminated fragment produces on split (I3).
|
|
6
|
+
*/
|
|
7
|
+
const indentFragment = (fragment) => {
|
|
8
|
+
const lines = fragment.split(/\r\n|\n/);
|
|
9
|
+
return (lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines).map((line) => ` ${line}`);
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* V-11's human output: one line per fact. One `already verified` line per
|
|
13
|
+
* prior entry by the same actor, in list order (V-2 says "entries",
|
|
14
|
+
* plural), then the success line. A prior entry by a DIFFERENT actor is
|
|
15
|
+
* not called out: it stays on disk untouched (V-1) and is simply not this
|
|
16
|
+
* line's subject. Under `--dry-run` (I3), a `would write:` header and the
|
|
17
|
+
* exact fragment a real run would splice in follow, so the preview
|
|
18
|
+
* exercises — and shows — the same edit the write path would make.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
22
|
+
const humanVerify = (input) => [
|
|
23
|
+
...input.priorAt.map((at) => `already verified by ${input.by} at ${at}; appending`),
|
|
24
|
+
input.dryRun ? `would verify ${input.id} by ${input.by} at ${input.at} (dry run, nothing written)` : `verified ${input.id} by ${input.by} at ${input.at}`,
|
|
25
|
+
...input.dryRun ? ["would write:", ...indentFragment(input.fragment)] : []
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { humanVerify };
|
package/version.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/version.ts
|
|
2
|
+
/**
|
|
3
|
+
* The version string reported by `okfit --version`. `@savvy-web/bundler`
|
|
4
|
+
* replaces `process.env.__PACKAGE_VERSION__` with this package's own
|
|
5
|
+
* version at build time (K-32), so a release can never desync from the
|
|
6
|
+
* printed version. `"0.0.0"` is the unbuilt-source fallback and reads as
|
|
7
|
+
* dev mode.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
const CLI_VERSION = "0.3.0";
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { CLI_VERSION };
|