@agentproto/manifest 0.1.0-alpha.1 → 0.1.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/dist/index.d.ts +60 -1
- package/dist/index.mjs +36 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -131,5 +131,64 @@ declare function createVerbs<TParams extends {
|
|
|
131
131
|
slug?: string;
|
|
132
132
|
name?: string;
|
|
133
133
|
}, THandle>(spec: DoctypeSpec<TParams, THandle>): Verbs<TParams, THandle>;
|
|
134
|
+
/**
|
|
135
|
+
* A render-and-validate thunk for use with {@link updateManifestSet}.
|
|
136
|
+
*
|
|
137
|
+
* Wrap an existing verb call with `dryRun: true` so that validation and
|
|
138
|
+
* rendering happen without touching disk. `updateManifestSet` calls the thunk
|
|
139
|
+
* in its phase-1 pass; if it throws the entire operation is aborted before
|
|
140
|
+
* any file is written.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* const ops: ManifestOp[] = [
|
|
145
|
+
* () => toolVerbs.create(toolParams, { dir, dryRun: true }),
|
|
146
|
+
* () => agentVerbs.update(agentPath, mutator, { dryRun: true }),
|
|
147
|
+
* ]
|
|
148
|
+
* await updateManifestSet(ops)
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
type ManifestOp = () => Promise<{
|
|
152
|
+
path: string;
|
|
153
|
+
rendered: string;
|
|
154
|
+
}>;
|
|
155
|
+
interface ManifestSetOptions {
|
|
156
|
+
/**
|
|
157
|
+
* If true, copy each existing target to `<path>.bak` before the commit
|
|
158
|
+
* (rename) phase. The backup is created before any rename so the original
|
|
159
|
+
* content survives a partial failure. Backup files are never removed on
|
|
160
|
+
* error — the caller can inspect them for recovery.
|
|
161
|
+
*/
|
|
162
|
+
backup?: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Write multiple manifests as a best-effort atomic set.
|
|
166
|
+
*
|
|
167
|
+
* ## Sequence
|
|
168
|
+
* 1. **Render + validate** — call every `op()` thunk concurrently. Any
|
|
169
|
+
* validation error aborts here; nothing is written.
|
|
170
|
+
* 2. **Stage** — write each rendered string to a sibling temp file
|
|
171
|
+
* `<path>.tmp-<hex>` in the same directory.
|
|
172
|
+
* 3. **Backup** *(optional)* — if `opts.backup`, copy each existing target to
|
|
173
|
+
* `<path>.bak`.
|
|
174
|
+
* 4. **Commit** — `rename(tmp, target)` for each file sequentially.
|
|
175
|
+
*
|
|
176
|
+
* ## Atomicity guarantee (honest)
|
|
177
|
+
* Each individual `rename()` is atomic on POSIX (same-device, same-dir swap).
|
|
178
|
+
* The **sequence** of renames is NOT globally atomic. A process crash between
|
|
179
|
+
* two renames leaves some targets at their new content and others unchanged —
|
|
180
|
+
* there is no cross-file rollback. This is an inherent POSIX limitation;
|
|
181
|
+
* true multi-file atomicity requires a WAL or journal outside this package.
|
|
182
|
+
*
|
|
183
|
+
* ## Error handling
|
|
184
|
+
* On any error in the stage or commit phase all remaining `.tmp-*` temp files
|
|
185
|
+
* are removed before rethrowing. Targets that were already committed in the
|
|
186
|
+
* same run are **not** rolled back — they hold their new content. Targets not
|
|
187
|
+
* yet reached remain unchanged.
|
|
188
|
+
*/
|
|
189
|
+
declare function updateManifestSet(ops: ManifestOp[], opts?: ManifestSetOptions): Promise<Array<{
|
|
190
|
+
path: string;
|
|
191
|
+
rendered: string;
|
|
192
|
+
}>>;
|
|
134
193
|
|
|
135
|
-
export { type CreateOptions, type DoctypeSpec, type ListOptions, type RefBlock, type ResolveContext, type VerbResult, type Verbs, createVerbs };
|
|
194
|
+
export { type CreateOptions, type DoctypeSpec, type ListOptions, type ManifestOp, type ManifestSetOptions, type RefBlock, type ResolveContext, type VerbResult, type Verbs, createVerbs, updateManifestSet };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { filterSerializable } from '@agentproto/define-doctype';
|
|
2
2
|
import matter from 'gray-matter';
|
|
3
|
-
import { mkdir, writeFile,
|
|
4
|
-
import {
|
|
3
|
+
import { mkdir, writeFile, access, copyFile, rename, rm, readFile, readdir } from 'fs/promises';
|
|
4
|
+
import { dirname, join, isAbsolute } from 'path';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @agentproto/manifest v0.1.0-alpha
|
|
@@ -117,12 +118,44 @@ function createVerbs(spec) {
|
|
|
117
118
|
}
|
|
118
119
|
return { create, load, list, update, resolve, delete: deleteFn };
|
|
119
120
|
}
|
|
121
|
+
async function updateManifestSet(ops, opts = {}) {
|
|
122
|
+
const results = await Promise.all(ops.map((op) => op()));
|
|
123
|
+
const suffix = randomBytes(4).toString("hex");
|
|
124
|
+
const tmps = new Array(results.length).fill("");
|
|
125
|
+
try {
|
|
126
|
+
for (const [i, { path, rendered }] of results.entries()) {
|
|
127
|
+
const tmp = `${path}.tmp-${suffix}`;
|
|
128
|
+
await mkdir(dirname(path), { recursive: true });
|
|
129
|
+
await writeFile(tmp, rendered, "utf8");
|
|
130
|
+
tmps[i] = tmp;
|
|
131
|
+
}
|
|
132
|
+
if (opts.backup) {
|
|
133
|
+
for (const { path } of results) {
|
|
134
|
+
try {
|
|
135
|
+
await access(path);
|
|
136
|
+
await copyFile(path, `${path}.bak`);
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const [i, { path }] of results.entries()) {
|
|
142
|
+
await rename(tmps[i], path);
|
|
143
|
+
tmps[i] = "";
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
await Promise.allSettled(
|
|
147
|
+
tmps.filter(Boolean).map((t) => rm(t, { force: true }))
|
|
148
|
+
);
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
120
153
|
function defaultBody(spec, frontmatter) {
|
|
121
154
|
const id = typeof frontmatter.name === "string" ? frontmatter.name : typeof frontmatter.id === "string" ? frontmatter.id : typeof frontmatter.slug === "string" ? frontmatter.slug : spec.name;
|
|
122
155
|
return `# ${id}
|
|
123
156
|
`;
|
|
124
157
|
}
|
|
125
158
|
|
|
126
|
-
export { createVerbs };
|
|
159
|
+
export { createVerbs, updateManifestSet };
|
|
127
160
|
//# sourceMappingURL=index.mjs.map
|
|
128
161
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/manifest — generic verbs for AIP doctypes.\n *\n * Every per-AIP package (`@agentproto/tool`, `@agentproto/driver`, …)\n * supplies a `DoctypeSpec` describing the per-package knobs (name,\n * AIP number, schema literal, path convention, validator, parser).\n * `createVerbs(spec)` returns the full lifecycle:\n *\n * create(params, opts) params → .md on disk\n * load(path) .md on disk → handle\n * list(dir, filter?) walk → handle[]\n * update(path, mutator) load → patch → write back\n * resolve(block, ctx?) { inline | ref | file } → handle\n * delete(path) fs.unlink\n *\n * Adding a new verb is a single place; every package picks it up\n * automatically — same model as `createDoctype` for the validation\n * pipeline.\n */\n\nimport { filterSerializable } from \"@agentproto/define-doctype\"\nimport matter from \"gray-matter\"\nimport type { Dirent } from \"node:fs\"\nimport { mkdir, readFile, readdir, rm, writeFile } from \"node:fs/promises\"\nimport { dirname, isAbsolute, join } from \"node:path\"\n\n/**\n * Per-package descriptor consumed by `createVerbs`.\n *\n * Generic over the params shape (input to `define`) and the handle\n * shape (output of `define`). For most AIPs `THandle` extends\n * `TParams` — handles are just frozen, defaulted definitions.\n */\nexport interface DoctypeSpec<\n TParams extends { id?: string; slug?: string; name?: string },\n THandle,\n> {\n /** Lower-case singular: \"tool\", \"driver\", \"operator\". Used in errors. */\n name: string\n /** AIP number — surfaces in error messages for provenance. */\n aip: number\n /** Frontmatter `schema:` literal (e.g. \"agentproto/tool/v1\"). */\n schemaLiteral: string\n /**\n * Filename of the doctype's manifest. By default `<DOCTYPE>.md`,\n * derived from `name.toUpperCase()`. Override only when the\n * doctype's filename diverges from convention.\n */\n filename?: string\n /**\n * Path-of-handle convention. Given a validated handle, return the\n * workspace-relative path the manifest lives at. Examples:\n * tool: `(h) => `${h.id}/TOOL.md``\n * policy: `(h) => `policies/${h.slug}/POLICY.md``\n * operator: `(h) => `${h.id}/OPERATOR.md``\n */\n pathOf(handle: THandle): string\n /** TS authoring path: validates params + applies defaults. */\n define(params: TParams): THandle\n /** MD authoring path: parses YAML+body. Throws on invalid frontmatter. */\n parse(source: string): { frontmatter: Record<string, unknown>; body: string }\n /**\n * Optional projection: handle → manifest-shaped frontmatter object.\n * Default: pass the params through `filterSerializable` (drops zod\n * schemas + functions). Override when the handle and frontmatter\n * shapes diverge non-trivially.\n */\n toFrontmatter?(params: TParams): Record<string, unknown>\n}\n\nexport interface CreateOptions {\n /** Workspace-relative or absolute base directory. */\n dir: string\n /** Body markdown after the frontmatter. Defaults to a one-line stub. */\n body?: string\n /** Render only — don't write. Returns the rendered string. */\n dryRun?: boolean\n}\n\nexport interface VerbResult<THandle> {\n path: string\n handle: THandle\n rendered: string\n}\n\nexport interface ListOptions {\n /** Filter the discovered handles. Receives the loaded handle. */\n filter?: <H>(handle: H) => boolean\n /** Skip subdirectories matching these names. Default: [\"node_modules\", \".git\", \"dist\", \".next\"]. */\n skipDirs?: readonly string[]\n}\n\nexport type RefBlock<TParams> =\n | { inline: TParams }\n | { ref: string }\n | { file: string }\n\nexport interface ResolveContext {\n /** Base dir for `file:` references. Required when resolving file blocks. */\n baseDir?: string\n /**\n * Registry resolver for `ref:` strings (e.g. `@agentik/runners/python-3.12`\n * → handle). Optional; throws when an unresolvable ref is encountered.\n */\n resolveRef?: (ref: string) => Promise<unknown> | unknown\n}\n\nexport interface Verbs<TParams, THandle> {\n /** TS-authored params → write `<dir>/<pathOf(handle)>` on disk. */\n create(params: TParams, opts: CreateOptions): Promise<VerbResult<THandle>>\n /** Read a manifest from disk → handle. */\n load(path: string): Promise<{ path: string; handle: THandle; body: string }>\n /** Walk `dir` for the doctype's `*.md` files; return loaded handles. */\n list(dir: string, opts?: ListOptions): Promise<THandle[]>\n /** Load → mutate → write back. Mutator receives the loaded params. */\n update(\n path: string,\n mutator: (params: TParams, ctx: { handle: THandle; body: string }) => TParams | Promise<TParams>,\n opts?: { body?: string; dryRun?: boolean },\n ): Promise<VerbResult<THandle>>\n /**\n * Resolve a `{ inline | ref | file }` block to a handle. Used by\n * doctypes that compose other doctypes (AIP-17/30/36 pattern).\n */\n resolve(\n block: RefBlock<TParams>,\n ctx?: ResolveContext,\n ): Promise<THandle>\n /** Remove the manifest file. Does NOT recurse into the containing dir. */\n delete(path: string): Promise<void>\n}\n\nconst DEFAULT_SKIP_DIRS: readonly string[] = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \".next\",\n \".turbo\",\n]\n\nexport function createVerbs<\n TParams extends { id?: string; slug?: string; name?: string },\n THandle,\n>(spec: DoctypeSpec<TParams, THandle>): Verbs<TParams, THandle> {\n const filename = spec.filename ?? `${spec.name.toUpperCase()}.md`\n const toFrontmatter =\n spec.toFrontmatter ??\n ((params: TParams) =>\n filterSerializable({\n schema: spec.schemaLiteral,\n ...params,\n }) as Record<string, unknown>)\n\n async function create(\n params: TParams,\n opts: CreateOptions,\n ): Promise<VerbResult<THandle>> {\n const handle = spec.define(params)\n const relativePath = spec.pathOf(handle)\n const path = join(opts.dir, relativePath)\n const frontmatter = toFrontmatter(params)\n const rendered = matter.stringify(\n opts.body ?? defaultBody(spec, frontmatter),\n frontmatter,\n )\n if (!opts.dryRun) {\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, rendered, \"utf8\")\n }\n return { path, handle, rendered }\n }\n\n async function load(\n path: string,\n ): Promise<{ path: string; handle: THandle; body: string }> {\n const source = await readFile(path, \"utf8\")\n const { frontmatter, body } = spec.parse(source)\n // The frontmatter is already zod-validated by `parse` (each per-\n // package parser runs the schema). Cast through `define` so the\n // cross-AIP invariants run too — same path the TS authoring uses.\n const handle = spec.define(frontmatter as unknown as TParams)\n return { path, handle, body }\n }\n\n async function list(\n dir: string,\n opts: ListOptions = {},\n ): Promise<THandle[]> {\n const skip = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS)\n const out: THandle[] = []\n\n async function walk(current: string): Promise<void> {\n // The Dirent overload returns `Dirent<NonSharedBuffer>[]` in\n // Node 22+'s @types/node when called with non-string buffer\n // input. Coerce by typing the entries as `Dirent[]` (string\n // names by default) since we always pass a string path.\n let entries: Dirent[]\n try {\n entries = (await readdir(current, { withFileTypes: true })) as Dirent[]\n } catch {\n return\n }\n for (const entry of entries) {\n const entryName = String(entry.name)\n if (entry.isDirectory()) {\n if (skip.has(entryName)) continue\n await walk(join(current, entryName))\n continue\n }\n if (!entry.isFile()) continue\n if (entryName !== filename) continue\n try {\n const { handle } = await load(join(current, entryName))\n if (!opts.filter || opts.filter(handle)) out.push(handle)\n } catch {\n // Malformed manifests skip the list — `load` will throw the\n // useful diagnostic when a caller wants to see it.\n }\n }\n }\n\n await walk(dir)\n return out\n }\n\n async function update(\n path: string,\n mutator: (\n params: TParams,\n ctx: { handle: THandle; body: string },\n ) => TParams | Promise<TParams>,\n opts: { body?: string; dryRun?: boolean } = {},\n ): Promise<VerbResult<THandle>> {\n const { handle, body } = await load(path)\n // Reconstruct params from the frontmatter — the load path already\n // validated, so this is just a reverse projection.\n const source = await readFile(path, \"utf8\")\n const { frontmatter } = spec.parse(source)\n const params = frontmatter as unknown as TParams\n const mutated = await mutator(params, { handle, body })\n const newHandle = spec.define(mutated)\n const newFrontmatter = toFrontmatter(mutated)\n const rendered = matter.stringify(\n opts.body ?? body,\n newFrontmatter,\n )\n if (!opts.dryRun) {\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, rendered, \"utf8\")\n }\n return { path, handle: newHandle, rendered }\n }\n\n async function resolve(\n block: RefBlock<TParams>,\n ctx: ResolveContext = {},\n ): Promise<THandle> {\n if (\"inline\" in block) {\n return spec.define(block.inline)\n }\n if (\"file\" in block) {\n const baseDir = ctx.baseDir ?? \".\"\n const path = isAbsolute(block.file) ? block.file : join(baseDir, block.file)\n const { handle } = await load(path)\n return handle\n }\n if (\"ref\" in block) {\n if (!ctx.resolveRef) {\n throw new Error(\n `${spec.name}.resolve (AIP-${spec.aip}): block has \\`ref: '${block.ref}'\\` but no resolveRef provided in context — registries must inject their own resolver`,\n )\n }\n const resolved = await ctx.resolveRef(block.ref)\n return spec.define(resolved as TParams)\n }\n throw new Error(\n `${spec.name}.resolve (AIP-${spec.aip}): block must have one of inline | ref | file`,\n )\n }\n\n async function deleteFn(path: string): Promise<void> {\n await rm(path, { force: true })\n }\n\n return { create, load, list, update, resolve, delete: deleteFn }\n}\n\nfunction defaultBody<P, H>(\n spec: DoctypeSpec<P extends { id?: string; slug?: string; name?: string } ? P : never, H>,\n frontmatter: Record<string, unknown>,\n): string {\n const id =\n typeof frontmatter.name === \"string\"\n ? frontmatter.name\n : typeof frontmatter.id === \"string\"\n ? frontmatter.id\n : typeof frontmatter.slug === \"string\"\n ? frontmatter.slug\n : spec.name\n return `# ${id}\\n`\n}\n\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @agentproto/manifest — generic verbs for AIP doctypes.\n *\n * Every per-AIP package (`@agentproto/tool`, `@agentproto/driver`, …)\n * supplies a `DoctypeSpec` describing the per-package knobs (name,\n * AIP number, schema literal, path convention, validator, parser).\n * `createVerbs(spec)` returns the full lifecycle:\n *\n * create(params, opts) params → .md on disk\n * load(path) .md on disk → handle\n * list(dir, filter?) walk → handle[]\n * update(path, mutator) load → patch → write back\n * resolve(block, ctx?) { inline | ref | file } → handle\n * delete(path) fs.unlink\n *\n * Adding a new verb is a single place; every package picks it up\n * automatically — same model as `createDoctype` for the validation\n * pipeline.\n */\n\nimport { filterSerializable } from \"@agentproto/define-doctype\"\nimport matter from \"gray-matter\"\nimport type { Dirent } from \"node:fs\"\nimport { access, copyFile, mkdir, readFile, readdir, rename, rm, writeFile } from \"node:fs/promises\"\nimport { dirname, isAbsolute, join } from \"node:path\"\nimport { randomBytes } from \"node:crypto\"\n\n/**\n * Per-package descriptor consumed by `createVerbs`.\n *\n * Generic over the params shape (input to `define`) and the handle\n * shape (output of `define`). For most AIPs `THandle` extends\n * `TParams` — handles are just frozen, defaulted definitions.\n */\nexport interface DoctypeSpec<\n TParams extends { id?: string; slug?: string; name?: string },\n THandle,\n> {\n /** Lower-case singular: \"tool\", \"driver\", \"operator\". Used in errors. */\n name: string\n /** AIP number — surfaces in error messages for provenance. */\n aip: number\n /** Frontmatter `schema:` literal (e.g. \"agentproto/tool/v1\"). */\n schemaLiteral: string\n /**\n * Filename of the doctype's manifest. By default `<DOCTYPE>.md`,\n * derived from `name.toUpperCase()`. Override only when the\n * doctype's filename diverges from convention.\n */\n filename?: string\n /**\n * Path-of-handle convention. Given a validated handle, return the\n * workspace-relative path the manifest lives at. Examples:\n * tool: `(h) => `${h.id}/TOOL.md``\n * policy: `(h) => `policies/${h.slug}/POLICY.md``\n * operator: `(h) => `${h.id}/OPERATOR.md``\n */\n pathOf(handle: THandle): string\n /** TS authoring path: validates params + applies defaults. */\n define(params: TParams): THandle\n /** MD authoring path: parses YAML+body. Throws on invalid frontmatter. */\n parse(source: string): { frontmatter: Record<string, unknown>; body: string }\n /**\n * Optional projection: handle → manifest-shaped frontmatter object.\n * Default: pass the params through `filterSerializable` (drops zod\n * schemas + functions). Override when the handle and frontmatter\n * shapes diverge non-trivially.\n */\n toFrontmatter?(params: TParams): Record<string, unknown>\n}\n\nexport interface CreateOptions {\n /** Workspace-relative or absolute base directory. */\n dir: string\n /** Body markdown after the frontmatter. Defaults to a one-line stub. */\n body?: string\n /** Render only — don't write. Returns the rendered string. */\n dryRun?: boolean\n}\n\nexport interface VerbResult<THandle> {\n path: string\n handle: THandle\n rendered: string\n}\n\nexport interface ListOptions {\n /** Filter the discovered handles. Receives the loaded handle. */\n filter?: <H>(handle: H) => boolean\n /** Skip subdirectories matching these names. Default: [\"node_modules\", \".git\", \"dist\", \".next\"]. */\n skipDirs?: readonly string[]\n}\n\nexport type RefBlock<TParams> =\n | { inline: TParams }\n | { ref: string }\n | { file: string }\n\nexport interface ResolveContext {\n /** Base dir for `file:` references. Required when resolving file blocks. */\n baseDir?: string\n /**\n * Registry resolver for `ref:` strings (e.g. `@agentik/runners/python-3.12`\n * → handle). Optional; throws when an unresolvable ref is encountered.\n */\n resolveRef?: (ref: string) => Promise<unknown> | unknown\n}\n\nexport interface Verbs<TParams, THandle> {\n /** TS-authored params → write `<dir>/<pathOf(handle)>` on disk. */\n create(params: TParams, opts: CreateOptions): Promise<VerbResult<THandle>>\n /** Read a manifest from disk → handle. */\n load(path: string): Promise<{ path: string; handle: THandle; body: string }>\n /** Walk `dir` for the doctype's `*.md` files; return loaded handles. */\n list(dir: string, opts?: ListOptions): Promise<THandle[]>\n /** Load → mutate → write back. Mutator receives the loaded params. */\n update(\n path: string,\n mutator: (params: TParams, ctx: { handle: THandle; body: string }) => TParams | Promise<TParams>,\n opts?: { body?: string; dryRun?: boolean },\n ): Promise<VerbResult<THandle>>\n /**\n * Resolve a `{ inline | ref | file }` block to a handle. Used by\n * doctypes that compose other doctypes (AIP-17/30/36 pattern).\n */\n resolve(\n block: RefBlock<TParams>,\n ctx?: ResolveContext,\n ): Promise<THandle>\n /** Remove the manifest file. Does NOT recurse into the containing dir. */\n delete(path: string): Promise<void>\n}\n\nconst DEFAULT_SKIP_DIRS: readonly string[] = [\n \"node_modules\",\n \".git\",\n \"dist\",\n \".next\",\n \".turbo\",\n]\n\nexport function createVerbs<\n TParams extends { id?: string; slug?: string; name?: string },\n THandle,\n>(spec: DoctypeSpec<TParams, THandle>): Verbs<TParams, THandle> {\n const filename = spec.filename ?? `${spec.name.toUpperCase()}.md`\n const toFrontmatter =\n spec.toFrontmatter ??\n ((params: TParams) =>\n filterSerializable({\n schema: spec.schemaLiteral,\n ...params,\n }) as Record<string, unknown>)\n\n async function create(\n params: TParams,\n opts: CreateOptions,\n ): Promise<VerbResult<THandle>> {\n const handle = spec.define(params)\n const relativePath = spec.pathOf(handle)\n const path = join(opts.dir, relativePath)\n const frontmatter = toFrontmatter(params)\n const rendered = matter.stringify(\n opts.body ?? defaultBody(spec, frontmatter),\n frontmatter,\n )\n if (!opts.dryRun) {\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, rendered, \"utf8\")\n }\n return { path, handle, rendered }\n }\n\n async function load(\n path: string,\n ): Promise<{ path: string; handle: THandle; body: string }> {\n const source = await readFile(path, \"utf8\")\n const { frontmatter, body } = spec.parse(source)\n // The frontmatter is already zod-validated by `parse` (each per-\n // package parser runs the schema). Cast through `define` so the\n // cross-AIP invariants run too — same path the TS authoring uses.\n const handle = spec.define(frontmatter as unknown as TParams)\n return { path, handle, body }\n }\n\n async function list(\n dir: string,\n opts: ListOptions = {},\n ): Promise<THandle[]> {\n const skip = new Set(opts.skipDirs ?? DEFAULT_SKIP_DIRS)\n const out: THandle[] = []\n\n async function walk(current: string): Promise<void> {\n // The Dirent overload returns `Dirent<NonSharedBuffer>[]` in\n // Node 22+'s @types/node when called with non-string buffer\n // input. Coerce by typing the entries as `Dirent[]` (string\n // names by default) since we always pass a string path.\n let entries: Dirent[]\n try {\n entries = (await readdir(current, { withFileTypes: true })) as Dirent[]\n } catch {\n return\n }\n for (const entry of entries) {\n const entryName = String(entry.name)\n if (entry.isDirectory()) {\n if (skip.has(entryName)) continue\n await walk(join(current, entryName))\n continue\n }\n if (!entry.isFile()) continue\n if (entryName !== filename) continue\n try {\n const { handle } = await load(join(current, entryName))\n if (!opts.filter || opts.filter(handle)) out.push(handle)\n } catch {\n // Malformed manifests skip the list — `load` will throw the\n // useful diagnostic when a caller wants to see it.\n }\n }\n }\n\n await walk(dir)\n return out\n }\n\n async function update(\n path: string,\n mutator: (\n params: TParams,\n ctx: { handle: THandle; body: string },\n ) => TParams | Promise<TParams>,\n opts: { body?: string; dryRun?: boolean } = {},\n ): Promise<VerbResult<THandle>> {\n const { handle, body } = await load(path)\n // Reconstruct params from the frontmatter — the load path already\n // validated, so this is just a reverse projection.\n const source = await readFile(path, \"utf8\")\n const { frontmatter } = spec.parse(source)\n const params = frontmatter as unknown as TParams\n const mutated = await mutator(params, { handle, body })\n const newHandle = spec.define(mutated)\n const newFrontmatter = toFrontmatter(mutated)\n const rendered = matter.stringify(\n opts.body ?? body,\n newFrontmatter,\n )\n if (!opts.dryRun) {\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, rendered, \"utf8\")\n }\n return { path, handle: newHandle, rendered }\n }\n\n async function resolve(\n block: RefBlock<TParams>,\n ctx: ResolveContext = {},\n ): Promise<THandle> {\n if (\"inline\" in block) {\n return spec.define(block.inline)\n }\n if (\"file\" in block) {\n const baseDir = ctx.baseDir ?? \".\"\n const path = isAbsolute(block.file) ? block.file : join(baseDir, block.file)\n const { handle } = await load(path)\n return handle\n }\n if (\"ref\" in block) {\n if (!ctx.resolveRef) {\n throw new Error(\n `${spec.name}.resolve (AIP-${spec.aip}): block has \\`ref: '${block.ref}'\\` but no resolveRef provided in context — registries must inject their own resolver`,\n )\n }\n const resolved = await ctx.resolveRef(block.ref)\n return spec.define(resolved as TParams)\n }\n throw new Error(\n `${spec.name}.resolve (AIP-${spec.aip}): block must have one of inline | ref | file`,\n )\n }\n\n async function deleteFn(path: string): Promise<void> {\n await rm(path, { force: true })\n }\n\n return { create, load, list, update, resolve, delete: deleteFn }\n}\n\n/**\n * A render-and-validate thunk for use with {@link updateManifestSet}.\n *\n * Wrap an existing verb call with `dryRun: true` so that validation and\n * rendering happen without touching disk. `updateManifestSet` calls the thunk\n * in its phase-1 pass; if it throws the entire operation is aborted before\n * any file is written.\n *\n * @example\n * ```ts\n * const ops: ManifestOp[] = [\n * () => toolVerbs.create(toolParams, { dir, dryRun: true }),\n * () => agentVerbs.update(agentPath, mutator, { dryRun: true }),\n * ]\n * await updateManifestSet(ops)\n * ```\n */\nexport type ManifestOp = () => Promise<{ path: string; rendered: string }>\n\nexport interface ManifestSetOptions {\n /**\n * If true, copy each existing target to `<path>.bak` before the commit\n * (rename) phase. The backup is created before any rename so the original\n * content survives a partial failure. Backup files are never removed on\n * error — the caller can inspect them for recovery.\n */\n backup?: boolean\n}\n\n/**\n * Write multiple manifests as a best-effort atomic set.\n *\n * ## Sequence\n * 1. **Render + validate** — call every `op()` thunk concurrently. Any\n * validation error aborts here; nothing is written.\n * 2. **Stage** — write each rendered string to a sibling temp file\n * `<path>.tmp-<hex>` in the same directory.\n * 3. **Backup** *(optional)* — if `opts.backup`, copy each existing target to\n * `<path>.bak`.\n * 4. **Commit** — `rename(tmp, target)` for each file sequentially.\n *\n * ## Atomicity guarantee (honest)\n * Each individual `rename()` is atomic on POSIX (same-device, same-dir swap).\n * The **sequence** of renames is NOT globally atomic. A process crash between\n * two renames leaves some targets at their new content and others unchanged —\n * there is no cross-file rollback. This is an inherent POSIX limitation;\n * true multi-file atomicity requires a WAL or journal outside this package.\n *\n * ## Error handling\n * On any error in the stage or commit phase all remaining `.tmp-*` temp files\n * are removed before rethrowing. Targets that were already committed in the\n * same run are **not** rolled back — they hold their new content. Targets not\n * yet reached remain unchanged.\n */\nexport async function updateManifestSet(\n ops: ManifestOp[],\n opts: ManifestSetOptions = {},\n): Promise<Array<{ path: string; rendered: string }>> {\n // Phase 1: Render + validate all ops. Fail early; nothing written yet.\n const results = await Promise.all(ops.map((op) => op()))\n\n const suffix = randomBytes(4).toString(\"hex\")\n // Mirrors `results` index-for-index. Empty string = not yet staged (skip cleanup).\n const tmps: string[] = new Array(results.length).fill(\"\")\n\n try {\n // Phase 2: Stage — write each to a sibling .tmp file.\n // entries() yields [index, element] with element fully typed (no undefined).\n for (const [i, { path, rendered }] of results.entries()) {\n const tmp = `${path}.tmp-${suffix}`\n await mkdir(dirname(path), { recursive: true })\n await writeFile(tmp, rendered, \"utf8\")\n tmps[i] = tmp\n }\n\n // Phase 3: Backup (optional) — copy existing targets before rename.\n if (opts.backup) {\n for (const { path } of results) {\n try {\n await access(path)\n await copyFile(path, `${path}.bak`)\n } catch {\n // File doesn't exist yet — no backup needed.\n }\n }\n }\n\n // Phase 4: Commit — atomic rename of each staged tmp → target.\n // Each rename() is individually atomic (POSIX same-device).\n // The loop is NOT globally atomic; see the doc-comment above.\n for (const [i, { path }] of results.entries()) {\n // tmps[i] is guaranteed non-null: same length as results, set in phase 2.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n await rename(tmps[i]!, path)\n tmps[i] = \"\" // Committed; exclude from cleanup if a later rename fails.\n }\n } catch (err) {\n // Remove staged .tmp files that were not yet committed.\n await Promise.allSettled(\n tmps.filter(Boolean).map((t) => rm(t, { force: true })),\n )\n throw err\n }\n\n return results\n}\n\nfunction defaultBody<P, H>(\n spec: DoctypeSpec<P extends { id?: string; slug?: string; name?: string } ? P : never, H>,\n frontmatter: Record<string, unknown>,\n): string {\n const id =\n typeof frontmatter.name === \"string\"\n ? frontmatter.name\n : typeof frontmatter.id === \"string\"\n ? frontmatter.id\n : typeof frontmatter.slug === \"string\"\n ? frontmatter.slug\n : spec.name\n return `# ${id}\\n`\n}\n\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentproto/manifest",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "agentproto/manifest — generic verbs for AIP doctypes (create, load, list, update, resolve, delete). Each per-AIP package supplies a DoctypeSpec describing its name / AIP number / schema literal / path convention / validator+parser; createVerbs(spec) returns the full lifecycle. Adding a new verb (hash, sign, migrate, …) is one place; every package picks it up automatically.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentproto",
|