@okfit/cli 0.1.0 → 0.2.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 +47 -4
- package/commands/context.js +78 -0
- package/commands/init.js +177 -0
- package/commands/root.js +26 -3
- package/commands/sync.js +111 -0
- package/commands/validate.js +98 -0
- package/commands/verify.js +98 -0
- package/config/anchor.js +57 -0
- package/config/layer.js +129 -0
- package/config/resolve.js +67 -0
- package/context/run.js +35 -0
- package/errors.js +173 -0
- package/index.d.ts +891 -6
- package/index.js +15 -10
- package/init/scaffold.js +174 -0
- package/internal/exit.js +14 -0
- package/internal/tty.js +13 -0
- package/package.js +5 -0
- package/package.json +17 -5
- package/render/context.js +108 -0
- package/render/exit.js +53 -0
- package/render/human.js +68 -0
- package/render/json.js +126 -0
- package/render/sort.js +46 -0
- package/render/sync.js +101 -0
- package/render/verify.js +72 -0
- package/sync/generated.js +111 -0
- package/sync/index.js +73 -0
- package/sync/log.js +127 -0
- package/sync/run.js +65 -0
- package/sync/write.js +48 -0
- package/validate/run.js +63 -0
- package/verify/locate.js +249 -0
- package/verify/run.js +106 -0
- package/verify/splice.js +75 -0
- package/version.js +14 -0
package/verify/locate.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { FrontmatterSource } from "@effected/markdown";
|
|
3
|
+
import { YamlAlias, YamlDocument, YamlMap, YamlScalar, YamlSeq } from "@effected/yaml";
|
|
4
|
+
|
|
5
|
+
//#region src/verify/locate.ts
|
|
6
|
+
/**
|
|
7
|
+
* Strip a leading BOM before anything else; the caller re-prepends `bom`
|
|
8
|
+
* verbatim on write (V-15). `FrontmatterSource.split`'s own remarks require
|
|
9
|
+
* this: "a leading BOM means offset 0 is not a fence — strip one before
|
|
10
|
+
* calling" (`FrontmatterSource.ts:123`).
|
|
11
|
+
*
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
const stripBom = (source) => source.charCodeAt(0) === 65279 ? {
|
|
15
|
+
text: source.slice(1),
|
|
16
|
+
bom: ""
|
|
17
|
+
} : {
|
|
18
|
+
text: source,
|
|
19
|
+
bom: ""
|
|
20
|
+
};
|
|
21
|
+
/** CRLF when the document's FIRST newline is CRLF, else LF (V-15). @internal */
|
|
22
|
+
const detectNewline = (source) => {
|
|
23
|
+
const index = source.indexOf("\n");
|
|
24
|
+
return index > 0 && source.charCodeAt(index - 1) === 13 ? "\r\n" : "\n";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The terminator the spliced fragment must use: `split`'s own fidelity
|
|
28
|
+
* field for the opening fence's terminator when it is LF or CRLF, else
|
|
29
|
+
* {@link detectNewline}. `FrontmatterNewline` also admits a lone `"\r"`,
|
|
30
|
+
* which no fragment may emit, so that case falls through to the scan.
|
|
31
|
+
*
|
|
32
|
+
* @internal
|
|
33
|
+
*/
|
|
34
|
+
const documentNewline = (text) => {
|
|
35
|
+
const newline = FrontmatterSource.split(text).frontmatter?.newline;
|
|
36
|
+
if (newline === "\r\n") return "\r\n";
|
|
37
|
+
if (newline === "\n") return "\n";
|
|
38
|
+
return detectNewline(text);
|
|
39
|
+
};
|
|
40
|
+
/** The run of spaces ending at `offset`, or `""` when anything else precedes it on the line. @internal */
|
|
41
|
+
const indentAt = (value, offset) => {
|
|
42
|
+
const lineStart = value.lastIndexOf("\n", offset - 1) + 1;
|
|
43
|
+
const prefix = value.slice(lineStart, offset);
|
|
44
|
+
return /^ *$/.test(prefix) ? prefix : "";
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* True when `node`, or any mapping directly inside a sequence `node`,
|
|
48
|
+
* carries a `<<` key. That is every level a legal `Verification.List` can
|
|
49
|
+
* reach; anything deeper already fails to decode as a `Verification`
|
|
50
|
+
* (contract §11 item 3).
|
|
51
|
+
*/
|
|
52
|
+
const hasMergeKey = (node) => {
|
|
53
|
+
if (node instanceof YamlMap) return node.items.some((pair) => pair.key instanceof YamlScalar && pair.key.value === "<<");
|
|
54
|
+
if (node instanceof YamlSeq) return node.items.some((item) => item instanceof YamlMap && hasMergeKey(item));
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Split `source`'s frontmatter, parse its value, find the top-level
|
|
59
|
+
* `verified` pair, and classify it (contract §3.1). Fails only when the
|
|
60
|
+
* frontmatter YAML is fatally unparseable — impossible for a concept that
|
|
61
|
+
* reached `bundle.concepts`, but typed rather than assumed.
|
|
62
|
+
*
|
|
63
|
+
* Offset translation is `valueStart = 3 + newline.length` — `"---".length`
|
|
64
|
+
* plus the opening fence's own terminator, taken from `split`'s fidelity
|
|
65
|
+
* field rather than re-derived with `indexOf` (contract §12 note 9). Every
|
|
66
|
+
* whole-file offset below is `valueStart + <yaml offset>`.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
*/
|
|
70
|
+
const locate = Effect.fn("okfit/verify/locate")(function* (source) {
|
|
71
|
+
const block = FrontmatterSource.split(source).frontmatter;
|
|
72
|
+
if (block === void 0) return {
|
|
73
|
+
_tag: "unsupported",
|
|
74
|
+
shape: "no-frontmatter"
|
|
75
|
+
};
|
|
76
|
+
const value = block.value;
|
|
77
|
+
const valueStart = 3 + (block.newline ?? "\n").length;
|
|
78
|
+
const contents = (yield* YamlDocument.parse(value)).contents;
|
|
79
|
+
if (!(contents instanceof YamlMap)) return {
|
|
80
|
+
_tag: "unsupported",
|
|
81
|
+
shape: "not-a-mapping"
|
|
82
|
+
};
|
|
83
|
+
const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "verified");
|
|
84
|
+
if (pair === void 0) return {
|
|
85
|
+
_tag: "absent",
|
|
86
|
+
insertAt: valueStart + value.length
|
|
87
|
+
};
|
|
88
|
+
const key = pair.key;
|
|
89
|
+
const node = pair.value;
|
|
90
|
+
if (node === null) return {
|
|
91
|
+
_tag: "unsupported",
|
|
92
|
+
shape: "empty"
|
|
93
|
+
};
|
|
94
|
+
if (node instanceof YamlAlias) return {
|
|
95
|
+
_tag: "unsupported",
|
|
96
|
+
shape: "alias"
|
|
97
|
+
};
|
|
98
|
+
if (hasMergeKey(node)) return {
|
|
99
|
+
_tag: "unsupported",
|
|
100
|
+
shape: "merge-key"
|
|
101
|
+
};
|
|
102
|
+
if (node instanceof YamlScalar) return {
|
|
103
|
+
_tag: "unsupported",
|
|
104
|
+
shape: "scalar"
|
|
105
|
+
};
|
|
106
|
+
if (node instanceof YamlSeq) {
|
|
107
|
+
if (node.style === "flow") return {
|
|
108
|
+
_tag: "flowSeq",
|
|
109
|
+
insertAt: valueStart + node.offset + node.length - 1,
|
|
110
|
+
empty: node.items.length === 0
|
|
111
|
+
};
|
|
112
|
+
const last = node.items[node.items.length - 1];
|
|
113
|
+
if (last === void 0) return {
|
|
114
|
+
_tag: "unsupported",
|
|
115
|
+
shape: "empty"
|
|
116
|
+
};
|
|
117
|
+
const end = last.offset + last.length;
|
|
118
|
+
return {
|
|
119
|
+
_tag: "blockSeq",
|
|
120
|
+
insertAt: valueStart + end,
|
|
121
|
+
afterNewline: value[end - 1] === "\n",
|
|
122
|
+
indent: indentAt(value, node.offset),
|
|
123
|
+
lastItemStyle: last instanceof YamlMap && last.style === "flow" ? "flow" : "block"
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (node instanceof YamlMap) {
|
|
127
|
+
const start = valueStart + key.offset + key.length + 1;
|
|
128
|
+
const rawEnd = node.offset + node.length;
|
|
129
|
+
const raw = value.slice(node.offset, rawEnd);
|
|
130
|
+
const original = raw.replace(/(?:\r\n|\n|\r)$/, "");
|
|
131
|
+
return {
|
|
132
|
+
_tag: "bareMapping",
|
|
133
|
+
style: node.style === "flow" ? "flow" : "block",
|
|
134
|
+
start,
|
|
135
|
+
end: valueStart + rawEnd,
|
|
136
|
+
indent: `${indentAt(value, key.offset)} `,
|
|
137
|
+
original,
|
|
138
|
+
endsWithNewline: original.length !== raw.length
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
_tag: "unsupported",
|
|
143
|
+
shape: "not-a-mapping"
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
/**
|
|
147
|
+
* Locate the `at` key inside a concept's top-level `generated` BLOCK
|
|
148
|
+
* mapping. Callers MUST have already confirmed `generated` exists on the
|
|
149
|
+
* decoded concept (`generated-missing`, S-3/design §3 rule 5, is the
|
|
150
|
+
* caller's check against `concept.frontmatter.generated`, never this
|
|
151
|
+
* function's) — matches how `locate` already re-parses independently of
|
|
152
|
+
* whatever `Bundle.load` retained, for `verified`. The insert style is
|
|
153
|
+
* one new newline-terminated line after the last key's line of the
|
|
154
|
+
* mapping, at that mapping's indent, using the document newline (S-2,
|
|
155
|
+
* confirmed as already correct in FW's own proposal — contract §14 note
|
|
156
|
+
* 1 — with no correction needed against this file's `locate` precedent).
|
|
157
|
+
*
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
const locateGenerated = Effect.fn("okfit/verify/locateGenerated")(function* (source) {
|
|
161
|
+
const block = FrontmatterSource.split(source).frontmatter;
|
|
162
|
+
if (block === void 0) return {
|
|
163
|
+
_tag: "unsupported",
|
|
164
|
+
shape: "no-frontmatter"
|
|
165
|
+
};
|
|
166
|
+
const value = block.value;
|
|
167
|
+
const valueStart = 3 + (block.newline ?? "\n").length;
|
|
168
|
+
const contents = (yield* YamlDocument.parse(value)).contents;
|
|
169
|
+
if (!(contents instanceof YamlMap)) return {
|
|
170
|
+
_tag: "unsupported",
|
|
171
|
+
shape: "not-a-mapping"
|
|
172
|
+
};
|
|
173
|
+
const pair = contents.items.find((item) => item.key instanceof YamlScalar && item.key.value === "generated");
|
|
174
|
+
if (pair === void 0) return {
|
|
175
|
+
_tag: "unsupported",
|
|
176
|
+
shape: "no-generated-key"
|
|
177
|
+
};
|
|
178
|
+
const node = pair.value;
|
|
179
|
+
if (node === null) return {
|
|
180
|
+
_tag: "unsupported",
|
|
181
|
+
shape: "empty"
|
|
182
|
+
};
|
|
183
|
+
if (node instanceof YamlAlias) return {
|
|
184
|
+
_tag: "unsupported",
|
|
185
|
+
shape: "alias"
|
|
186
|
+
};
|
|
187
|
+
if (hasMergeKey(node)) return {
|
|
188
|
+
_tag: "unsupported",
|
|
189
|
+
shape: "merge-key"
|
|
190
|
+
};
|
|
191
|
+
if (node instanceof YamlScalar) return {
|
|
192
|
+
_tag: "unsupported",
|
|
193
|
+
shape: "scalar"
|
|
194
|
+
};
|
|
195
|
+
if (node instanceof YamlSeq) return {
|
|
196
|
+
_tag: "unsupported",
|
|
197
|
+
shape: "sequence"
|
|
198
|
+
};
|
|
199
|
+
if (!(node instanceof YamlMap)) return {
|
|
200
|
+
_tag: "unsupported",
|
|
201
|
+
shape: "not-a-mapping"
|
|
202
|
+
};
|
|
203
|
+
if (node.style === "flow") return {
|
|
204
|
+
_tag: "unsupported",
|
|
205
|
+
shape: "flow-mapping"
|
|
206
|
+
};
|
|
207
|
+
const indent = indentAt(value, node.offset);
|
|
208
|
+
const atPair = node.items.find((item) => item.key instanceof YamlScalar && item.key.value === "at");
|
|
209
|
+
if (atPair === void 0) {
|
|
210
|
+
const last = node.items[node.items.length - 1];
|
|
211
|
+
if (last === void 0) return {
|
|
212
|
+
_tag: "unsupported",
|
|
213
|
+
shape: "empty"
|
|
214
|
+
};
|
|
215
|
+
const lastEnd = last.value !== null ? last.value.offset + last.value.length : last.key.offset + last.key.length;
|
|
216
|
+
const nl = value.indexOf("\n", lastEnd);
|
|
217
|
+
return {
|
|
218
|
+
_tag: "insertAfterLastKey",
|
|
219
|
+
insertAt: valueStart + (nl === -1 ? value.length : nl + 1),
|
|
220
|
+
indent
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const atValue = atPair.value;
|
|
224
|
+
if (atValue === null) return {
|
|
225
|
+
_tag: "unsupported",
|
|
226
|
+
shape: "at-empty"
|
|
227
|
+
};
|
|
228
|
+
if (atValue instanceof YamlAlias) return {
|
|
229
|
+
_tag: "unsupported",
|
|
230
|
+
shape: "alias"
|
|
231
|
+
};
|
|
232
|
+
if (!(atValue instanceof YamlScalar)) return {
|
|
233
|
+
_tag: "unsupported",
|
|
234
|
+
shape: "at-not-scalar"
|
|
235
|
+
};
|
|
236
|
+
if (atValue.style === "block-literal" || atValue.style === "block-folded") return {
|
|
237
|
+
_tag: "unsupported",
|
|
238
|
+
shape: "at-block-scalar"
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
_tag: "replaceScalar",
|
|
242
|
+
start: valueStart + atValue.offset,
|
|
243
|
+
end: valueStart + atValue.offset + atValue.length,
|
|
244
|
+
quote: atValue.style
|
|
245
|
+
};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
//#endregion
|
|
249
|
+
export { detectNewline, documentNewline, indentAt, locate, locateGenerated, stripBom };
|
package/verify/run.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { VerifyConceptNotFoundError, VerifyUnsupportedFrontmatterError } from "../errors.js";
|
|
2
|
+
import { documentNewline, locate, stripBom } from "./locate.js";
|
|
3
|
+
import { splice } from "./splice.js";
|
|
4
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
5
|
+
import { Bundle, ConceptId, Timestamp } from "@okfit/core";
|
|
6
|
+
import { Derivation } from "@okfit/profiles";
|
|
7
|
+
import { MarkdownEdit } from "@effected/markdown";
|
|
8
|
+
|
|
9
|
+
//#region src/verify/run.ts
|
|
10
|
+
/**
|
|
11
|
+
* The only four diagnostic codes that can co-occur with a MISSING
|
|
12
|
+
* `concepts` entry (V-9, contract §12 note 10): `Bundle.ts:143/164/175/186`
|
|
13
|
+
* emit the three frontmatter codes and return without a concept, and
|
|
14
|
+
* `internal/frontmatter.ts:73` emits `type-missing` for `decodeConcept`'s
|
|
15
|
+
* only failure branch. Every other code — `family-invalid`,
|
|
16
|
+
* `computation-runtime-missing`, `legacy-timestamp` — is pushed as an issue
|
|
17
|
+
* while `decodeConcept` still returns a concept, so the concept IS in the
|
|
18
|
+
* map (`internal/conceptDecode.ts:84-134`).
|
|
19
|
+
*/
|
|
20
|
+
const UNDECODABLE_CODES = /* @__PURE__ */ new Set([
|
|
21
|
+
"frontmatter-unparseable",
|
|
22
|
+
"frontmatter-unclosed",
|
|
23
|
+
"frontmatter-missing",
|
|
24
|
+
"type-missing"
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* Contract §2.3 steps 6–15. Loads the bundle, resolves the id, resolves the
|
|
28
|
+
* human actor from git, splices exactly one entry into the concept's
|
|
29
|
+
* frontmatter and writes it back atomically.
|
|
30
|
+
*
|
|
31
|
+
* Nothing here reads `status`, `stale_after`, or a type's
|
|
32
|
+
* `require_verified`: `verify` is mechanical, not config-aware (V-4, V-5).
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
const runVerify = Effect.fn("okfit/verify/runVerify")(function* (options) {
|
|
37
|
+
const fs = yield* FileSystem.FileSystem;
|
|
38
|
+
const path = yield* Path.Path;
|
|
39
|
+
const bundle = yield* Bundle.load({ root: options.bundleRoot });
|
|
40
|
+
const normalized = ConceptId.normalize(options.id);
|
|
41
|
+
if (Option.isNone(normalized)) return yield* new VerifyConceptNotFoundError({
|
|
42
|
+
id: options.id,
|
|
43
|
+
root: bundle.root,
|
|
44
|
+
reason: "not-a-concept"
|
|
45
|
+
});
|
|
46
|
+
const id = normalized.value;
|
|
47
|
+
const conceptFile = ConceptId.toPath(id);
|
|
48
|
+
if (ConceptId.isReservedFile(conceptFile)) return yield* new VerifyConceptNotFoundError({
|
|
49
|
+
id,
|
|
50
|
+
root: bundle.root,
|
|
51
|
+
reason: "reserved"
|
|
52
|
+
});
|
|
53
|
+
const concept = bundle.concepts.get(id);
|
|
54
|
+
if (concept === void 0) {
|
|
55
|
+
const diagnostic = bundle.diagnostics.find((entry) => entry.file === conceptFile && UNDECODABLE_CODES.has(entry.code));
|
|
56
|
+
return yield* new VerifyConceptNotFoundError({
|
|
57
|
+
id,
|
|
58
|
+
root: bundle.root,
|
|
59
|
+
...diagnostic === void 0 ? { reason: "not-a-concept" } : {
|
|
60
|
+
reason: "undecodable",
|
|
61
|
+
diagnosticCode: diagnostic.code
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const actor = yield* Derivation.generatedBy({
|
|
66
|
+
writer: "human",
|
|
67
|
+
cwd: options.projectRoot,
|
|
68
|
+
config: options.config
|
|
69
|
+
});
|
|
70
|
+
const absolutePath = path.join(bundle.root, concept.path);
|
|
71
|
+
const source = yield* fs.readFileString(absolutePath);
|
|
72
|
+
const { text, bom } = stripBom(source);
|
|
73
|
+
const located = yield* locate(text);
|
|
74
|
+
if (located._tag === "unsupported") return yield* new VerifyUnsupportedFrontmatterError({
|
|
75
|
+
id,
|
|
76
|
+
shape: located.shape
|
|
77
|
+
});
|
|
78
|
+
const at = Schema.encodeSync(Timestamp)(options.at);
|
|
79
|
+
const priorAt = (concept.frontmatter.verified ?? []).filter((entry) => entry.by === actor).map((entry) => Schema.encodeSync(Timestamp)(entry.at));
|
|
80
|
+
const edit = splice(located, {
|
|
81
|
+
by: actor,
|
|
82
|
+
at
|
|
83
|
+
}, documentNewline(text));
|
|
84
|
+
const finalText = bom + MarkdownEdit.applyAll(text, [edit]);
|
|
85
|
+
if (!options.dryRun) {
|
|
86
|
+
const target = yield* fs.realPath(absolutePath);
|
|
87
|
+
const tempPath = `${target}.okfit-verify.tmp`;
|
|
88
|
+
const original = yield* fs.stat(target);
|
|
89
|
+
yield* fs.writeFileString(tempPath, finalText);
|
|
90
|
+
yield* fs.chmod(tempPath, original.mode);
|
|
91
|
+
yield* fs.rename(tempPath, target).pipe(Effect.onError(() => fs.remove(tempPath, { force: true }).pipe(Effect.ignore)));
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
bundleRoot: bundle.root,
|
|
96
|
+
conceptPath: concept.path,
|
|
97
|
+
by: actor,
|
|
98
|
+
at,
|
|
99
|
+
priorAt,
|
|
100
|
+
dryRun: options.dryRun,
|
|
101
|
+
fragment: edit.content
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { runVerify };
|
package/verify/splice.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { MarkdownEdit } from "@effected/markdown";
|
|
2
|
+
|
|
3
|
+
//#region src/verify/splice.ts
|
|
4
|
+
/** Prefix two spaces to every line but the first, joining with `newline` (V-13). */
|
|
5
|
+
const reindent = (original, newline) => original.split(/\r\n|\n|\r/).map((line, index) => index === 0 ? line : ` ${line}`).join(newline);
|
|
6
|
+
/**
|
|
7
|
+
* Build the one edit for `target` (contract §3.2). Never calls a YAML
|
|
8
|
+
* stringifier: the only syntax emitted is `- `, `{`, `}`, `: `, `,`,
|
|
9
|
+
* spaces and `newline`.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
const splice = (target, entry, newline) => {
|
|
14
|
+
const flow = `{ by: ${entry.by}, at: ${entry.at} }`;
|
|
15
|
+
const block = (indent) => `${indent}- by: ${entry.by}${newline}${indent} at: ${entry.at}`;
|
|
16
|
+
switch (target._tag) {
|
|
17
|
+
case "absent": return MarkdownEdit.make({
|
|
18
|
+
offset: target.insertAt,
|
|
19
|
+
length: 0,
|
|
20
|
+
content: `verified:${newline} - by: ${entry.by}${newline} at: ${entry.at}${newline}`
|
|
21
|
+
});
|
|
22
|
+
case "blockSeq": {
|
|
23
|
+
const fragment = target.lastItemStyle === "flow" ? `${target.indent}- ${flow}` : block(target.indent);
|
|
24
|
+
return MarkdownEdit.make({
|
|
25
|
+
offset: target.insertAt,
|
|
26
|
+
length: 0,
|
|
27
|
+
content: target.afterNewline ? `${fragment}${newline}` : `${newline}${fragment}`
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
case "flowSeq": return MarkdownEdit.make({
|
|
31
|
+
offset: target.insertAt,
|
|
32
|
+
length: 0,
|
|
33
|
+
content: target.empty ? flow : `, ${flow}`
|
|
34
|
+
});
|
|
35
|
+
case "bareMapping": return target.style === "flow" ? MarkdownEdit.make({
|
|
36
|
+
offset: target.start,
|
|
37
|
+
length: target.end - target.start,
|
|
38
|
+
content: `${newline}${target.indent}- ${target.original}${newline}${target.indent}- ${flow}`
|
|
39
|
+
}) : MarkdownEdit.make({
|
|
40
|
+
offset: target.start,
|
|
41
|
+
length: target.end - target.start,
|
|
42
|
+
content: `${newline}${target.indent}- ${reindent(target.original, newline)}${newline}${block(target.indent)}${newline}`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Build the one edit for `target` (contract §6.1). Never calls a YAML
|
|
48
|
+
* stringifier: the only syntax emitted is the indent, `at: `, the
|
|
49
|
+
* preserved quote character (if any), and `newline`. `encodedAt` is
|
|
50
|
+
* already `Schema.encodeSync(Timestamp)`'d by the caller (S-1: splice,
|
|
51
|
+
* never `YamlFormat.modify` — it drops quote style and re-normalises line
|
|
52
|
+
* endings, V-12/V-15 violations `sync/generated.ts` cannot afford).
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
const spliceGenerated = (target, encodedAt, newline) => {
|
|
57
|
+
switch (target._tag) {
|
|
58
|
+
case "insertAfterLastKey": return MarkdownEdit.make({
|
|
59
|
+
offset: target.insertAt,
|
|
60
|
+
length: 0,
|
|
61
|
+
content: `${target.indent}at: ${encodedAt}${newline}`
|
|
62
|
+
});
|
|
63
|
+
case "replaceScalar": {
|
|
64
|
+
const quoted = target.quote === "single-quoted" ? `'${encodedAt}'` : target.quote === "double-quoted" ? `"${encodedAt}"` : encodedAt;
|
|
65
|
+
return MarkdownEdit.make({
|
|
66
|
+
offset: target.start,
|
|
67
|
+
length: target.end - target.start,
|
|
68
|
+
content: quoted
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { splice, spliceGenerated };
|
package/version.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { version } from "./package.js";
|
|
2
|
+
|
|
3
|
+
//#region src/version.ts
|
|
4
|
+
/**
|
|
5
|
+
* The version string reported by `okfit --version`, read from the package's
|
|
6
|
+
* own manifest (K-32) so a release can never desync from the printed
|
|
7
|
+
* version. Consumed by `bin.ts` only.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
const CLI_VERSION = version;
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { CLI_VERSION };
|