@nebutra/play-loader 0.1.1 → 0.1.3

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/src/index.ts DELETED
@@ -1,295 +0,0 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, join, relative } from "node:path";
3
- import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
4
- import { CapabilityError } from "@nebutra/errors";
5
- import { type ParsedFrontmatter, parseSkillMarkdown, type SkillMeta } from "@nebutra/tool-registry";
6
-
7
- export interface PlayDependency {
8
- readonly play: string;
9
- readonly outputs: readonly string[];
10
- }
11
-
12
- export interface PlaySubagent {
13
- readonly role: string;
14
- readonly allowedSkills: readonly string[];
15
- }
16
-
17
- export interface PlayMeta extends SkillMeta {
18
- readonly kind: "play";
19
- }
20
-
21
- export interface LoadedPlay {
22
- readonly meta: PlayMeta;
23
- readonly body: string;
24
- readonly frontmatter: ParsedFrontmatter;
25
- readonly requiredSkills: readonly string[];
26
- readonly subAgents: readonly PlaySubagent[];
27
- readonly dependsOnPlays: readonly PlayDependency[];
28
- }
29
-
30
- export interface PlayNode {
31
- readonly name: string;
32
- readonly dependsOn: readonly string[];
33
- }
34
-
35
- export interface PlayTestReport {
36
- readonly name: string;
37
- readonly ok: boolean;
38
- readonly checks: readonly string[];
39
- readonly suggestion?: string;
40
- }
41
-
42
- export interface PlayLoaderDoctorReport {
43
- readonly ok: boolean;
44
- readonly plays: number;
45
- readonly root: string;
46
- readonly suggestion?: string;
47
- }
48
-
49
- async function walk(dir: string): Promise<string[]> {
50
- const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
51
- const files: string[] = [];
52
- for (const entry of entries) {
53
- const full = join(dir, entry.name);
54
- if (entry.isDirectory()) files.push(...(await walk(full)));
55
- else files.push(full);
56
- }
57
- return files;
58
- }
59
-
60
- function asString(value: unknown): string | undefined {
61
- return typeof value === "string" ? value : undefined;
62
- }
63
-
64
- function asStringArray(value: unknown): string[] {
65
- return Array.isArray(value)
66
- ? value.filter((item): item is string => typeof item === "string")
67
- : [];
68
- }
69
-
70
- function asRecord(value: unknown): Record<string, unknown> | undefined {
71
- return value && typeof value === "object" && !Array.isArray(value)
72
- ? (value as Record<string, unknown>)
73
- : undefined;
74
- }
75
-
76
- function normalizePlayName(name: string): string {
77
- return name
78
- .trim()
79
- .toLowerCase()
80
- .replace(/[^a-z0-9_-]+/g, "_")
81
- .replace(/^_+|_+$/g, "");
82
- }
83
-
84
- async function appendPlayDebug(entry: Record<string, unknown>): Promise<void> {
85
- await appendCapabilityDebug("play-loader", entry);
86
- }
87
-
88
- export async function readPlayDebug(limit = 10): Promise<unknown[]> {
89
- return readCapabilityDebug("play-loader", { limit });
90
- }
91
-
92
- function parseSubagents(value: unknown): PlaySubagent[] {
93
- if (!Array.isArray(value)) return [];
94
- return value.flatMap((item): PlaySubagent[] => {
95
- const record = asRecord(item);
96
- const role = asString(record?.role);
97
- if (!record || !role) return [];
98
- return [{ role, allowedSkills: asStringArray(record.allowed_skills) }];
99
- });
100
- }
101
-
102
- function parsePlayDependencies(value: unknown): PlayDependency[] {
103
- if (!Array.isArray(value)) return [];
104
- return value.flatMap((item): PlayDependency[] => {
105
- if (typeof item === "string") return [{ play: item, outputs: [] }];
106
- const record = asRecord(item);
107
- const play = asString(record?.play);
108
- if (!record || !play) return [];
109
- return [{ play, outputs: asStringArray(record.outputs) }];
110
- });
111
- }
112
-
113
- export function parsePlayMarkdown(markdown: string): LoadedPlay {
114
- const loaded = parseSkillMarkdown(markdown);
115
- const { frontmatter } = loaded;
116
- if (frontmatter.kind !== "play") {
117
- throw new CapabilityError("play-loader", "Play SKILL.md must set kind: play", {
118
- suggestion: "Set `kind: play` in frontmatter or keep this document in tool-registry only.",
119
- statusCode: 400,
120
- metadata: { name: loaded.meta.name, kind: frontmatter.kind },
121
- });
122
- }
123
-
124
- return {
125
- meta: { ...loaded.meta, kind: "play" },
126
- body: loaded.body,
127
- frontmatter,
128
- requiredSkills: asStringArray(frontmatter.required_skills),
129
- subAgents: parseSubagents(frontmatter.sub_agents),
130
- dependsOnPlays: parsePlayDependencies(frontmatter.depends_on_plays),
131
- };
132
- }
133
-
134
- export function resolvePlayChain(nodes: readonly PlayNode[]): readonly string[] {
135
- const byName = new Map(nodes.map((node) => [node.name, node]));
136
- const visited = new Set<string>();
137
- const visiting = new Set<string>();
138
- const ordered: string[] = [];
139
-
140
- const visit = (name: string): void => {
141
- if (visited.has(name)) return;
142
- if (visiting.has(name)) {
143
- throw new CapabilityError("play-loader", "Play dependency cycle detected", {
144
- suggestion: "Remove the cycle or merge mutually dependent plays into one declaration.",
145
- statusCode: 400,
146
- metadata: { name },
147
- });
148
- }
149
- visiting.add(name);
150
- const node = byName.get(name);
151
- for (const dependency of node?.dependsOn ?? []) visit(dependency);
152
- visiting.delete(name);
153
- visited.add(name);
154
- ordered.push(name);
155
- };
156
-
157
- for (const node of nodes) visit(node.name);
158
- return ordered;
159
- }
160
-
161
- export class PlayLoader {
162
- readonly #root: string;
163
- readonly #plays = new Map<string, { readonly meta: PlayMeta; readonly path: string }>();
164
-
165
- private constructor(root: string) {
166
- this.#root = root;
167
- }
168
-
169
- static async open(root: string): Promise<PlayLoader> {
170
- const loader = new PlayLoader(root);
171
- await mkdir(loader.playsRoot(), { recursive: true });
172
- await loader.reload();
173
- return loader;
174
- }
175
-
176
- playsRoot(): string {
177
- return join(this.#root, "plays");
178
- }
179
-
180
- async writePlay(name: string, markdown: string): Promise<void> {
181
- const normalized = normalizePlayName(name);
182
- const parsed = parsePlayMarkdown(markdown);
183
- if (parsed.meta.name !== normalized) {
184
- throw new CapabilityError("play-loader", "Play name does not match path", {
185
- suggestion: "Use the same lowercase codename in the path and SKILL.md frontmatter.",
186
- metadata: { pathName: normalized, playName: parsed.meta.name },
187
- statusCode: 400,
188
- });
189
- }
190
- const path = join(this.playsRoot(), normalized, "SKILL.md");
191
- await mkdir(dirname(path), { recursive: true });
192
- await writeFile(path, markdown, "utf8");
193
- await this.reload();
194
- await appendPlayDebug({ type: "write", play: normalized });
195
- }
196
-
197
- async newPlay(name: string): Promise<LoadedPlay> {
198
- const normalized = normalizePlayName(name);
199
- const markdown = [
200
- "---",
201
- `name: ${normalized}`,
202
- "kind: play",
203
- "version: 1.0.0",
204
- `description: ${normalized.replaceAll("_", " ")} play`,
205
- "required_skills: []",
206
- "sub_agents: []",
207
- "depends_on_plays: []",
208
- "---",
209
- "",
210
- "## What this play does",
211
- "",
212
- "Describe the user story and expected output.",
213
- ].join("\n");
214
- await this.writePlay(normalized, markdown);
215
- return this.load(normalized);
216
- }
217
-
218
- async reload(): Promise<void> {
219
- this.#plays.clear();
220
- for (const file of await walk(this.playsRoot())) {
221
- if (!file.endsWith("SKILL.md")) continue;
222
- const raw = await readFile(file, "utf8");
223
- const play = parsePlayMarkdown(raw);
224
- this.#plays.set(play.meta.name, { meta: play.meta, path: relative(this.#root, file) });
225
- }
226
- await appendPlayDebug({ type: "reload", plays: this.#plays.size });
227
- }
228
-
229
- async list(): Promise<PlayMeta[]> {
230
- return [...this.#plays.values()]
231
- .map((entry) => entry.meta)
232
- .sort((left, right) => left.name.localeCompare(right.name));
233
- }
234
-
235
- async load(name: string): Promise<LoadedPlay> {
236
- const play = this.#plays.get(normalizePlayName(name));
237
- if (!play) {
238
- throw new CapabilityError("play-loader", "Play not found", {
239
- suggestion: "Run `pnpm play:list` or create it with `pnpm play:new <name>`.",
240
- metadata: { name },
241
- statusCode: 404,
242
- });
243
- }
244
- const loaded = parsePlayMarkdown(await readFile(join(this.#root, play.path), "utf8"));
245
- await appendPlayDebug({ type: "load", play: loaded.meta.name });
246
- return loaded;
247
- }
248
-
249
- async install(source: string): Promise<LoadedPlay> {
250
- if (!source.startsWith("file:")) {
251
- throw new CapabilityError("play-loader", "Only file installs are enabled locally", {
252
- suggestion:
253
- "Download remote plays through a reviewed distribution workflow, then install with file:<path>.",
254
- metadata: { source },
255
- statusCode: 400,
256
- });
257
- }
258
- const raw = await readFile(source.slice("file:".length), "utf8");
259
- const play = parsePlayMarkdown(raw);
260
- await this.writePlay(play.meta.name, raw);
261
- return this.load(play.meta.name);
262
- }
263
-
264
- async test(name: string): Promise<PlayTestReport> {
265
- try {
266
- const play = await this.load(name);
267
- return {
268
- name: play.meta.name,
269
- ok: play.body.length > 0,
270
- checks: ["frontmatter", "kind:play", "body", "dependency-dag"],
271
- ...(!play.body ? { suggestion: "Add a markdown body after the frontmatter." } : {}),
272
- };
273
- } catch (error) {
274
- return {
275
- name,
276
- ok: false,
277
- checks: [],
278
- suggestion:
279
- error instanceof CapabilityError
280
- ? "Create the play with `pnpm play:new <name>` or fix its SKILL.md metadata."
281
- : "Run `pnpm play:list` and verify the play directory exists.",
282
- };
283
- }
284
- }
285
-
286
- async doctor(): Promise<PlayLoaderDoctorReport> {
287
- const plays = this.#plays.size;
288
- return {
289
- ok: plays > 0,
290
- plays,
291
- root: this.#root,
292
- ...(plays === 0 ? { suggestion: "Create a play with `pnpm play:new <name>`." } : {}),
293
- };
294
- }
295
- }
package/tsconfig.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "target": "esnext",
7
- "types": ["node"],
8
- "incremental": false
9
- },
10
- "include": ["src"],
11
- "exclude": ["node_modules", "dist"]
12
- }
package/tsup.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts", "src/cli.ts"],
5
- format: ["esm"],
6
- dts: true,
7
- sourcemap: true,
8
- clean: true,
9
- target: "es2022",
10
- external: ["@nebutra/errors", "@nebutra/tool-registry"],
11
- });