toolcraft 0.0.50 → 0.0.52
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/node_modules/@poe-code/agent-defs/README.md +35 -0
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/mock.js +9 -2
- package/node_modules/@poe-code/agent-human-in-loop/dist/providers/osascript-script.js +3 -1
- package/node_modules/@poe-code/agent-human-in-loop/dist/request-approval.js +44 -2
- package/node_modules/@poe-code/agent-human-in-loop/package.json +0 -1
- package/node_modules/@poe-code/agent-mcp-config/README.md +54 -0
- package/node_modules/@poe-code/agent-mcp-config/package.json +0 -2
- package/node_modules/@poe-code/config-mutations/README.md +55 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +1 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +65 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +4 -11
- package/node_modules/@poe-code/config-mutations/package.json +0 -1
- package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +17 -0
- package/node_modules/@poe-code/frontmatter/dist/fences.js +33 -5
- package/node_modules/@poe-code/frontmatter/dist/parse.js +86 -14
- package/node_modules/@poe-code/frontmatter/dist/stringify.js +13 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +14 -1
- package/node_modules/@poe-code/process-runner/dist/docker/build-context.d.ts +5 -0
- package/node_modules/@poe-code/process-runner/dist/docker/build-context.js +37 -0
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +29 -29
- package/node_modules/@poe-code/process-runner/dist/index.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/index.js +1 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +49 -3
- package/node_modules/@poe-code/process-runner/package.json +8 -1
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +7 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues.js +34 -7
- package/node_modules/@poe-code/task-list/dist/backends/markdown-dir.js +75 -19
- package/node_modules/@poe-code/task-list/dist/backends/utils.d.ts +2 -0
- package/node_modules/@poe-code/task-list/dist/backends/utils.js +23 -2
- package/node_modules/@poe-code/task-list/dist/backends/yaml-file.js +16 -12
- package/node_modules/@poe-code/task-list/dist/state-machine.js +9 -0
- package/node_modules/@poe-code/task-list/package.json +0 -1
- package/node_modules/auth-store/dist/create-secret-store.js +4 -3
- package/node_modules/auth-store/dist/encrypted-file-store.js +49 -2
- package/node_modules/auth-store/dist/keychain-store.js +11 -4
- package/node_modules/auth-store/package.json +0 -1
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +69 -12
- package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +100 -68
- package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +19 -18
- package/node_modules/mcp-oauth/dist/client/token-endpoint.js +37 -31
- package/node_modules/mcp-oauth/package.json +0 -1
- package/node_modules/tiny-mcp-client/dist/internal.js +96 -10
- package/node_modules/tiny-mcp-client/package.json +0 -3
- package/node_modules/tiny-mcp-client/src/internal.ts +120 -18
- package/node_modules/tiny-mcp-client/src/transports.test.ts +231 -2
- package/node_modules/toolcraft-design/package.json +0 -1
- package/package.json +3 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @poe-code/agent-defs
|
|
2
|
+
|
|
3
|
+
Shared catalog of supported coding-agent definitions.
|
|
4
|
+
|
|
5
|
+
This package owns the declarative agent metadata used by CLI, SDK, spawn,
|
|
6
|
+
configuration, and UI surfaces: stable ids, labels, aliases, config paths,
|
|
7
|
+
binary names, API-shape support, OTEL capture wiring, and brand colors.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { allAgents, normalizeAgentId, parseAgentSpecifier } from "@poe-code/agent-defs";
|
|
13
|
+
|
|
14
|
+
const specifier = parseAgentSpecifier("claude:sonnet");
|
|
15
|
+
const normalized = normalizeAgentId(specifier.agent);
|
|
16
|
+
const codex = allAgents.find((agent) => agent.id === "codex");
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Public API
|
|
20
|
+
|
|
21
|
+
- `allAgents`: frozen list of built-in `AgentDefinition` records.
|
|
22
|
+
- `resolveAgentId(input)`: resolves ids, names, and aliases to a stable agent id.
|
|
23
|
+
- `parseAgentSpecifier(input)`: parses `agent` or `agent:model` input.
|
|
24
|
+
- `formatAgentSpecifier(specifier)`: formats an agent specifier.
|
|
25
|
+
- `normalizeAgentId(input)`: normalizes the agent part through the registry.
|
|
26
|
+
- Agent definition exports such as `codexAgent`, `claudeCodeAgent`, and `geminiCliAgent`.
|
|
27
|
+
|
|
28
|
+
## Config Options
|
|
29
|
+
|
|
30
|
+
This package does not load external config. Agent definitions are declared in
|
|
31
|
+
source and exposed as immutable data.
|
|
32
|
+
|
|
33
|
+
## Environment Variables
|
|
34
|
+
|
|
35
|
+
This package does not read or expose environment variables.
|
|
@@ -3,9 +3,16 @@ export function mockProvider(answer) {
|
|
|
3
3
|
id: "mock",
|
|
4
4
|
async requestApproval(_request) {
|
|
5
5
|
if (typeof answer === "function") {
|
|
6
|
-
return await answer();
|
|
6
|
+
return cloneApprovalResult(await answer());
|
|
7
7
|
}
|
|
8
|
-
return answer;
|
|
8
|
+
return cloneApprovalResult(answer);
|
|
9
9
|
},
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
|
+
function cloneApprovalResult(result) {
|
|
13
|
+
return result.outcome === "approved"
|
|
14
|
+
? { outcome: "approved" }
|
|
15
|
+
: result.reason === undefined
|
|
16
|
+
? { outcome: "declined" }
|
|
17
|
+
: { outcome: "declined", reason: result.reason };
|
|
18
|
+
}
|
|
@@ -1,4 +1,46 @@
|
|
|
1
|
-
export function requestApproval(args) {
|
|
1
|
+
export async function requestApproval(args) {
|
|
2
2
|
const { provider, ...request } = args;
|
|
3
|
-
return provider.requestApproval(request);
|
|
3
|
+
return normalizeApprovalResult(await provider.requestApproval(normalizeApprovalRequest(request)));
|
|
4
|
+
}
|
|
5
|
+
function normalizeApprovalRequest(request) {
|
|
6
|
+
if (typeof request.message !== "string" || request.message.trim().length === 0) {
|
|
7
|
+
throw new Error("Approval request message must not be blank");
|
|
8
|
+
}
|
|
9
|
+
if (request.declineInputPrompt !== undefined &&
|
|
10
|
+
typeof request.declineInputPrompt !== "string") {
|
|
11
|
+
throw new Error("Approval request declineInputPrompt must be a string");
|
|
12
|
+
}
|
|
13
|
+
return request.declineInputPrompt === undefined
|
|
14
|
+
? { message: request.message }
|
|
15
|
+
: {
|
|
16
|
+
message: request.message,
|
|
17
|
+
declineInputPrompt: request.declineInputPrompt,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function normalizeApprovalResult(result) {
|
|
21
|
+
if (!isObjectRecord(result)) {
|
|
22
|
+
throw new Error("Approval provider returned an invalid result");
|
|
23
|
+
}
|
|
24
|
+
const outcome = getOwnEntry(result, "outcome");
|
|
25
|
+
if (outcome === "approved") {
|
|
26
|
+
return { outcome: "approved" };
|
|
27
|
+
}
|
|
28
|
+
if (outcome === "declined") {
|
|
29
|
+
const reason = getOwnEntry(result, "reason");
|
|
30
|
+
if (reason === undefined) {
|
|
31
|
+
return { outcome: "declined" };
|
|
32
|
+
}
|
|
33
|
+
if (typeof reason === "string") {
|
|
34
|
+
return { outcome: "declined", reason };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new Error("Approval provider returned an invalid result");
|
|
38
|
+
}
|
|
39
|
+
function isObjectRecord(value) {
|
|
40
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
function getOwnEntry(record, key) {
|
|
43
|
+
return Object.prototype.hasOwnProperty.call(record, key)
|
|
44
|
+
? record[key]
|
|
45
|
+
: undefined;
|
|
4
46
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @poe-code/agent-mcp-config
|
|
2
|
+
|
|
3
|
+
MCP configuration writer for supported local coding agents.
|
|
4
|
+
|
|
5
|
+
This package maps a normalized MCP server entry to each agent's native config
|
|
6
|
+
file, config key, file format, and shape. It is used by configure and
|
|
7
|
+
unconfigure flows so caller code can stay declarative.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { configure, unconfigure, supportedAgents } from "@poe-code/agent-mcp-config";
|
|
13
|
+
|
|
14
|
+
await configure(
|
|
15
|
+
"codex",
|
|
16
|
+
{
|
|
17
|
+
name: "workspace",
|
|
18
|
+
config: { transport: "stdio", command: "node", args: ["./mcp.js"] }
|
|
19
|
+
},
|
|
20
|
+
{ fs, homeDir: "/Users/me", platform: "darwin" }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
console.log(supportedAgents);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Public API
|
|
27
|
+
|
|
28
|
+
- `configure(agentId, server, options)`: adds or replaces a matching MCP server entry.
|
|
29
|
+
- `unconfigure(agentId, server, options)`: removes an MCP server entry.
|
|
30
|
+
- `supportedAgents`: agent ids with config-file MCP support.
|
|
31
|
+
- `isSupported(agentId)`: returns whether an agent has MCP config support.
|
|
32
|
+
- `resolveAgentSupport(input)`: resolves aliases and returns support metadata.
|
|
33
|
+
- `UnsupportedAgentError`: thrown for unsupported known agents.
|
|
34
|
+
|
|
35
|
+
## Config Options
|
|
36
|
+
|
|
37
|
+
`ApplyOptions` controls how mutations are applied:
|
|
38
|
+
|
|
39
|
+
| Option | Type | Description |
|
|
40
|
+
| ------ | ---- | ----------- |
|
|
41
|
+
| `fs` | `FileSystem` | Filesystem adapter from `@poe-code/config-mutations`. |
|
|
42
|
+
| `homeDir` | `string` | Home directory used to resolve `~` in agent config paths. |
|
|
43
|
+
| `platform` | `"darwin" \| "linux" \| "win32"` | Selects platform-specific config paths. |
|
|
44
|
+
| `dryRun` | `boolean` | Computes mutations without writing files. |
|
|
45
|
+
| `observers` | `MutationObservers` | Receives mutation lifecycle events. |
|
|
46
|
+
|
|
47
|
+
The package's built-in agent config table declares `configFile`, `configKey`,
|
|
48
|
+
`format`, `shape`, and optional `mcpOutputFormat` per supported agent.
|
|
49
|
+
|
|
50
|
+
## Environment Variables
|
|
51
|
+
|
|
52
|
+
This package does not read or expose environment variables. Server-specific
|
|
53
|
+
environment variables may be written into target agent config files when they
|
|
54
|
+
are present in an `McpStdioServer` entry.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @poe-code/config-mutations
|
|
2
|
+
|
|
3
|
+
Composable file and config mutation engine.
|
|
4
|
+
|
|
5
|
+
This package applies ordered filesystem, JSON, TOML, YAML, and template
|
|
6
|
+
mutations with dry-run support and observer hooks. Callers inject the filesystem
|
|
7
|
+
so tests can use in-memory adapters and production flows can use
|
|
8
|
+
`node:fs/promises`.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { configMutation, fileMutation, runMutations } from "@poe-code/config-mutations";
|
|
14
|
+
|
|
15
|
+
await runMutations(
|
|
16
|
+
[
|
|
17
|
+
fileMutation.ensureDirectory({ path: "~/.config/example" }),
|
|
18
|
+
configMutation.merge({
|
|
19
|
+
target: "~/.config/example/config.json",
|
|
20
|
+
format: "json",
|
|
21
|
+
value: { enabled: true }
|
|
22
|
+
})
|
|
23
|
+
],
|
|
24
|
+
{ fs, homeDir: "/Users/me" }
|
|
25
|
+
);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Public API
|
|
29
|
+
|
|
30
|
+
- `runMutations(mutations, context)`: applies mutations in order.
|
|
31
|
+
- `configMutation`: builders for config merge, prune, and transform mutations.
|
|
32
|
+
- `fileMutation`: builders for directory, file, backup, restore, and mode mutations.
|
|
33
|
+
- `templateMutation`: builders for template writes and template-backed config merges.
|
|
34
|
+
- `renderTemplate(template, variables)`: renders template variables.
|
|
35
|
+
- Filesystem helpers: `isNotFound`, `readFileIfExists`, `pathExists`, and `createTimestamp`.
|
|
36
|
+
|
|
37
|
+
## Config Options
|
|
38
|
+
|
|
39
|
+
`MutationContext` controls execution:
|
|
40
|
+
|
|
41
|
+
| Option | Type | Description |
|
|
42
|
+
| ------ | ---- | ----------- |
|
|
43
|
+
| `fs` | `FileSystem` | Required filesystem adapter. |
|
|
44
|
+
| `homeDir` | `string` | Required home directory for `~` expansion. |
|
|
45
|
+
| `dryRun` | `boolean` | Reports changes without writing them. |
|
|
46
|
+
| `observers` | `MutationObservers` | Receives mutation lifecycle events. |
|
|
47
|
+
| `templates` | `TemplateLoader` | Loads templates referenced by template mutations. |
|
|
48
|
+
| `pathMapper` | `PathMapper` | Redirects target directories for isolated config flows. |
|
|
49
|
+
|
|
50
|
+
Mutation builders also expose per-mutation options such as target path, format,
|
|
51
|
+
label, force removal, backup behavior, template id, and transform callbacks.
|
|
52
|
+
|
|
53
|
+
## Environment Variables
|
|
54
|
+
|
|
55
|
+
This package does not read or expose environment variables.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Mutation, MutationContext, MutationOutcome, MutationDetails, MutationOptions } from "../types.js";
|
|
2
|
+
export declare function resolveMutationDetails(mutation: Mutation, context: MutationContext, options: MutationOptions): MutationDetails;
|
|
2
3
|
export declare function applyMutation(mutation: Mutation, context: MutationContext, options: MutationOptions): Promise<{
|
|
3
4
|
outcome: MutationOutcome;
|
|
4
5
|
details: MutationDetails;
|
|
@@ -126,6 +126,58 @@ function describeMutation(kind, targetPath) {
|
|
|
126
126
|
return "Operation";
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
function mutationTargetPath(mutation, options) {
|
|
130
|
+
switch (mutation.kind) {
|
|
131
|
+
case "ensureDirectory":
|
|
132
|
+
case "removeDirectory":
|
|
133
|
+
return resolveValue(mutation.path, options);
|
|
134
|
+
case "removeFile":
|
|
135
|
+
case "chmod":
|
|
136
|
+
case "backup":
|
|
137
|
+
case "restoreBackup":
|
|
138
|
+
case "configMerge":
|
|
139
|
+
case "configPrune":
|
|
140
|
+
case "configTransform":
|
|
141
|
+
case "templateWrite":
|
|
142
|
+
case "templateMergeToml":
|
|
143
|
+
case "templateMergeJson":
|
|
144
|
+
return resolveValue(mutation.target, options);
|
|
145
|
+
default:
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export function resolveMutationDetails(mutation, context, options) {
|
|
150
|
+
try {
|
|
151
|
+
const rawTarget = mutationTargetPath(mutation, options);
|
|
152
|
+
if (rawTarget === undefined) {
|
|
153
|
+
return {
|
|
154
|
+
kind: mutation.kind,
|
|
155
|
+
label: mutation.label ?? mutation.kind
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const targetPath = resolvePath(rawTarget, context.homeDir, context.pathMapper);
|
|
160
|
+
return {
|
|
161
|
+
kind: mutation.kind,
|
|
162
|
+
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
163
|
+
targetPath
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return {
|
|
168
|
+
kind: mutation.kind,
|
|
169
|
+
label: mutation.label ?? describeMutation(mutation.kind, rawTarget),
|
|
170
|
+
targetPath: undefined
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return {
|
|
176
|
+
kind: mutation.kind,
|
|
177
|
+
label: mutation.label ?? mutation.kind
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
129
181
|
function pruneKeysByPrefix(table, prefix) {
|
|
130
182
|
const result = {};
|
|
131
183
|
for (const [key, value] of Object.entries(table)) {
|
|
@@ -223,6 +275,7 @@ async function applyEnsureDirectory(mutation, context, options) {
|
|
|
223
275
|
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
224
276
|
targetPath
|
|
225
277
|
};
|
|
278
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
226
279
|
const existed = await pathExists(context.fs, targetPath);
|
|
227
280
|
if (!context.dryRun) {
|
|
228
281
|
await context.fs.mkdir(targetPath, { recursive: true });
|
|
@@ -342,6 +395,7 @@ async function applyChmod(mutation, context, options) {
|
|
|
342
395
|
};
|
|
343
396
|
}
|
|
344
397
|
try {
|
|
398
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
345
399
|
const stat = await context.fs.stat(targetPath);
|
|
346
400
|
const currentMode = typeof stat.mode === "number" ? stat.mode & 0o777 : null;
|
|
347
401
|
if (currentMode === mutation.mode) {
|
|
@@ -376,6 +430,7 @@ async function applyBackup(mutation, context, options) {
|
|
|
376
430
|
label: mutation.label ?? describeMutation(mutation.kind, targetPath),
|
|
377
431
|
targetPath
|
|
378
432
|
};
|
|
433
|
+
await assertRegularWriteTarget(context, targetPath);
|
|
379
434
|
if (mutation.once && (await findLatestGeneratedBackup(context.fs, targetPath)) !== null) {
|
|
380
435
|
return {
|
|
381
436
|
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
@@ -524,6 +579,9 @@ async function applyConfigMerge(mutation, context, options) {
|
|
|
524
579
|
preserveContent = null;
|
|
525
580
|
}
|
|
526
581
|
const value = resolveValue(mutation.value, options);
|
|
582
|
+
if (!isConfigObject(value)) {
|
|
583
|
+
throw new Error(`configMerge value must be an object for "${rawPath}".`);
|
|
584
|
+
}
|
|
527
585
|
// Keep prefix pruning on the same proto-safe object-write path as normal merges.
|
|
528
586
|
let merged;
|
|
529
587
|
if (mutation.pruneByPrefix) {
|
|
@@ -661,6 +719,13 @@ async function applyConfigTransform(mutation, context, options) {
|
|
|
661
719
|
};
|
|
662
720
|
}
|
|
663
721
|
const serialized = serializeConfigUpdate(format, preserveContent, current, transformed);
|
|
722
|
+
const serializedChanged = serialized !== rawContent;
|
|
723
|
+
if (!serializedChanged) {
|
|
724
|
+
return {
|
|
725
|
+
outcome: { changed: false, effect: "none", detail: "noop" },
|
|
726
|
+
details
|
|
727
|
+
};
|
|
728
|
+
}
|
|
664
729
|
if (!context.dryRun) {
|
|
665
730
|
await writeAtomically(context, targetPath, serialized);
|
|
666
731
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyMutation } from "./apply-mutation.js";
|
|
1
|
+
import { applyMutation, resolveMutationDetails } from "./apply-mutation.js";
|
|
2
2
|
/**
|
|
3
3
|
* Execute an array of mutations in order.
|
|
4
4
|
*
|
|
@@ -21,12 +21,9 @@ export async function runMutations(mutations, context, options) {
|
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
async function executeMutation(mutation, context, options) {
|
|
24
|
+
const pendingDetails = resolveMutationDetails(mutation, context, options);
|
|
24
25
|
// Call onStart observer
|
|
25
|
-
context.observers?.onStart?.(
|
|
26
|
-
kind: mutation.kind,
|
|
27
|
-
label: mutation.label ?? mutation.kind,
|
|
28
|
-
targetPath: undefined // Will be resolved during apply
|
|
29
|
-
});
|
|
26
|
+
context.observers?.onStart?.(pendingDetails);
|
|
30
27
|
try {
|
|
31
28
|
const { outcome, details } = await applyMutation(mutation, context, options);
|
|
32
29
|
// Call onComplete observer
|
|
@@ -35,11 +32,7 @@ async function executeMutation(mutation, context, options) {
|
|
|
35
32
|
}
|
|
36
33
|
catch (error) {
|
|
37
34
|
// Call onError observer
|
|
38
|
-
context.observers?.onError?.(
|
|
39
|
-
kind: mutation.kind,
|
|
40
|
-
label: mutation.label ?? mutation.kind,
|
|
41
|
-
targetPath: undefined
|
|
42
|
-
}, error);
|
|
35
|
+
context.observers?.onError?.(pendingDetails, error);
|
|
43
36
|
// Re-throw the error
|
|
44
37
|
throw error;
|
|
45
38
|
}
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
export type FrontmatterBlock = {
|
|
2
2
|
raw: string;
|
|
3
3
|
body: string;
|
|
4
|
+
rawStart: number;
|
|
5
|
+
rawEnd: number;
|
|
4
6
|
};
|
|
5
7
|
export type SplitFrontmatterResult = FrontmatterBlock | {
|
|
6
8
|
body: string;
|
|
7
9
|
};
|
|
10
|
+
export type InspectFrontmatterResult = (FrontmatterBlock & {
|
|
11
|
+
kind: "frontmatter";
|
|
12
|
+
}) | {
|
|
13
|
+
kind: "missing-closing-fence";
|
|
14
|
+
raw: string;
|
|
15
|
+
rawStart: number;
|
|
16
|
+
rawEnd: number;
|
|
17
|
+
body: "";
|
|
18
|
+
message: string;
|
|
19
|
+
position: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "body";
|
|
22
|
+
body: string;
|
|
23
|
+
};
|
|
8
24
|
export declare function splitFrontmatterBlock(source: string): SplitFrontmatterResult;
|
|
25
|
+
export declare function inspectFrontmatterBlock(source: string): InspectFrontmatterResult;
|
|
@@ -1,15 +1,43 @@
|
|
|
1
|
+
const MISSING_END_DELIMITER_MESSAGE = "Missing YAML frontmatter end delimiter (---).";
|
|
1
2
|
export function splitFrontmatterBlock(source) {
|
|
3
|
+
const inspected = inspectFrontmatterBlock(source);
|
|
4
|
+
if (inspected.kind === "body") {
|
|
5
|
+
return { body: inspected.body };
|
|
6
|
+
}
|
|
7
|
+
if (inspected.kind === "missing-closing-fence") {
|
|
8
|
+
throw new Error(inspected.message);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
raw: inspected.raw,
|
|
12
|
+
rawStart: inspected.rawStart,
|
|
13
|
+
rawEnd: inspected.rawEnd,
|
|
14
|
+
body: inspected.body
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function inspectFrontmatterBlock(source) {
|
|
2
18
|
const content = source.startsWith("\uFEFF") ? source.slice(1) : source;
|
|
19
|
+
const sourceOffset = source.length - content.length;
|
|
3
20
|
const opening = readOpeningFence(content);
|
|
4
21
|
if (opening === undefined) {
|
|
5
|
-
return { body: source };
|
|
22
|
+
return { kind: "body", body: source };
|
|
6
23
|
}
|
|
7
24
|
const closing = findClosingFence(content, opening.next);
|
|
8
25
|
if (closing === undefined) {
|
|
9
|
-
|
|
26
|
+
return {
|
|
27
|
+
kind: "missing-closing-fence",
|
|
28
|
+
raw: content.slice(opening.next),
|
|
29
|
+
rawStart: sourceOffset + opening.next,
|
|
30
|
+
rawEnd: source.length,
|
|
31
|
+
body: "",
|
|
32
|
+
message: MISSING_END_DELIMITER_MESSAGE,
|
|
33
|
+
position: source.length
|
|
34
|
+
};
|
|
10
35
|
}
|
|
11
36
|
return {
|
|
37
|
+
kind: "frontmatter",
|
|
12
38
|
raw: content.slice(opening.next, closing.index),
|
|
39
|
+
rawStart: sourceOffset + opening.next,
|
|
40
|
+
rawEnd: sourceOffset + closing.index,
|
|
13
41
|
body: content.slice(closing.end + closing.lineBreakLength)
|
|
14
42
|
};
|
|
15
43
|
}
|
|
@@ -18,7 +46,7 @@ function readOpeningFence(source) {
|
|
|
18
46
|
return undefined;
|
|
19
47
|
}
|
|
20
48
|
const lineEnd = findLineEnd(source, 0);
|
|
21
|
-
if (lineEnd.lineBreakLength === 0 || source.slice(0, lineEnd.index)
|
|
49
|
+
if (lineEnd.lineBreakLength === 0 || !isFenceLine(source.slice(0, lineEnd.index))) {
|
|
22
50
|
return undefined;
|
|
23
51
|
}
|
|
24
52
|
return { next: lineEnd.index + lineEnd.lineBreakLength };
|
|
@@ -27,7 +55,7 @@ function findClosingFence(source, start) {
|
|
|
27
55
|
let lineStart = start;
|
|
28
56
|
while (lineStart <= source.length) {
|
|
29
57
|
const lineEnd = findLineEnd(source, lineStart);
|
|
30
|
-
if (
|
|
58
|
+
if (isFenceLine(source.slice(lineStart, lineEnd.index))) {
|
|
31
59
|
return {
|
|
32
60
|
index: lineStart,
|
|
33
61
|
end: lineEnd.index,
|
|
@@ -41,7 +69,7 @@ function findClosingFence(source, start) {
|
|
|
41
69
|
}
|
|
42
70
|
return undefined;
|
|
43
71
|
}
|
|
44
|
-
function
|
|
72
|
+
function isFenceLine(line) {
|
|
45
73
|
if (!line.startsWith("---")) {
|
|
46
74
|
return false;
|
|
47
75
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LineCounter, parse, parseDocument } from "yaml";
|
|
2
|
-
import { splitFrontmatterBlock } from "./fences.js";
|
|
2
|
+
import { inspectFrontmatterBlock, splitFrontmatterBlock } from "./fences.js";
|
|
3
3
|
export class FrontmatterParseError extends Error {
|
|
4
4
|
constructor(message) {
|
|
5
5
|
super(message);
|
|
@@ -20,9 +20,9 @@ export function parseFrontmatter(source, options = {}) {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
export function parseFrontmatterDocument(source, options = {}) {
|
|
23
|
-
const split =
|
|
24
|
-
const lineCounter =
|
|
25
|
-
if (split.
|
|
23
|
+
const split = inspectFrontmatterBlock(source);
|
|
24
|
+
const lineCounter = createSourceLineCounter(source);
|
|
25
|
+
if (split.kind === "body") {
|
|
26
26
|
return {
|
|
27
27
|
frontmatter: {},
|
|
28
28
|
body: split.body,
|
|
@@ -30,14 +30,26 @@ export function parseFrontmatterDocument(source, options = {}) {
|
|
|
30
30
|
lineCounter
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
if (split.kind === "missing-closing-fence") {
|
|
34
|
+
return {
|
|
35
|
+
frontmatter: {},
|
|
36
|
+
body: split.body,
|
|
37
|
+
errors: [{ message: split.message, pos: [split.position, split.position] }],
|
|
38
|
+
lineCounter
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const yamlLineCounter = new LineCounter();
|
|
42
|
+
const normalizedYaml = normalizeYamlLineEndings(split.raw);
|
|
43
|
+
const document = parseDocument(normalizedYaml, {
|
|
44
|
+
lineCounter: yamlLineCounter,
|
|
35
45
|
prettyErrors: false,
|
|
36
46
|
uniqueKeys: options.uniqueKeys ?? false
|
|
37
47
|
});
|
|
38
48
|
const errors = document.errors.map((error) => ({
|
|
39
49
|
message: error.message,
|
|
40
|
-
...(error.pos === undefined
|
|
50
|
+
...(error.pos === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { pos: translateYamlErrorPosition(error.pos, split) })
|
|
41
53
|
}));
|
|
42
54
|
if (errors.length > 0) {
|
|
43
55
|
return {
|
|
@@ -47,12 +59,72 @@ export function parseFrontmatterDocument(source, options = {}) {
|
|
|
47
59
|
lineCounter
|
|
48
60
|
};
|
|
49
61
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
try {
|
|
63
|
+
return {
|
|
64
|
+
frontmatter: normalizeYamlFrontmatter(document.toJSON()),
|
|
65
|
+
body: split.body,
|
|
66
|
+
errors,
|
|
67
|
+
lineCounter
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof FrontmatterParseError) {
|
|
72
|
+
return {
|
|
73
|
+
frontmatter: {},
|
|
74
|
+
body: split.body,
|
|
75
|
+
errors: [{ message: error.message }],
|
|
76
|
+
lineCounter
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function createSourceLineCounter(source) {
|
|
83
|
+
const lineCounter = new LineCounter();
|
|
84
|
+
lineCounter.addNewLine(0);
|
|
85
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
86
|
+
const character = source[index];
|
|
87
|
+
if (character === "\n") {
|
|
88
|
+
lineCounter.addNewLine(index + 1);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (character === "\r") {
|
|
92
|
+
lineCounter.addNewLine(index + (source[index + 1] === "\n" ? 2 : 1));
|
|
93
|
+
if (source[index + 1] === "\n") {
|
|
94
|
+
index += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lineCounter;
|
|
99
|
+
}
|
|
100
|
+
function translateYamlErrorPosition(pos, split) {
|
|
101
|
+
return pos.map((offset) => split.rawStart + normalizeYamlErrorOffset(split.raw, offset));
|
|
102
|
+
}
|
|
103
|
+
function normalizeYamlErrorOffset(raw, offset) {
|
|
104
|
+
if (offset >= raw.length && endsWithLineBreak(raw)) {
|
|
105
|
+
return findPreviousLineStart(raw, raw.length);
|
|
106
|
+
}
|
|
107
|
+
return offset;
|
|
108
|
+
}
|
|
109
|
+
function endsWithLineBreak(value) {
|
|
110
|
+
return value.endsWith("\n") || value.endsWith("\r");
|
|
111
|
+
}
|
|
112
|
+
function findPreviousLineStart(value, end) {
|
|
113
|
+
let index = end - 1;
|
|
114
|
+
if (value[index] === "\n") {
|
|
115
|
+
index -= 1;
|
|
116
|
+
}
|
|
117
|
+
if (value[index] === "\r") {
|
|
118
|
+
index -= 1;
|
|
119
|
+
}
|
|
120
|
+
while (index >= 0) {
|
|
121
|
+
const character = value[index];
|
|
122
|
+
if (character === "\n" || character === "\r") {
|
|
123
|
+
return index + 1;
|
|
124
|
+
}
|
|
125
|
+
index -= 1;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
56
128
|
}
|
|
57
129
|
function splitFrontmatter(source) {
|
|
58
130
|
try {
|
|
@@ -102,7 +174,7 @@ function normalizeYamlFrontmatter(value) {
|
|
|
102
174
|
if (value === null || value === undefined) {
|
|
103
175
|
return {};
|
|
104
176
|
}
|
|
105
|
-
if (!
|
|
177
|
+
if (!isPlainRecord(value)) {
|
|
106
178
|
throw new FrontmatterParseError("YAML frontmatter must parse to an object.");
|
|
107
179
|
}
|
|
108
180
|
return normalizeYamlValue(value);
|
|
@@ -2,6 +2,7 @@ import { stringify } from "yaml";
|
|
|
2
2
|
import { FrontmatterParseError } from "./parse.js";
|
|
3
3
|
export function stringifyFrontmatter(frontmatter, body) {
|
|
4
4
|
try {
|
|
5
|
+
assertFrontmatterRoot(frontmatter);
|
|
5
6
|
assertAcyclic(frontmatter);
|
|
6
7
|
return `---\n${stringify(frontmatter, { aliasDuplicateObjects: false }).trimEnd()}\n---\n${body}`;
|
|
7
8
|
}
|
|
@@ -13,6 +14,11 @@ export function stringifyFrontmatter(frontmatter, body) {
|
|
|
13
14
|
throw new FrontmatterParseError(`Invalid YAML frontmatter: ${message}`);
|
|
14
15
|
}
|
|
15
16
|
}
|
|
17
|
+
function assertFrontmatterRoot(value) {
|
|
18
|
+
if (!isPlainRecord(value)) {
|
|
19
|
+
throw new FrontmatterParseError("YAML frontmatter must parse to an object.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
16
22
|
function assertAcyclic(value, seen = new WeakSet()) {
|
|
17
23
|
if (typeof value !== "object" || value === null) {
|
|
18
24
|
return;
|
|
@@ -33,3 +39,10 @@ function assertAcyclic(value, seen = new WeakSet()) {
|
|
|
33
39
|
}
|
|
34
40
|
seen.delete(value);
|
|
35
41
|
}
|
|
42
|
+
function isPlainRecord(value) {
|
|
43
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const prototype = Object.getPrototypeOf(value);
|
|
47
|
+
return prototype === Object.prototype || prototype === null;
|
|
48
|
+
}
|