@okfit/cli 0.2.0 → 0.4.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 +10 -7
- package/bin/okfit.js +2 -51
- package/commands/context.js +3 -6
- package/commands/init.js +3 -9
- package/commands/sync.js +3 -6
- package/commands/validate.js +3 -8
- package/commands/verify.js +3 -7
- package/errors.js +2 -105
- package/index.d.ts +25 -769
- package/index.js +4 -13
- package/main.d.ts +15 -0
- package/main.js +44 -0
- package/package.json +9 -6
- package/render/context.js +1 -80
- package/render/human.js +1 -1
- package/render/sync.js +1 -61
- package/render/verify.js +1 -44
- package/version.js +6 -6
- package/config/anchor.js +0 -57
- package/config/layer.js +0 -129
- package/config/resolve.js +0 -67
- package/context/run.js +0 -35
- package/init/scaffold.js +0 -174
- package/package.js +0 -5
- package/render/exit.js +0 -53
- package/render/json.js +0 -126
- package/render/sort.js +0 -46
- package/sync/generated.js +0 -111
- package/sync/index.js +0 -73
- package/sync/log.js +0 -127
- package/sync/run.js +0 -65
- package/sync/write.js +0 -48
- package/validate/run.js +0 -63
- package/verify/locate.js +0 -249
- package/verify/run.js +0 -106
- package/verify/splice.js +0 -75
package/index.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { buildConfigLayer, provideConfig } from "./config/layer.js";
|
|
3
|
-
import { resolveBundleRoot, resolveProjectRoot } from "./config/anchor.js";
|
|
4
|
-
import { DEFAULT_PROFILE_NAME, resolveProjectConfig } from "./config/resolve.js";
|
|
5
|
-
import { runContext } from "./context/run.js";
|
|
6
|
-
import { ContextEnvelope, ContextTag, ContextType, contextEnvelope, humanContext } from "./render/context.js";
|
|
7
|
-
import { forDiagnostics, tally } from "./render/exit.js";
|
|
8
|
-
import { collect, sort } from "./render/sort.js";
|
|
9
|
-
import { JsonDiagnostic, JsonEnvelope, JsonErrorEnvelope, JsonSummary, json, jsonError } from "./render/json.js";
|
|
1
|
+
import { humanContext } from "./render/context.js";
|
|
10
2
|
import { CLI_VERSION } from "./version.js";
|
|
11
|
-
import { CONFIG_RELATIVE_PATH, configValue, files, targetPaths } from "./init/scaffold.js";
|
|
12
3
|
import { human, line, summary } from "./render/human.js";
|
|
13
|
-
import {
|
|
14
|
-
import { VerifyEnvelope, humanVerify, verifyEnvelope } from "./render/verify.js";
|
|
4
|
+
import { humanVerify } from "./render/verify.js";
|
|
15
5
|
import { rootCommand } from "./commands/root.js";
|
|
6
|
+
import { renderFailure } from "./errors.js";
|
|
16
7
|
|
|
17
|
-
export { CLI_VERSION,
|
|
8
|
+
export { CLI_VERSION, human, humanContext, humanVerify, line, renderFailure, rootCommand, summary };
|
package/main.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/main.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The assembled okfit CLI program.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Run the okfit CLI. Owns the process: installs the runtime teardown and
|
|
9
|
+
* sets the exit code. `NodeRuntime.runMain` does not return a promise.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export declare const main: () => void;
|
|
14
|
+
//#endregion
|
|
15
|
+
//# sourceMappingURL=main.d.ts.map
|
package/main.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CLI_VERSION } from "./version.js";
|
|
2
|
+
import { rootCommand } from "./commands/root.js";
|
|
3
|
+
import { renderFailure } from "./errors.js";
|
|
4
|
+
import { Command } from "effect/unstable/cli";
|
|
5
|
+
import { Now, OkfitPlatform } from "@okfit/engine";
|
|
6
|
+
import { DateTime, Effect, Option } from "effect";
|
|
7
|
+
import { CliLogger, CliRuntime } from "@effected/cli";
|
|
8
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
|
|
9
|
+
|
|
10
|
+
//#region src/main.ts
|
|
11
|
+
/**
|
|
12
|
+
* The assembled okfit CLI program.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* K-47: an ISO-8601 `OKFIT_NOW` when set, else the wall clock. A documented
|
|
18
|
+
* test hook, not user-facing. Resolved exactly once, here, and provided to
|
|
19
|
+
* the whole command tree through the `Now` tag so no command handler ever
|
|
20
|
+
* reads `process.env["OKFIT_NOW"]` itself.
|
|
21
|
+
*/
|
|
22
|
+
const nowEffect = Option.fromNullishOr(process.env.OKFIT_NOW).pipe(Option.flatMap((iso) => DateTime.make(iso)), Option.match({
|
|
23
|
+
onNone: () => DateTime.now,
|
|
24
|
+
onSome: Effect.succeed
|
|
25
|
+
}));
|
|
26
|
+
/**
|
|
27
|
+
* Run the okfit CLI. Owns the process: installs the runtime teardown and
|
|
28
|
+
* sets the exit code. `NodeRuntime.runMain` does not return a promise.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
const main = () => {
|
|
33
|
+
const program = Effect.gen(function* () {
|
|
34
|
+
const now = yield* nowEffect;
|
|
35
|
+
return yield* Command.run(rootCommand, { version: CLI_VERSION }).pipe(Effect.provideService(Now, now), Effect.catchTag("ShowHelp", (help) => Effect.fail(CliRuntime.reported(help, help.errors.length > 0 ? 64 : 0))));
|
|
36
|
+
}).pipe(Effect.provide(OkfitPlatform), CliRuntime.reportFailures({
|
|
37
|
+
exitCode: 3,
|
|
38
|
+
render: renderFailure
|
|
39
|
+
}));
|
|
40
|
+
NodeRuntime.runMain(program.pipe(Effect.provide(CliLogger.layer())));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { main };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okfit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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,6 +31,11 @@
|
|
|
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": {
|
|
@@ -38,20 +43,18 @@
|
|
|
38
43
|
},
|
|
39
44
|
"dependencies": {
|
|
40
45
|
"@effect/platform-node": "4.0.0-rc.112",
|
|
41
|
-
"@effected/app": "^0.15.0",
|
|
42
46
|
"@effected/cli": "^0.3.1",
|
|
43
47
|
"@effected/config-file": "^0.7.0",
|
|
44
48
|
"@effected/git": "^0.12.0",
|
|
45
49
|
"@effected/glob": "^0.5.0",
|
|
46
50
|
"@effected/jsonc": "^0.9.0",
|
|
47
51
|
"@effected/markdown": "^0.9.1",
|
|
48
|
-
"@effected/store": "^0.7.0",
|
|
49
52
|
"@effected/toml": "^0.6.0",
|
|
50
53
|
"@effected/walker": "^0.7.0",
|
|
51
|
-
"@effected/xdg": "^0.4.1",
|
|
52
54
|
"@effected/yaml": "^0.14.0",
|
|
53
|
-
"@okfit/core": "0.
|
|
54
|
-
"@okfit/
|
|
55
|
+
"@okfit/core": "0.3.0",
|
|
56
|
+
"@okfit/engine": "0.2.0",
|
|
57
|
+
"@okfit/profiles": "0.3.0",
|
|
55
58
|
"effect": "4.0.0-rc.112"
|
|
56
59
|
},
|
|
57
60
|
"engines": {
|
package/render/context.js
CHANGED
|
@@ -1,83 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
|
|
3
1
|
//#region src/render/context.ts
|
|
4
|
-
/** One `types[]` entry (M-15). @public */
|
|
5
|
-
const ContextType = Schema.Struct({
|
|
6
|
-
name: Schema.String,
|
|
7
|
-
description: Schema.NullOr(Schema.String),
|
|
8
|
-
guidance: Schema.NullOr(Schema.String)
|
|
9
|
-
});
|
|
10
|
-
/** One `tags[]` entry (M-15). @public */
|
|
11
|
-
const ContextTag = Schema.Struct({
|
|
12
|
-
name: Schema.String,
|
|
13
|
-
description: Schema.NullOr(Schema.String)
|
|
14
|
-
});
|
|
15
|
-
/**
|
|
16
|
-
* M-15's envelope: schema 1, snake_case, orientation data only. Distinct
|
|
17
|
-
* from `JsonEnvelope` (`render/json.ts`) — `context` never runs conformance
|
|
18
|
-
* or lint checks, so there is no `diagnostics` array and no `exit_code`
|
|
19
|
-
* field at all.
|
|
20
|
-
*
|
|
21
|
-
* Every field is `Schema.NullOr`, never `Schema.optionalKey`: a consumer
|
|
22
|
-
* (the two hook scripts) reads a fixed key set and gets JSON `null` for an
|
|
23
|
-
* absent value, rather than having to distinguish a missing key from a
|
|
24
|
-
* null one. This is a deliberate difference from `JsonDiagnostic`'s
|
|
25
|
-
* `range`, which is `optionalKey` and omitted when absent
|
|
26
|
-
* (`render/json.ts:14,68`).
|
|
27
|
-
*
|
|
28
|
-
* `profile_requested` (final-review Important 1) is the profile name the
|
|
29
|
-
* config asked for after the K-4 default rule — `resolveProjectConfig`'s
|
|
30
|
-
* own `profileName` — and is `null` only when no config file was found at
|
|
31
|
-
* all. `profile` keeps its original meaning (the resolved profile's name,
|
|
32
|
-
* or `null` when the requested name is unknown or `"none"`). The two
|
|
33
|
-
* differ exactly when a config named an unrecognised profile: `profile`
|
|
34
|
-
* is `null` but `profile_requested` still names what was asked for, so a
|
|
35
|
-
* consumer can tell "no profile configured" apart from "an unknown profile
|
|
36
|
-
* was configured".
|
|
37
|
-
*
|
|
38
|
-
* @public
|
|
39
|
-
*/
|
|
40
|
-
const ContextEnvelope = Schema.Struct({
|
|
41
|
-
schema: Schema.Literal(1),
|
|
42
|
-
project_root: Schema.String,
|
|
43
|
-
bundle_root: Schema.String,
|
|
44
|
-
config_path: Schema.NullOr(Schema.String),
|
|
45
|
-
profile: Schema.NullOr(Schema.String),
|
|
46
|
-
profile_requested: Schema.NullOr(Schema.String),
|
|
47
|
-
index_path: Schema.String,
|
|
48
|
-
index_exists: Schema.Boolean,
|
|
49
|
-
actors: Schema.Struct({ agent: Schema.NullOr(Schema.String) }),
|
|
50
|
-
types: Schema.Array(ContextType),
|
|
51
|
-
tags: Schema.Array(ContextTag)
|
|
52
|
-
});
|
|
53
|
-
/**
|
|
54
|
-
* Build the envelope from the merged config. `types`/`tags` sort by `name`
|
|
55
|
-
* with plain code-unit comparison, never locale-dependent — the same rule
|
|
56
|
-
* `render/sort.ts`'s K-17 comparator and
|
|
57
|
-
* `packages/profiles/src/SoftwareProject.ts:119`'s own sort use.
|
|
58
|
-
*
|
|
59
|
-
* @public
|
|
60
|
-
*/
|
|
61
|
-
const contextEnvelope = (input) => ({
|
|
62
|
-
schema: 1,
|
|
63
|
-
project_root: input.projectRoot,
|
|
64
|
-
bundle_root: input.bundleRoot,
|
|
65
|
-
config_path: input.configPath,
|
|
66
|
-
profile: input.profile,
|
|
67
|
-
profile_requested: input.profileRequested,
|
|
68
|
-
index_path: input.indexPath,
|
|
69
|
-
index_exists: input.indexExists,
|
|
70
|
-
actors: { agent: input.config.actors?.agent ?? null },
|
|
71
|
-
types: Object.entries(input.config.types ?? {}).map(([name, decl]) => ({
|
|
72
|
-
name,
|
|
73
|
-
description: decl.description ?? null,
|
|
74
|
-
guidance: decl.guidance ?? null
|
|
75
|
-
})).toSorted((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
|
|
76
|
-
tags: Object.entries(input.config.tags ?? {}).map(([name, decl]) => ({
|
|
77
|
-
name,
|
|
78
|
-
description: decl.description ?? null
|
|
79
|
-
})).toSorted((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
|
|
80
|
-
});
|
|
81
2
|
/**
|
|
82
3
|
* The `human` format: a short header block, then one line per type and one
|
|
83
4
|
* per tag. Pure; the caller pipes each line through `Console.log`.
|
|
@@ -105,4 +26,4 @@ const humanContext = (envelope) => [
|
|
|
105
26
|
];
|
|
106
27
|
|
|
107
28
|
//#endregion
|
|
108
|
-
export {
|
|
29
|
+
export { humanContext };
|
package/render/human.js
CHANGED
package/render/sync.js
CHANGED
|
@@ -1,64 +1,4 @@
|
|
|
1
|
-
import { SkipReason } from "../sync/run.js";
|
|
2
|
-
import { Schema } from "effect";
|
|
3
|
-
|
|
4
1
|
//#region src/render/sync.ts
|
|
5
|
-
/** @public */
|
|
6
|
-
const SyncModeEnvelope = Schema.Struct({
|
|
7
|
-
selected: Schema.Boolean,
|
|
8
|
-
written: Schema.Array(Schema.String),
|
|
9
|
-
unchanged: Schema.Array(Schema.String),
|
|
10
|
-
skipped: Schema.Array(Schema.Struct({
|
|
11
|
-
id: Schema.String,
|
|
12
|
-
reason: SkipReason
|
|
13
|
-
}))
|
|
14
|
-
});
|
|
15
|
-
/**
|
|
16
|
-
* Contract §14 note 5: `schema: 1` is INFERRED, not in the design's own
|
|
17
|
-
* §2 JSON example — every other envelope in this codebase
|
|
18
|
-
* (`JsonEnvelope`, `VerifyEnvelope`, `ContextEnvelope`) opens with it, so
|
|
19
|
-
* this one does too. `exit_code` is the literal `0`: `sync` has no
|
|
20
|
-
* content tier (unlike `validate`'s `0 | 1 | 2`) and this envelope only
|
|
21
|
-
* exists on the success path — a failure produces `JsonErrorEnvelope`
|
|
22
|
-
* (K-22) instead, unchanged.
|
|
23
|
-
*
|
|
24
|
-
* @public
|
|
25
|
-
*/
|
|
26
|
-
const SyncEnvelope = Schema.Struct({
|
|
27
|
-
schema: Schema.Literal(1),
|
|
28
|
-
okfit_version: Schema.String,
|
|
29
|
-
root: Schema.String,
|
|
30
|
-
dry_run: Schema.Boolean,
|
|
31
|
-
exit_code: Schema.Literal(0),
|
|
32
|
-
generated: SyncModeEnvelope,
|
|
33
|
-
index: SyncModeEnvelope,
|
|
34
|
-
log: SyncModeEnvelope
|
|
35
|
-
});
|
|
36
|
-
const toModeEnvelope = (mode) => ({
|
|
37
|
-
selected: mode.selected,
|
|
38
|
-
written: [...mode.written],
|
|
39
|
-
unchanged: [...mode.unchanged],
|
|
40
|
-
skipped: mode.skipped.map((entry) => ({
|
|
41
|
-
id: entry.id,
|
|
42
|
-
reason: entry.reason
|
|
43
|
-
}))
|
|
44
|
-
});
|
|
45
|
-
/**
|
|
46
|
-
* `root` alone goes through `displayRoot` at the call site
|
|
47
|
-
* (`commands/sync.ts`) — `generated`/`index`/`log`'s own lists are
|
|
48
|
-
* already bundle-relative and need no cwd-relativisation (contract §9.1).
|
|
49
|
-
*
|
|
50
|
-
* @public
|
|
51
|
-
*/
|
|
52
|
-
const syncEnvelope = (input) => ({
|
|
53
|
-
schema: 1,
|
|
54
|
-
okfit_version: input.okfitVersion,
|
|
55
|
-
root: input.root,
|
|
56
|
-
dry_run: input.dryRun,
|
|
57
|
-
exit_code: 0,
|
|
58
|
-
generated: toModeEnvelope(input.result.generated),
|
|
59
|
-
index: toModeEnvelope(input.result.index),
|
|
60
|
-
log: toModeEnvelope(input.result.log)
|
|
61
|
-
});
|
|
62
2
|
/** Contract §9.2's fixed reason-sentence table, closed over `SkipReason`. */
|
|
63
3
|
const REASON_SENTENCE = {
|
|
64
4
|
untracked: "not tracked by git",
|
|
@@ -98,4 +38,4 @@ const humanSync = (result) => {
|
|
|
98
38
|
};
|
|
99
39
|
|
|
100
40
|
//#endregion
|
|
101
|
-
export {
|
|
41
|
+
export { humanSync };
|
package/render/verify.js
CHANGED
|
@@ -1,48 +1,5 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
|
|
3
1
|
//#region src/render/verify.ts
|
|
4
2
|
/**
|
|
5
|
-
* V-11's success envelope, schema 1, snake_case — the same convention as
|
|
6
|
-
* `render/json.ts`'s `JsonEnvelope`. Identical in shape for a dry run,
|
|
7
|
-
* which sets `dry_run: true` and still exits 0. There is no content tier,
|
|
8
|
-
* so `exit_code` is the literal `0`; a failure produces K-22's
|
|
9
|
-
* `JsonErrorEnvelope` instead, unchanged.
|
|
10
|
-
*
|
|
11
|
-
* @public
|
|
12
|
-
*/
|
|
13
|
-
const VerifyEnvelope = Schema.Struct({
|
|
14
|
-
schema: Schema.Literal(1),
|
|
15
|
-
okfit_version: Schema.String,
|
|
16
|
-
id: Schema.String,
|
|
17
|
-
path: Schema.String,
|
|
18
|
-
verified: Schema.Struct({
|
|
19
|
-
by: Schema.String,
|
|
20
|
-
at: Schema.String
|
|
21
|
-
}),
|
|
22
|
-
dry_run: Schema.Boolean,
|
|
23
|
-
exit_code: Schema.Literal(0)
|
|
24
|
-
});
|
|
25
|
-
/**
|
|
26
|
-
* `path` is already the display form — `displayRoot(cwd, bundle.root, path)`
|
|
27
|
-
* joined to the bundle-relative `concept.path` — because `LoadedConcept.path`
|
|
28
|
-
* alone would print `decisions/cli-exit-codes.md`, not
|
|
29
|
-
* `okf/decisions/cli-exit-codes.md` (contract §12 note 5).
|
|
30
|
-
*
|
|
31
|
-
* @public
|
|
32
|
-
*/
|
|
33
|
-
const verifyEnvelope = (input) => ({
|
|
34
|
-
schema: 1,
|
|
35
|
-
okfit_version: input.okfitVersion,
|
|
36
|
-
id: input.id,
|
|
37
|
-
path: input.path,
|
|
38
|
-
verified: {
|
|
39
|
-
by: input.by,
|
|
40
|
-
at: input.at
|
|
41
|
-
},
|
|
42
|
-
dry_run: input.dryRun,
|
|
43
|
-
exit_code: 0
|
|
44
|
-
});
|
|
45
|
-
/**
|
|
46
3
|
* Indent every line of `fragment` two spaces for display under a
|
|
47
4
|
* `would write:` header, dropping the single trailing empty line a
|
|
48
5
|
* newline-terminated fragment produces on split (I3).
|
|
@@ -69,4 +26,4 @@ const humanVerify = (input) => [
|
|
|
69
26
|
];
|
|
70
27
|
|
|
71
28
|
//#endregion
|
|
72
|
-
export {
|
|
29
|
+
export { humanVerify };
|
package/version.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { version } from "./package.js";
|
|
2
|
-
|
|
3
1
|
//#region src/version.ts
|
|
4
2
|
/**
|
|
5
|
-
* The version string reported by `okfit --version
|
|
6
|
-
*
|
|
7
|
-
* version
|
|
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
8
|
*
|
|
9
9
|
* @public
|
|
10
10
|
*/
|
|
11
|
-
const CLI_VERSION =
|
|
11
|
+
const CLI_VERSION = "0.4.0";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
export { CLI_VERSION };
|
package/config/anchor.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
//#region src/config/anchor.ts
|
|
2
|
-
/**
|
|
3
|
-
* The explicit `--config` anchor rule (C-8): the parent of `.config` "if the
|
|
4
|
-
* file sits in a `.config` directory, else the file's directory", with no
|
|
5
|
-
* exception for a custom name. Upstream's `ConfigResolver.explicitPath`
|
|
6
|
-
* reports no `dir` in its `match` by design, so this stays a hand-rolled,
|
|
7
|
-
* basename-based rule rather than reading `ConfigMatch.dir` — the one
|
|
8
|
-
* remaining path-tail string match in the CLI outside `init/scaffold.ts`
|
|
9
|
-
* (C1-4).
|
|
10
|
-
*/
|
|
11
|
-
const anchorForExplicit = (configPath, path) => {
|
|
12
|
-
const parent = path.dirname(configPath);
|
|
13
|
-
return path.basename(parent) === ".config" ? path.dirname(parent) : parent;
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* K-12's project root, as amended by K-58 and C1-2, in order:
|
|
17
|
-
*
|
|
18
|
-
* 1. `pathArg`, if given. `Argument.path` has already resolved it absolute.
|
|
19
|
-
* 2. otherwise, if `--config` was given: `anchorForExplicit` applied to that
|
|
20
|
-
* path.
|
|
21
|
-
* 3. otherwise, if a config was discovered by the `"project"` resolver
|
|
22
|
-
* (`ConfigResolver.upwardWalk`, named `"project"` in `layer.ts`) AND it
|
|
23
|
-
* reported a `dir`: that `dir` verbatim — it is already the ancestor
|
|
24
|
-
* `upwardWalk` anchored the match against (the parent of `.config` for a
|
|
25
|
-
* `.config/okfit.toml` candidate, the candidate's own directory
|
|
26
|
-
* otherwise), so no path-tail test is needed here any more.
|
|
27
|
-
* 4. otherwise `cwd` — this covers every other resolver name (`"xdg"`,
|
|
28
|
-
* `"native"`, `"system"`) as defence in depth, a `"project"` match with no
|
|
29
|
-
* `dir` (should not happen, since `upwardWalk` always reports one), and
|
|
30
|
-
* no discovery at all.
|
|
31
|
-
*
|
|
32
|
-
* The CLI never probes for `.git` (K-12), even though
|
|
33
|
-
* `ConfigResolver.gitRoot` exists.
|
|
34
|
-
*
|
|
35
|
-
* @public
|
|
36
|
-
*/
|
|
37
|
-
const resolveProjectRoot = (input) => {
|
|
38
|
-
if (input.pathArg._tag === "Some") return input.pathArg.value;
|
|
39
|
-
if (input.explicitConfigPath._tag === "Some") return anchorForExplicit(input.explicitConfigPath.value, input.path);
|
|
40
|
-
if (input.discovered._tag === "Some") {
|
|
41
|
-
const discovered = input.discovered.value;
|
|
42
|
-
if (discovered.resolver === "project" && discovered.dir !== void 0) return discovered.dir;
|
|
43
|
-
}
|
|
44
|
-
return input.cwd;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* `<project root>/<bundle.path>`, absolute (K-2). `config` is the MERGED
|
|
48
|
-
* config, so `bundle.path` is `OkfitConfig.DEFAULTS.bundle.path` (`"okf"`)
|
|
49
|
-
* unless a file or profile overrode it. The bundle root is never what
|
|
50
|
-
* `[path]` names.
|
|
51
|
-
*
|
|
52
|
-
* @public
|
|
53
|
-
*/
|
|
54
|
-
const resolveBundleRoot = (projectRoot, config, path) => path.resolve(projectRoot, config.bundle?.path ?? "okf");
|
|
55
|
-
|
|
56
|
-
//#endregion
|
|
57
|
-
export { resolveBundleRoot, resolveProjectRoot };
|
package/config/layer.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { ConfigMalformedError, ConfigPathNotFoundError } from "../errors.js";
|
|
2
|
-
import { Cause, Effect, FileSystem, Option, Result } from "effect";
|
|
3
|
-
import { AppConfig } from "@effected/app";
|
|
4
|
-
import { ConfigCodecError, ConfigResolver, ConfigValidationError, MergeStrategy, TomlCodec } from "@effected/config-file";
|
|
5
|
-
import { OkfitConfig, OkfitConfigFile } from "@okfit/core";
|
|
6
|
-
|
|
7
|
-
//#region src/config/layer.ts
|
|
8
|
-
/**
|
|
9
|
-
* The `OkfitConfigFile` layer for one invocation (K-9 to K-11, K-57, C-6),
|
|
10
|
-
* now one `AppConfig.layer(OkfitConfigFile, ...)` call in both branches; only
|
|
11
|
-
* the chain options vary:
|
|
12
|
-
*
|
|
13
|
-
* - `explicitConfigPath` is `Some`: `resolvers: [ConfigResolver.explicitPath(path)]`
|
|
14
|
-
* and `xdg: false` — K-10's "only that path" rule, so the chain is exactly
|
|
15
|
-
* one resolver and no `systemEtc` tier is added either.
|
|
16
|
-
* - `explicitConfigPath` is `None`: `resolvers: [ConfigResolver.upwardWalk({ filenames, ... })]`
|
|
17
|
-
* carrying C-1's three per-directory candidates (`.okfit.toml`,
|
|
18
|
-
* `okfit.toml`, `.config/okfit.toml`, directory-major so a child's later
|
|
19
|
-
* candidate beats a parent's earlier one — C-2's ascent to the filesystem
|
|
20
|
-
* root is `upwardWalk`'s own default with no `stopAt`), named `"project"`
|
|
21
|
-
* so `resolve.ts`/`anchor.ts` can identify it without string-matching a
|
|
22
|
-
* path tail. `systemEtc` is C-5's `/etc` tier, present unless
|
|
23
|
-
* `systemConfigDir` overrides its root (tests only — no production call
|
|
24
|
-
* site sets it). `xdg` and `native` stay on their defaults (C-4), which is
|
|
25
|
-
* what puts personal defaults at `$XDG_CONFIG_HOME/okfit/config.toml` and
|
|
26
|
-
* the native probe behind it.
|
|
27
|
-
*
|
|
28
|
-
* `filename: "config.toml"` is the same in both branches — it is the XDG/
|
|
29
|
-
* native/system tiers' filename, unrelated to the C-1 project names above.
|
|
30
|
-
* `defaultPath` is `AppConfig.layer`'s own `XdgConfig.savePath(filename)`, so
|
|
31
|
-
* `save`/`update` do not fail with `ConfigDefaultPathMissingError`; nothing
|
|
32
|
-
* here sets it explicitly any more.
|
|
33
|
-
*
|
|
34
|
-
* `discoveryCwd` is `[path]` when given, else `process.cwd()` (K-2), passed
|
|
35
|
-
* straight through as `upwardWalk`'s own `cwd` so nothing in this module
|
|
36
|
-
* reads the process.
|
|
37
|
-
*
|
|
38
|
-
* `App.layer`, `AppStore` and `AppCache` appear nowhere, so no
|
|
39
|
-
* `store.db`/`cache.db` is ever created (K-9).
|
|
40
|
-
*
|
|
41
|
-
* @public
|
|
42
|
-
*/
|
|
43
|
-
const buildConfigLayer = (options) => Option.isSome(options.explicitConfigPath) ? AppConfig.layer(OkfitConfigFile, {
|
|
44
|
-
filename: "config.toml",
|
|
45
|
-
schema: OkfitConfig,
|
|
46
|
-
codec: TomlCodec,
|
|
47
|
-
strategy: MergeStrategy.firstMatch(),
|
|
48
|
-
resolvers: [ConfigResolver.explicitPath(options.explicitConfigPath.value)],
|
|
49
|
-
xdg: false
|
|
50
|
-
}) : AppConfig.layer(OkfitConfigFile, {
|
|
51
|
-
filename: "config.toml",
|
|
52
|
-
schema: OkfitConfig,
|
|
53
|
-
codec: TomlCodec,
|
|
54
|
-
strategy: MergeStrategy.firstMatch(),
|
|
55
|
-
resolvers: [ConfigResolver.upwardWalk({
|
|
56
|
-
filenames: [
|
|
57
|
-
".okfit.toml",
|
|
58
|
-
"okfit.toml",
|
|
59
|
-
".config/okfit.toml"
|
|
60
|
-
],
|
|
61
|
-
cwd: options.discoveryCwd,
|
|
62
|
-
name: "project"
|
|
63
|
-
})],
|
|
64
|
-
systemEtc: options.systemConfigDir === void 0 ? true : { dir: options.systemConfigDir }
|
|
65
|
-
});
|
|
66
|
-
/**
|
|
67
|
-
* The K-1 pre-flight and the provide, in that order and in one place: stat
|
|
68
|
-
* `explicitConfigPath` with `FileSystem.exists` and fail with
|
|
69
|
-
* `ConfigPathNotFoundError` BEFORE `buildConfigLayer` is called at all. This
|
|
70
|
-
* is why the CLI does not use `Command.provide(cmd, (input) => layer)`,
|
|
71
|
-
* which would construct the layer first (Judge notes 9) — here the stat
|
|
72
|
-
* runs as the first step of one `Effect.gen`, and `buildConfigLayer` is only
|
|
73
|
-
* reached, and only then actually run, on the step after it.
|
|
74
|
-
*
|
|
75
|
-
* `FileSystem.FileSystem.exists` is `(path: string) => Effect.Effect<boolean, PlatformError>`
|
|
76
|
-
* (`EF/FileSystem.ts:143-145`), not infallible, so
|
|
77
|
-
* `PlatformError.PlatformError` joins the error channel here (decision 7):
|
|
78
|
-
* an unusual stat failure (e.g. a permission error on a parent directory)
|
|
79
|
-
* renders through `renderFailure`'s catch-all rule rather than being
|
|
80
|
-
* uncatchable by the type checker.
|
|
81
|
-
*
|
|
82
|
-
* K-46/K-63 fix: a `ConfigCodecError`/`ConfigValidationError` from
|
|
83
|
-
* `buildConfigLayer`'s provided layer is wrapped into `ConfigMalformedError`
|
|
84
|
-
* whenever the failing path is known, so `renderFailure` can name it:
|
|
85
|
-
*
|
|
86
|
-
* - `ConfigCodecError` now carries its own `path: string | undefined`
|
|
87
|
-
* (config-file 0.7.0, `ConfigFile.discover` re-raises with `path` attached
|
|
88
|
-
* at every site that fed the codec a path it resolved) — used when
|
|
89
|
-
* present, so a malformed file found during DISCOVERY (no `--config`) now
|
|
90
|
-
* also wraps into `ConfigMalformedError` naming the candidate that failed,
|
|
91
|
-
* closing the K-63 gap. It falls back to `explicitConfigPath` only when
|
|
92
|
-
* the library's own `path` is `undefined` and `--config` was given; only
|
|
93
|
-
* when neither is known does the cause pass through unwrapped.
|
|
94
|
-
* - `ConfigValidationError` carries its own `path: Option<string>` — used
|
|
95
|
-
* when present (either branch), falling back to `explicitConfigPath` when
|
|
96
|
-
* the library's own `path` is `None` and `--config` was given.
|
|
97
|
-
*
|
|
98
|
-
* @public
|
|
99
|
-
*/
|
|
100
|
-
const provideConfig = (options) => (effect) => Effect.gen(function* () {
|
|
101
|
-
if (Option.isSome(options.explicitConfigPath)) {
|
|
102
|
-
const path = options.explicitConfigPath.value;
|
|
103
|
-
if (!(yield* (yield* FileSystem.FileSystem).exists(path))) return yield* Effect.fail(new ConfigPathNotFoundError({ path }));
|
|
104
|
-
}
|
|
105
|
-
return yield* effect.pipe(Effect.provide(buildConfigLayer(options)), Effect.catchCause((cause) => {
|
|
106
|
-
const found = Cause.findFail(cause);
|
|
107
|
-
if (Result.isSuccess(found)) {
|
|
108
|
-
const error = found.success.error;
|
|
109
|
-
if (error instanceof ConfigCodecError) {
|
|
110
|
-
const path = error.path !== void 0 ? error.path : Option.isSome(options.explicitConfigPath) ? options.explicitConfigPath.value : void 0;
|
|
111
|
-
if (path !== void 0) return Effect.fail(new ConfigMalformedError({
|
|
112
|
-
path,
|
|
113
|
-
cause: error
|
|
114
|
-
}));
|
|
115
|
-
}
|
|
116
|
-
if (error instanceof ConfigValidationError) {
|
|
117
|
-
const path = Option.isSome(options.explicitConfigPath) ? options.explicitConfigPath : error.path;
|
|
118
|
-
if (Option.isSome(path)) return Effect.fail(new ConfigMalformedError({
|
|
119
|
-
path: path.value,
|
|
120
|
-
cause: error
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return Effect.failCause(cause);
|
|
125
|
-
}));
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
//#endregion
|
|
129
|
-
export { buildConfigLayer, provideConfig };
|
package/config/resolve.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { resolveBundleRoot, resolveProjectRoot } from "./anchor.js";
|
|
2
|
-
import { Console, Effect, Option, Path } from "effect";
|
|
3
|
-
import { OKF_SPEC_VERSION, OkfitConfig, OkfitConfigFile } from "@okfit/core";
|
|
4
|
-
import { Profiles } from "@okfit/profiles";
|
|
5
|
-
|
|
6
|
-
//#region src/config/resolve.ts
|
|
7
|
-
/**
|
|
8
|
-
* `OkfitConfig.DEFAULTS.bundle.profile` is `"software-project"` at runtime
|
|
9
|
-
* (the frozen literal always sets it), but `bundle` and `profile` are both
|
|
10
|
-
* `optionalKey` in the schema, so the type checker sees `string | undefined`
|
|
11
|
-
* two levels deep. The `?? "software-project"` fallback here is unreachable
|
|
12
|
-
* in practice; it exists only to satisfy `noUncheckedIndexedAccess`-style
|
|
13
|
-
* strictness without a non-null assertion. Formerly duplicated identically
|
|
14
|
-
* in `commands/validate.ts` and `commands/context.ts`; this is its one home
|
|
15
|
-
* now that both commands delegate to {@link resolveProjectConfig}.
|
|
16
|
-
*
|
|
17
|
-
* @public
|
|
18
|
-
*/
|
|
19
|
-
const DEFAULT_PROFILE_NAME = OkfitConfig.DEFAULTS.bundle?.profile ?? "software-project";
|
|
20
|
-
/**
|
|
21
|
-
* The config-resolution step byte-identical between `validate` and
|
|
22
|
-
* `context`'s handlers (A2 review finding): discover, pick `sources[0]`,
|
|
23
|
-
* default `{ extensions: {} }`, resolve the profile name and the K-4
|
|
24
|
-
* unknown-profile warning, merge `DEFAULTS < profile < file` (D-28), warn on
|
|
25
|
-
* an `okf_version` mismatch (K-15), then resolve the project and bundle
|
|
26
|
-
* roots (K-12). Every message string and the merge order are unchanged from
|
|
27
|
-
* the two commands' former inline copies.
|
|
28
|
-
*
|
|
29
|
-
* @public
|
|
30
|
-
*/
|
|
31
|
-
const resolveProjectConfig = (input) => Effect.gen(function* () {
|
|
32
|
-
const path = yield* Path.Path;
|
|
33
|
-
const discoveredSource = (yield* (yield* OkfitConfigFile).discover)[0];
|
|
34
|
-
const fileConfig = discoveredSource === void 0 ? { extensions: {} } : discoveredSource.value;
|
|
35
|
-
const profileName = fileConfig.bundle?.profile ?? DEFAULT_PROFILE_NAME;
|
|
36
|
-
const profile = profileName === "none" ? Option.none() : Profiles.get(profileName);
|
|
37
|
-
if (profileName !== "none" && Option.isNone(profile)) yield* Console.error(`warning: unknown profile "${profileName}"; continuing with defaults`);
|
|
38
|
-
const base = Option.match(profile, {
|
|
39
|
-
onNone: () => OkfitConfig.DEFAULTS,
|
|
40
|
-
onSome: (p) => OkfitConfig.merge(OkfitConfig.DEFAULTS, p.config)
|
|
41
|
-
});
|
|
42
|
-
const merged = OkfitConfig.merge(base, fileConfig);
|
|
43
|
-
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`);
|
|
44
|
-
const discovered = discoveredSource === void 0 ? Option.none() : Option.some({
|
|
45
|
-
path: discoveredSource.path,
|
|
46
|
-
resolver: discoveredSource.resolver,
|
|
47
|
-
...discoveredSource.match?.dir !== void 0 ? { dir: discoveredSource.match.dir } : {}
|
|
48
|
-
});
|
|
49
|
-
const projectRoot = resolveProjectRoot({
|
|
50
|
-
pathArg: input.pathArg,
|
|
51
|
-
explicitConfigPath: input.explicitConfigPath,
|
|
52
|
-
discovered,
|
|
53
|
-
cwd: input.cwd,
|
|
54
|
-
path
|
|
55
|
-
});
|
|
56
|
-
return {
|
|
57
|
-
projectRoot,
|
|
58
|
-
bundleRoot: resolveBundleRoot(projectRoot, merged, path),
|
|
59
|
-
config: merged,
|
|
60
|
-
profile,
|
|
61
|
-
profileName,
|
|
62
|
-
discovered
|
|
63
|
-
};
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
//#endregion
|
|
67
|
-
export { DEFAULT_PROFILE_NAME, resolveProjectConfig };
|
package/context/run.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { Effect, FileSystem, Path } from "effect";
|
|
2
|
-
|
|
3
|
-
//#region src/context/run.ts
|
|
4
|
-
/**
|
|
5
|
-
* The whole of `context`'s filesystem work: join `index.md` onto the bundle
|
|
6
|
-
* root and stat it. Loading a bundle and running conformance/lint checks
|
|
7
|
-
* over it never happen here — that is what makes `context` cheap enough
|
|
8
|
-
* for a hook to run on every session start and every in-bundle write
|
|
9
|
-
* (M-15).
|
|
10
|
-
*
|
|
11
|
-
* `"index.md"` is a literal here, not read from the profile's layout: the
|
|
12
|
-
* spec fixes the reserved filename regardless of profile, and a
|
|
13
|
-
* profile-less merge (`profile = "none"`) has no layout to read from at
|
|
14
|
-
* all — `context` behaves identically with and without a profile.
|
|
15
|
-
*
|
|
16
|
-
* `exists` is `(path) => Effect.Effect<boolean, PlatformError>`, so an
|
|
17
|
-
* unreadable parent directory would otherwise fail the command; here it is
|
|
18
|
-
* absorbed to `false`, because "the hook could not stat index.md" and
|
|
19
|
-
* "index.md is not there" are the same fact from a consumer's point of
|
|
20
|
-
* view.
|
|
21
|
-
*
|
|
22
|
-
* @public
|
|
23
|
-
*/
|
|
24
|
-
const runContext = (options) => Effect.gen(function* () {
|
|
25
|
-
const path = yield* Path.Path;
|
|
26
|
-
const fs = yield* FileSystem.FileSystem;
|
|
27
|
-
const indexPath = path.join(options.bundleRoot, "index.md");
|
|
28
|
-
return {
|
|
29
|
-
indexPath,
|
|
30
|
-
indexExists: yield* fs.exists(indexPath).pipe(Effect.orElseSucceed(() => false))
|
|
31
|
-
};
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
//#endregion
|
|
35
|
-
export { runContext };
|