@kinotic-ai/spawn 0.3.0 → 0.5.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.js CHANGED
@@ -1,126 +1,6 @@
1
- // packages/spawn/src/api/SpawnEngine.ts
2
- import { Liquid } from "liquidjs";
3
- import { z } from "zod";
4
- var PropertySchemaSchema = z.object({
5
- type: z.enum(["string", "number", "integer", "boolean"]).optional(),
6
- description: z.string().optional(),
7
- default: z.union([z.string(), z.number(), z.boolean()]).optional(),
8
- enum: z.array(z.string()).optional()
9
- });
10
- var SpawnConfigSchema = z.object({
11
- inherits: z.string().optional(),
12
- globals: z.record(z.string(), z.unknown()).optional(),
13
- propertySchema: z.record(z.string(), PropertySchemaSchema).optional()
14
- });
15
- var IGNORED_FILE_NAMES = ["spawn.json", ".DS_Store"];
16
- function upperFirst(s) {
17
- return s.charAt(0).toUpperCase() + s.slice(1);
18
- }
19
- function camelCase(s) {
20
- return s.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase());
21
- }
22
-
23
- class SpawnEngine {
24
- engine;
25
- constructor() {
26
- this.engine = new Liquid({ cache: true, strictVariables: true });
27
- this.engine.registerFilter("packageToPath", (v) => v.replaceAll(".", "/"));
28
- this.engine.registerFilter("encodePackage", (v) => {
29
- v = v.replaceAll("-", "_");
30
- v = v.replace(/\.(\d+)/g, "._$1");
31
- return v;
32
- });
33
- this.engine.registerFilter("camelCase", (v) => camelCase(v));
34
- this.engine.registerFilter("upperFirst", (v) => upperFirst(v));
35
- }
36
- async renderSpawn(spawn, options) {
37
- const trees = [spawn];
38
- const configs = [];
39
- let currentConfig = this.parseConfig(spawn);
40
- if (currentConfig) {
41
- configs.push(currentConfig);
42
- }
43
- while (currentConfig?.inherits) {
44
- if (!options?.loadInherited) {
45
- throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
46
- }
47
- const inherited = await options.loadInherited(currentConfig.inherits);
48
- trees.push(inherited);
49
- currentConfig = this.parseConfig(inherited);
50
- if (currentConfig) {
51
- configs.push(currentConfig);
52
- }
53
- }
54
- let globals = {};
55
- let propertySchemas = {};
56
- for (const config of [...configs].reverse()) {
57
- if (config.globals) {
58
- globals = { ...globals, ...config.globals };
59
- }
60
- if (config.propertySchema) {
61
- propertySchemas = { ...propertySchemas, ...config.propertySchema };
62
- }
63
- }
64
- let context = { ...globals, ...options?.context };
65
- context = await this.resolveMissingProperties(propertySchemas, context, options?.propertyResolver);
66
- const files = {};
67
- const sources = {};
68
- for (const tree of [...trees].reverse()) {
69
- for (const source of Object.keys(tree).sort()) {
70
- const fileName = source.substring(source.lastIndexOf("/") + 1);
71
- if (IGNORED_FILE_NAMES.includes(fileName)) {
72
- continue;
73
- }
74
- let destination = source;
75
- if (destination.includes("{{")) {
76
- destination = await this.engine.parseAndRender(destination, context);
77
- }
78
- let content = tree[source];
79
- if (destination.endsWith(".liquid")) {
80
- destination = destination.substring(0, destination.length - 7);
81
- if (typeof content !== "string") {
82
- throw new Error(`Template ${source} must contain text content`);
83
- }
84
- content = await this.engine.parseAndRender(content, context);
85
- }
86
- files[destination] = content;
87
- sources[destination] = source;
88
- }
89
- }
90
- return { files, sources, context };
91
- }
92
- parseConfig(tree) {
93
- const raw = tree["spawn.json"];
94
- if (raw === undefined) {
95
- return;
96
- }
97
- const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
98
- return SpawnConfigSchema.parse(JSON.parse(text));
99
- }
100
- async resolveMissingProperties(propertySchemas, context, resolver) {
101
- const ret = { ...context };
102
- for (const key in propertySchemas) {
103
- if (!Object.prototype.hasOwnProperty.call(ret, key)) {
104
- if (!resolver) {
105
- throw new Error(`No value provided for required property '${key}'`);
106
- }
107
- const schema = propertySchemas[key];
108
- let message;
109
- if (schema.description?.includes("{{")) {
110
- message = this.engine.parseAndRenderSync(schema.description, ret);
111
- } else {
112
- message = schema.description ?? key;
113
- }
114
- let defaultValue = schema.default;
115
- if (typeof schema.default === "string" && schema.default.includes("{{")) {
116
- defaultValue = this.engine.parseAndRenderSync(schema.default, ret);
117
- }
118
- ret[key] = await resolver.resolve(key, schema, message, defaultValue);
119
- }
120
- }
121
- return ret;
122
- }
123
- }
1
+ import {
2
+ SpawnEngine
3
+ } from "./shared/spawn-1h1k8w8r.js";
124
4
  export {
125
5
  SpawnEngine
126
6
  };
@@ -0,0 +1,311 @@
1
+ var import_node_module = require("node:module");
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __toCommonJS = (from) => {
34
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
35
+ if (entry)
36
+ return entry;
37
+ entry = __defProp({}, "__esModule", { value: true });
38
+ if (from && typeof from === "object" || typeof from === "function") {
39
+ for (var key of __getOwnPropNames(from))
40
+ if (!__hasOwnProp.call(entry, key))
41
+ __defProp(entry, key, {
42
+ get: __accessProp.bind(from, key),
43
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
44
+ });
45
+ }
46
+ __moduleCache.set(from, entry);
47
+ return entry;
48
+ };
49
+ var __moduleCache;
50
+ var __returnValue = (v) => v;
51
+ function __exportSetter(name, newValue) {
52
+ this[name] = __returnValue.bind(null, newValue);
53
+ }
54
+ var __export = (target, all) => {
55
+ for (var name in all)
56
+ __defProp(target, name, {
57
+ get: all[name],
58
+ enumerable: true,
59
+ configurable: true,
60
+ set: __exportSetter.bind(all, name)
61
+ });
62
+ };
63
+
64
+ // packages/spawn/src/node/index.ts
65
+ var exports_node = {};
66
+ __export(exports_node, {
67
+ lintSpawnDir: () => lintSpawnDir,
68
+ assertPathWithin: () => assertPathWithin,
69
+ NodeSpawnRenderer: () => NodeSpawnRenderer
70
+ });
71
+ module.exports = __toCommonJS(exports_node);
72
+ var import_node_fs = __toESM(require("node:fs"));
73
+ var import_promises = __toESM(require("node:fs/promises"));
74
+ var import_node_path = __toESM(require("node:path"));
75
+
76
+ // packages/spawn/src/api/SpawnEngine.ts
77
+ var import_liquidjs = require("liquidjs");
78
+ var import_zod = require("zod");
79
+ var PropertySchemaSchema = import_zod.z.object({
80
+ type: import_zod.z.enum(["string", "number", "integer", "boolean"]).optional(),
81
+ description: import_zod.z.string().optional(),
82
+ default: import_zod.z.union([import_zod.z.string(), import_zod.z.number(), import_zod.z.boolean()]).optional(),
83
+ enum: import_zod.z.array(import_zod.z.string()).optional()
84
+ });
85
+ var SpawnConfigSchema = import_zod.z.object({
86
+ inherits: import_zod.z.string().optional(),
87
+ globals: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
88
+ propertySchema: import_zod.z.record(import_zod.z.string(), PropertySchemaSchema).optional()
89
+ });
90
+ var IGNORED_FILE_NAMES = ["spawn.json", ".DS_Store"];
91
+ function upperFirst(s) {
92
+ return s.charAt(0).toUpperCase() + s.slice(1);
93
+ }
94
+ function camelCase(s) {
95
+ return s.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toLowerCase());
96
+ }
97
+
98
+ class SpawnEngine {
99
+ engine;
100
+ constructor() {
101
+ this.engine = new import_liquidjs.Liquid({ cache: true, strictVariables: true });
102
+ this.engine.registerFilter("packageToPath", (v) => v.replaceAll(".", "/"));
103
+ this.engine.registerFilter("encodePackage", (v) => {
104
+ v = v.replaceAll("-", "_");
105
+ v = v.replace(/\.(\d+)/g, "._$1");
106
+ return v;
107
+ });
108
+ this.engine.registerFilter("camelCase", (v) => camelCase(v));
109
+ this.engine.registerFilter("upperFirst", (v) => upperFirst(v));
110
+ }
111
+ async renderSpawn(spawn, options) {
112
+ const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
113
+ let globals = {};
114
+ let propertySchemas = {};
115
+ for (const config of [...configs].reverse()) {
116
+ if (config.globals) {
117
+ globals = { ...globals, ...config.globals };
118
+ }
119
+ if (config.propertySchema) {
120
+ propertySchemas = { ...propertySchemas, ...config.propertySchema };
121
+ }
122
+ }
123
+ let context = { ...globals, ...options?.context };
124
+ context = await this.resolveMissingProperties(propertySchemas, context, options?.propertyResolver);
125
+ const files = {};
126
+ const sources = {};
127
+ for (const tree of [...trees].reverse()) {
128
+ for (const source of Object.keys(tree).sort()) {
129
+ const fileName = source.substring(source.lastIndexOf("/") + 1);
130
+ if (IGNORED_FILE_NAMES.includes(fileName)) {
131
+ continue;
132
+ }
133
+ let destination = source;
134
+ if (destination.includes("{{")) {
135
+ destination = await this.engine.parseAndRender(destination, context);
136
+ }
137
+ let content = tree[source];
138
+ if (destination.endsWith(".liquid")) {
139
+ destination = destination.substring(0, destination.length - 7);
140
+ if (typeof content !== "string") {
141
+ throw new Error(`Template ${source} must contain text content`);
142
+ }
143
+ content = await this.engine.parseAndRender(content, context);
144
+ }
145
+ files[destination] = content;
146
+ sources[destination] = source;
147
+ }
148
+ }
149
+ return { files, sources, context };
150
+ }
151
+ async lint(spawn, options) {
152
+ const { trees, configs } = await this.walkInheritance(spawn, options?.loadInherited);
153
+ const declared = new Set;
154
+ for (const config of configs) {
155
+ if (config.globals) {
156
+ Object.keys(config.globals).forEach((key) => declared.add(key));
157
+ }
158
+ if (config.propertySchema) {
159
+ Object.keys(config.propertySchema).forEach((key) => declared.add(key));
160
+ }
161
+ }
162
+ const usedIn = new Map;
163
+ const record = (name, file) => {
164
+ let files = usedIn.get(name);
165
+ if (!files) {
166
+ files = new Set;
167
+ usedIn.set(name, files);
168
+ }
169
+ files.add(file);
170
+ };
171
+ const externalVars = (template) => Object.keys(this.engine.parseAndAnalyzeSync(template, undefined, { partials: false }).globals);
172
+ for (const tree of trees) {
173
+ for (const source of Object.keys(tree)) {
174
+ const fileName = source.substring(source.lastIndexOf("/") + 1);
175
+ if (IGNORED_FILE_NAMES.includes(fileName)) {
176
+ continue;
177
+ }
178
+ externalVars(source).forEach((name) => record(name, source));
179
+ const content = tree[source];
180
+ if (source.endsWith(".liquid") && typeof content === "string") {
181
+ externalVars(content).forEach((name) => record(name, source));
182
+ }
183
+ }
184
+ }
185
+ const undeclared = [];
186
+ for (const [name, files] of usedIn) {
187
+ if (!declared.has(name)) {
188
+ undeclared.push({ name, files: [...files].sort() });
189
+ }
190
+ }
191
+ undeclared.sort((a, b) => a.name.localeCompare(b.name));
192
+ return { undeclared };
193
+ }
194
+ async walkInheritance(spawn, loadInherited) {
195
+ const trees = [spawn];
196
+ const configs = [];
197
+ let currentConfig = this.parseConfig(spawn);
198
+ if (currentConfig) {
199
+ configs.push(currentConfig);
200
+ }
201
+ while (currentConfig?.inherits) {
202
+ if (!loadInherited) {
203
+ throw new Error(`Spawn inherits '${currentConfig.inherits}' but no loadInherited callback was provided`);
204
+ }
205
+ const inherited = await loadInherited(currentConfig.inherits);
206
+ trees.push(inherited);
207
+ currentConfig = this.parseConfig(inherited);
208
+ if (currentConfig) {
209
+ configs.push(currentConfig);
210
+ }
211
+ }
212
+ return { trees, configs };
213
+ }
214
+ parseConfig(tree) {
215
+ const raw = tree["spawn.json"];
216
+ if (raw === undefined) {
217
+ return;
218
+ }
219
+ const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
220
+ return SpawnConfigSchema.parse(JSON.parse(text));
221
+ }
222
+ async resolveMissingProperties(propertySchemas, context, resolver) {
223
+ const ret = { ...context };
224
+ for (const key in propertySchemas) {
225
+ if (!Object.prototype.hasOwnProperty.call(ret, key)) {
226
+ if (!resolver) {
227
+ throw new Error(`No value provided for required property '${key}'`);
228
+ }
229
+ const schema = propertySchemas[key];
230
+ let message;
231
+ if (schema.description?.includes("{{")) {
232
+ message = this.engine.parseAndRenderSync(schema.description, ret);
233
+ } else {
234
+ message = schema.description ?? key;
235
+ }
236
+ let defaultValue = schema.default;
237
+ if (typeof schema.default === "string" && schema.default.includes("{{")) {
238
+ defaultValue = this.engine.parseAndRenderSync(schema.default, ret);
239
+ }
240
+ ret[key] = await resolver.resolve(key, schema, message, defaultValue);
241
+ }
242
+ }
243
+ return ret;
244
+ }
245
+ }
246
+
247
+ // packages/spawn/src/node/index.ts
248
+ function assertContained(root, resolved) {
249
+ const rel = import_node_path.default.relative(root, resolved);
250
+ if (rel.startsWith("..") || import_node_path.default.isAbsolute(rel)) {
251
+ throw new Error(`Path escapes ${root}: ${resolved}`);
252
+ }
253
+ return resolved;
254
+ }
255
+ function assertPathWithin(root, target) {
256
+ return assertContained(root, import_node_path.default.resolve(root, target));
257
+ }
258
+ async function loadSpawnTree(dir) {
259
+ const tree = {};
260
+ const entries = await import_promises.default.readdir(dir, { recursive: true, withFileTypes: true });
261
+ for (const entry of entries) {
262
+ if (!entry.isFile()) {
263
+ continue;
264
+ }
265
+ const filePath = import_node_path.default.join(entry.parentPath, entry.name);
266
+ const treePath = import_node_path.default.relative(dir, filePath).split(import_node_path.default.sep).join("/");
267
+ if (treePath.endsWith(".liquid")) {
268
+ tree[treePath] = await import_promises.default.readFile(filePath, { encoding: "utf8" });
269
+ } else {
270
+ tree[treePath] = await import_promises.default.readFile(filePath);
271
+ }
272
+ }
273
+ return tree;
274
+ }
275
+ function diskInheritanceLoader(spawnRoot) {
276
+ let currentDir = spawnRoot;
277
+ return async (ref) => {
278
+ currentDir = assertContained(spawnRoot, import_node_path.default.resolve(currentDir, ref));
279
+ if (!import_node_fs.default.existsSync(import_node_path.default.resolve(currentDir, "spawn.json"))) {
280
+ throw new Error(`Inherited spawn ${import_node_path.default.resolve(currentDir, "spawn.json")} does not exist`);
281
+ }
282
+ return loadSpawnTree(currentDir);
283
+ };
284
+ }
285
+ async function lintSpawnDir(dir) {
286
+ return new SpawnEngine().lint(await loadSpawnTree(dir), { loadInherited: diskInheritanceLoader(dir) });
287
+ }
288
+
289
+ class NodeSpawnRenderer {
290
+ engine = new SpawnEngine;
291
+ async render(spawnDir, destination, options) {
292
+ if (import_node_fs.default.existsSync(destination)) {
293
+ throw new Error(`The target directory ${destination} already exists`);
294
+ }
295
+ const result = await this.engine.renderSpawn(await loadSpawnTree(spawnDir), {
296
+ context: options?.context,
297
+ propertyResolver: options?.propertyResolver,
298
+ loadInherited: diskInheritanceLoader(spawnDir)
299
+ });
300
+ await this.writeSpawnTree(result.files, destination);
301
+ return result.context;
302
+ }
303
+ async writeSpawnTree(tree, destination) {
304
+ await import_promises.default.mkdir(destination, { recursive: true });
305
+ for (const [treePath, content] of Object.entries(tree)) {
306
+ const filePath = assertPathWithin(destination, treePath);
307
+ await import_promises.default.mkdir(import_node_path.default.dirname(filePath), { recursive: true });
308
+ await import_promises.default.writeFile(filePath, content);
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Declares a single property a spawn needs to render, as found in a
3
+ * spawn.json propertySchema. Descriptions and defaults may contain liquid
4
+ * expressions, which are rendered against the context resolved so far before
5
+ * the property is requested from a {@link PropertyResolver}.
6
+ */
7
+ interface PropertySchema {
8
+ type?: "string" | "number" | "integer" | "boolean";
9
+ description?: string;
10
+ default?: string | number | boolean;
11
+ enum?: string[];
12
+ }
13
+ /**
14
+ * Supplies values for properties declared in a spawn's propertySchema that are
15
+ * absent from the render context. An interactive host can prompt the user; a
16
+ * non-interactive host can omit the resolver entirely, in which case rendering
17
+ * fails on the first missing property.
18
+ */
19
+ interface PropertyResolver {
20
+ /**
21
+ * Returns the value to use for {@code key}.
22
+ *
23
+ * @param key the property name as declared in the propertySchema
24
+ * @param schema the declaration for the property (type, enum, etc.)
25
+ * @param message the property description with any liquid expressions already
26
+ * rendered against the context resolved so far, or the key when the
27
+ * schema has no description
28
+ * @param defaultValue the schema default with any liquid expressions already
29
+ * rendered, or undefined when the schema has no default
30
+ */
31
+ resolve(key: string, schema: PropertySchema, message: string, defaultValue?: unknown): Promise<unknown>;
32
+ }
33
+ /**
34
+ * A variable referenced in a spawn's templates that is declared neither in
35
+ * propertySchema nor in globals, reported by {@link SpawnEngine#lint}.
36
+ *
37
+ * @see SpawnLintResult
38
+ */
39
+ interface UndeclaredVariable {
40
+ /** The variable name as referenced in the templates. */
41
+ name: string;
42
+ /**
43
+ * The spawn paths where the variable appears — file contents for
44
+ * {@code .liquid} files, and path templates for any file whose path contains
45
+ * the variable.
46
+ */
47
+ files: string[];
48
+ }
49
+ /**
50
+ * The outcome of {@link SpawnEngine#lint}: the variables a spawn's templates
51
+ * reference that are not declared in propertySchema or globals. A spawn is
52
+ * lint-clean when {@code undeclared} is empty.
53
+ */
54
+ interface SpawnLintResult {
55
+ /** Referenced-but-undeclared variables, sorted by name. */
56
+ undeclared: UndeclaredVariable[];
57
+ }
58
+ /**
59
+ * Resolves {@code target} against {@code root} and asserts the result stays at or
60
+ * below {@code root}, so a path can't escape the directory being operated in via
61
+ * {@code ..} (whether authored that way or injected through a property value).
62
+ * Internal {@code ../} that stays within {@code root} is allowed.
63
+ */
64
+ declare function assertPathWithin(root: string, target: string): string;
65
+ /**
66
+ * Reports the variables a spawn directory's templates reference that are
67
+ * declared neither in propertySchema nor in globals (following inheritance on
68
+ * disk, confined to {@code dir}). See {@link SpawnEngine#lint}.
69
+ */
70
+ declare function lintSpawnDir(dir: string): Promise<SpawnLintResult>;
71
+ /**
72
+ * Options for {@link NodeSpawnRenderer#render}.
73
+ */
74
+ interface NodeRenderOptions {
75
+ /** Values made available to the templates. */
76
+ context?: Record<string, unknown>;
77
+ /**
78
+ * Supplies values for propertySchema entries missing from the context. With no
79
+ * resolver, a missing required property fails the render.
80
+ */
81
+ propertyResolver?: PropertyResolver;
82
+ }
83
+ /**
84
+ * Renders a Spawn from a directory on disk to another directory, using the
85
+ * host-agnostic {@link SpawnEngine} for the rendering and confining all reads
86
+ * (inheritance) to the source and all writes to the destination. The reusable
87
+ * filesystem adapter so node/bun callers don't reimplement the disk load/write
88
+ * or the directory-traversal guards.
89
+ */
90
+ declare class NodeSpawnRenderer {
91
+ private engine;
92
+ /**
93
+ * Renders the spawn at {@code spawnDir} into {@code destination}, which must not
94
+ * exist yet, and returns the full context used.
95
+ *
96
+ * @throws Error when {@code destination} already exists, an inheritance ref or a
97
+ * rendered path escapes its root, or a required property has no value
98
+ */
99
+ render(spawnDir: string, destination: string, options?: NodeRenderOptions): Promise<Record<string, unknown>>;
100
+ private writeSpawnTree;
101
+ }
102
+ export { lintSpawnDir, assertPathWithin, NodeSpawnRenderer, NodeRenderOptions };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Declares a single property a spawn needs to render, as found in a
3
+ * spawn.json propertySchema. Descriptions and defaults may contain liquid
4
+ * expressions, which are rendered against the context resolved so far before
5
+ * the property is requested from a {@link PropertyResolver}.
6
+ */
7
+ interface PropertySchema {
8
+ type?: "string" | "number" | "integer" | "boolean";
9
+ description?: string;
10
+ default?: string | number | boolean;
11
+ enum?: string[];
12
+ }
13
+ /**
14
+ * Supplies values for properties declared in a spawn's propertySchema that are
15
+ * absent from the render context. An interactive host can prompt the user; a
16
+ * non-interactive host can omit the resolver entirely, in which case rendering
17
+ * fails on the first missing property.
18
+ */
19
+ interface PropertyResolver {
20
+ /**
21
+ * Returns the value to use for {@code key}.
22
+ *
23
+ * @param key the property name as declared in the propertySchema
24
+ * @param schema the declaration for the property (type, enum, etc.)
25
+ * @param message the property description with any liquid expressions already
26
+ * rendered against the context resolved so far, or the key when the
27
+ * schema has no description
28
+ * @param defaultValue the schema default with any liquid expressions already
29
+ * rendered, or undefined when the schema has no default
30
+ */
31
+ resolve(key: string, schema: PropertySchema, message: string, defaultValue?: unknown): Promise<unknown>;
32
+ }
33
+ /**
34
+ * A variable referenced in a spawn's templates that is declared neither in
35
+ * propertySchema nor in globals, reported by {@link SpawnEngine#lint}.
36
+ *
37
+ * @see SpawnLintResult
38
+ */
39
+ interface UndeclaredVariable {
40
+ /** The variable name as referenced in the templates. */
41
+ name: string;
42
+ /**
43
+ * The spawn paths where the variable appears — file contents for
44
+ * {@code .liquid} files, and path templates for any file whose path contains
45
+ * the variable.
46
+ */
47
+ files: string[];
48
+ }
49
+ /**
50
+ * The outcome of {@link SpawnEngine#lint}: the variables a spawn's templates
51
+ * reference that are not declared in propertySchema or globals. A spawn is
52
+ * lint-clean when {@code undeclared} is empty.
53
+ */
54
+ interface SpawnLintResult {
55
+ /** Referenced-but-undeclared variables, sorted by name. */
56
+ undeclared: UndeclaredVariable[];
57
+ }
58
+ /**
59
+ * Resolves {@code target} against {@code root} and asserts the result stays at or
60
+ * below {@code root}, so a path can't escape the directory being operated in via
61
+ * {@code ..} (whether authored that way or injected through a property value).
62
+ * Internal {@code ../} that stays within {@code root} is allowed.
63
+ */
64
+ declare function assertPathWithin(root: string, target: string): string;
65
+ /**
66
+ * Reports the variables a spawn directory's templates reference that are
67
+ * declared neither in propertySchema nor in globals (following inheritance on
68
+ * disk, confined to {@code dir}). See {@link SpawnEngine#lint}.
69
+ */
70
+ declare function lintSpawnDir(dir: string): Promise<SpawnLintResult>;
71
+ /**
72
+ * Options for {@link NodeSpawnRenderer#render}.
73
+ */
74
+ interface NodeRenderOptions {
75
+ /** Values made available to the templates. */
76
+ context?: Record<string, unknown>;
77
+ /**
78
+ * Supplies values for propertySchema entries missing from the context. With no
79
+ * resolver, a missing required property fails the render.
80
+ */
81
+ propertyResolver?: PropertyResolver;
82
+ }
83
+ /**
84
+ * Renders a Spawn from a directory on disk to another directory, using the
85
+ * host-agnostic {@link SpawnEngine} for the rendering and confining all reads
86
+ * (inheritance) to the source and all writes to the destination. The reusable
87
+ * filesystem adapter so node/bun callers don't reimplement the disk load/write
88
+ * or the directory-traversal guards.
89
+ */
90
+ declare class NodeSpawnRenderer {
91
+ private engine;
92
+ /**
93
+ * Renders the spawn at {@code spawnDir} into {@code destination}, which must not
94
+ * exist yet, and returns the full context used.
95
+ *
96
+ * @throws Error when {@code destination} already exists, an inheritance ref or a
97
+ * rendered path escapes its root, or a required property has no value
98
+ */
99
+ render(spawnDir: string, destination: string, options?: NodeRenderOptions): Promise<Record<string, unknown>>;
100
+ private writeSpawnTree;
101
+ }
102
+ export { lintSpawnDir, assertPathWithin, NodeSpawnRenderer, NodeRenderOptions };