@introspection-ai/pi-recipes 0.1.0-beta.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/cli.js ADDED
@@ -0,0 +1,479 @@
1
+ #!/usr/bin/env node
2
+ import { execFile, spawn } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { realpath } from "node:fs/promises";
5
+ import { dirname, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { promisify } from "node:util";
8
+ import { createRecipeScaffold, validateRecipeDirectory, } from "./recipe-dev.js";
9
+ import { publishRecipe, } from "./recipe-publish.js";
10
+ import { addRecipe, customizeRecipe, defaultRecipeStoreDir, listRecipes, recipeDisplayName, recipePreferredIdentifier, recipeScopedIdentifier, removeRecipe, resolveRecipeDirectory, } from "./recipe-store.js";
11
+ import { readPiPackageManifest, } from "./recipe-package.js";
12
+ const execFileAsync = promisify(execFile);
13
+ const PACKAGE_NAME = "@introspection-ai/pi-recipes";
14
+ const DEFAULT_PI_EXTENSION_SOURCE = `npm:${PACKAGE_NAME}`;
15
+ function usage(commandName = "recipes") {
16
+ return [
17
+ `Usage: ${commandName} <command> [args]`,
18
+ "",
19
+ "Commands:",
20
+ " setup [source] Install the Pi recipes extension into Pi",
21
+ " create <dir> Create a starter recipe directory",
22
+ " install <source> Install or register a recipe source",
23
+ " customize <recipe> Copy an installed recipe into an editable local copy",
24
+ " list List installed recipes",
25
+ " remove <recipe> Remove an installed recipe record",
26
+ " path <recipe|path> Print the resolved recipe directory",
27
+ " doctor <target> Validate a recipe directory or installed recipe",
28
+ " publish <target> Publish a recipe to a GitHub repository",
29
+ "",
30
+ "Options:",
31
+ " --store <dir> Use a custom recipe store",
32
+ " --name <name> Recipe name for create",
33
+ " --setup-source <source>",
34
+ " Pi extension source for auto-setup",
35
+ " --github <owner/repo>",
36
+ " Create or update a GitHub recipe repository during publish",
37
+ " --message <text> Commit message for publish",
38
+ " --visibility <public|private>",
39
+ " Required with --github; controls GitHub repository visibility",
40
+ " --local Install the Pi extension into project settings during setup",
41
+ " --no-setup Skip automatic Pi extension setup",
42
+ " --force Re-clone an existing remote source",
43
+ " --json Print machine-readable JSON",
44
+ "",
45
+ "First-time setup:",
46
+ ` npm install -g ${PACKAGE_NAME}`,
47
+ ` ${commandName} install github:owner/repo`,
48
+ "",
49
+ "Create and try a recipe:",
50
+ ` ${commandName} create ./my-recipe`,
51
+ ` ${commandName} doctor ./my-recipe`,
52
+ ` ${commandName} install ./my-recipe`,
53
+ " pi --recipe my-recipe",
54
+ "",
55
+ "Publish a recipe:",
56
+ ` ${commandName} publish ./my-recipe --github owner/my-recipe --visibility private`,
57
+ "",
58
+ "Source examples:",
59
+ " ./local-recipe",
60
+ " git@github.com:owner/private-recipe.git",
61
+ " git+https://github.com/owner/recipe.git#v1.0.0",
62
+ " github:owner/repo/path/to/recipe#v1.0.0",
63
+ " owner/repo",
64
+ ].join("\n");
65
+ }
66
+ function parseArgs(argv) {
67
+ const values = [];
68
+ let command = "";
69
+ let storeDir;
70
+ let name;
71
+ let setupSource;
72
+ let github;
73
+ let message;
74
+ let visibility;
75
+ let local = false;
76
+ let noSetup = false;
77
+ let force = false;
78
+ let json = false;
79
+ for (let index = 0; index < argv.length; index += 1) {
80
+ const arg = argv[index];
81
+ if (arg === "--help" || arg === "-h") {
82
+ command = "help";
83
+ }
84
+ else if (arg === "--store") {
85
+ const value = argv[++index];
86
+ if (!value)
87
+ throw new Error("--store requires a directory");
88
+ storeDir = value;
89
+ }
90
+ else if (arg === "--name") {
91
+ const value = argv[++index];
92
+ if (!value)
93
+ throw new Error("--name requires a value");
94
+ name = value;
95
+ }
96
+ else if (arg === "--setup-source") {
97
+ const value = argv[++index];
98
+ if (!value)
99
+ throw new Error("--setup-source requires a value");
100
+ setupSource = value;
101
+ }
102
+ else if (arg === "--github") {
103
+ const value = argv[++index];
104
+ if (!value)
105
+ throw new Error("--github requires owner/repo");
106
+ github = value;
107
+ }
108
+ else if (arg === "--message" || arg === "-m") {
109
+ const value = argv[++index];
110
+ if (!value)
111
+ throw new Error("--message requires text");
112
+ message = value;
113
+ }
114
+ else if (arg === "--visibility") {
115
+ const value = argv[++index];
116
+ if (value !== "public" && value !== "private") {
117
+ throw new Error("--visibility requires public or private");
118
+ }
119
+ visibility = value;
120
+ }
121
+ else if (arg === "--local" || arg === "-l") {
122
+ local = true;
123
+ }
124
+ else if (arg === "--no-setup") {
125
+ noSetup = true;
126
+ }
127
+ else if (arg === "--force") {
128
+ force = true;
129
+ }
130
+ else if (arg === "--json") {
131
+ json = true;
132
+ }
133
+ else if (!command) {
134
+ command = arg;
135
+ }
136
+ else {
137
+ values.push(arg);
138
+ }
139
+ }
140
+ return {
141
+ command: command || "help",
142
+ values,
143
+ storeDir,
144
+ name,
145
+ setupSource,
146
+ github,
147
+ message,
148
+ visibility,
149
+ local,
150
+ noSetup,
151
+ force,
152
+ json,
153
+ };
154
+ }
155
+ function requireOne(args, label) {
156
+ const value = args.values[0];
157
+ if (!value)
158
+ throw new Error(`${args.command} requires ${label}`);
159
+ return value;
160
+ }
161
+ function printJson(value) {
162
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
163
+ }
164
+ function packageRoot() {
165
+ const filename = fileURLToPath(import.meta.url);
166
+ return resolve(dirname(filename), "..");
167
+ }
168
+ async function normalizedPath(path) {
169
+ try {
170
+ return await realpath(path);
171
+ }
172
+ catch {
173
+ return resolve(path);
174
+ }
175
+ }
176
+ async function piExtensionInstalled() {
177
+ let stdout = "";
178
+ try {
179
+ ({ stdout } = await execFileAsync("pi", ["list"], {
180
+ env: process.env,
181
+ maxBuffer: 1024 * 1024,
182
+ }));
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ if (stdout.includes(DEFAULT_PI_EXTENSION_SOURCE) || stdout.includes(PACKAGE_NAME)) {
188
+ return true;
189
+ }
190
+ const root = await normalizedPath(packageRoot());
191
+ for (const line of stdout.split(/\r?\n/)) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed || trimmed.startsWith("User packages:") || trimmed.startsWith("Project packages:")) {
194
+ continue;
195
+ }
196
+ if ((await normalizedPath(trimmed)) === root)
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+ async function installPiExtension(source, opts = {}) {
202
+ const args = ["install", source, ...(opts.local ? ["--local"] : [])];
203
+ try {
204
+ if (opts.quiet) {
205
+ await execFileAsync("pi", args, {
206
+ env: process.env,
207
+ maxBuffer: 1024 * 1024,
208
+ });
209
+ }
210
+ else {
211
+ await new Promise((resolveInstall, rejectInstall) => {
212
+ const child = spawn("pi", args, {
213
+ env: process.env,
214
+ stdio: "inherit",
215
+ });
216
+ child.on("error", rejectInstall);
217
+ child.on("close", (code, signal) => {
218
+ if (code === 0) {
219
+ resolveInstall();
220
+ }
221
+ else if (signal) {
222
+ rejectInstall(new Error(`pi ${args.join(" ")} terminated by signal ${signal}`));
223
+ }
224
+ else {
225
+ rejectInstall(new Error(`pi ${args.join(" ")} exited with code ${code ?? "unknown"}`));
226
+ }
227
+ });
228
+ });
229
+ }
230
+ }
231
+ catch (err) {
232
+ const message = err instanceof Error ? err.message : String(err);
233
+ throw new Error([
234
+ `Failed to install the Pi recipes extension with: pi ${args.join(" ")}`,
235
+ message,
236
+ "",
237
+ "Make sure `pi` is installed and available on PATH, then run:",
238
+ ` recipes setup ${source}`,
239
+ ].join("\n"));
240
+ }
241
+ }
242
+ async function ensurePiExtension(args) {
243
+ if (args.noSetup)
244
+ return;
245
+ if (await piExtensionInstalled())
246
+ return;
247
+ const source = args.setupSource ?? DEFAULT_PI_EXTENSION_SOURCE;
248
+ process.stderr.write(`Installing Pi recipes extension with: pi install ${source}\n`);
249
+ await installPiExtension(source, { local: args.local, quiet: true });
250
+ }
251
+ function printDoctorReport(report) {
252
+ process.stdout.write(`${report.manifest.name}@${report.manifest.version}\n`);
253
+ for (const finding of report.findings) {
254
+ process.stdout.write(`${finding.severity}: ${finding.code}: ${finding.message}\n`);
255
+ }
256
+ if (report.findings.length === 0)
257
+ process.stdout.write("ok\n");
258
+ const resourceEntries = Object.entries(report.resources);
259
+ if (resourceEntries.length > 0) {
260
+ process.stdout.write("\nResources:\n");
261
+ for (const [key, paths] of resourceEntries) {
262
+ process.stdout.write(` ${key}: ${paths.length}\n`);
263
+ }
264
+ }
265
+ }
266
+ function printPublishedRecipe(result) {
267
+ process.stdout.write([
268
+ `Published ${result.packageName}@${result.recipe.version}`,
269
+ result.recipeDir,
270
+ "",
271
+ `GitHub: ${result.github}`,
272
+ `Recipe name: ${result.shortName}`,
273
+ ...(result.scopedName !== result.shortName ? [`Scoped name: ${result.scopedName}`] : []),
274
+ `Repository: ${result.createdRepository ? "created" : "existing"}`,
275
+ `Commit: ${result.committed ? "created" : "no changes"}`,
276
+ "Push: ok",
277
+ "",
278
+ "Use:",
279
+ ` pi --recipe ${result.shortName}`,
280
+ ...(result.scopedName !== result.shortName ? [` pi --recipe ${result.scopedName}`] : []),
281
+ "",
282
+ ].join("\n"));
283
+ }
284
+ function recipeInstallSource(recipe) {
285
+ if (recipe.id.startsWith("github:"))
286
+ return `GitHub ${recipe.id.slice("github:".length)}`;
287
+ if (recipe.id.startsWith("local:"))
288
+ return "local editable copy";
289
+ if (recipe.id.startsWith("git:"))
290
+ return `Git ${recipe.id.slice("git:".length)}`;
291
+ if (recipe.source.startsWith("github:"))
292
+ return `GitHub ${recipe.source.slice("github:".length)}`;
293
+ return recipe.source;
294
+ }
295
+ function printRecipeList(recipes, storeDir) {
296
+ if (recipes.length === 0) {
297
+ process.stdout.write(`No recipes installed in ${storeDir}\n`);
298
+ return;
299
+ }
300
+ process.stdout.write(`Installed recipes (${recipes.length})\n`);
301
+ process.stdout.write(`Store: ${storeDir}\n\n`);
302
+ for (const [index, recipe] of recipes.entries()) {
303
+ const identifier = recipePreferredIdentifier(recipe);
304
+ const scopedIdentifier = recipeScopedIdentifier(recipe);
305
+ process.stdout.write(`${identifier}\n`);
306
+ if (scopedIdentifier !== identifier) {
307
+ process.stdout.write(` Scoped name: ${scopedIdentifier}\n`);
308
+ }
309
+ process.stdout.write(` Version: ${recipe.version}\n`);
310
+ process.stdout.write(` Installed from: ${recipeInstallSource(recipe)}\n`);
311
+ process.stdout.write(` Local files: ${recipe.path}\n`);
312
+ if (index < recipes.length - 1)
313
+ process.stdout.write("\n");
314
+ }
315
+ }
316
+ async function main(argv) {
317
+ const args = parseArgs(argv);
318
+ const opts = { storeDir: args.storeDir, cwd: process.cwd(), env: process.env };
319
+ const commandName = "recipes";
320
+ if (args.command === "help") {
321
+ process.stdout.write(`${usage(commandName)}\n`);
322
+ return 0;
323
+ }
324
+ if (args.command === "setup") {
325
+ const source = args.values[0] ?? args.setupSource ?? DEFAULT_PI_EXTENSION_SOURCE;
326
+ if (!args.force && await piExtensionInstalled()) {
327
+ process.stdout.write("Pi recipes extension is already installed.\n");
328
+ return 0;
329
+ }
330
+ await installPiExtension(source, { local: args.local });
331
+ process.stdout.write(`Pi recipes extension installed from ${source}\n`);
332
+ return 0;
333
+ }
334
+ if (args.command === "create") {
335
+ const target = requireOne(args, "<dir>");
336
+ const result = createRecipeScaffold(target, {
337
+ cwd: opts.cwd,
338
+ name: args.name,
339
+ force: args.force,
340
+ });
341
+ if (args.json) {
342
+ printJson(result);
343
+ }
344
+ else {
345
+ process.stdout.write(`Created recipe ${result.name}\n${result.recipeDir}\n\n`);
346
+ for (const file of result.files) {
347
+ process.stdout.write(`${file.action}: ${file.path}\n`);
348
+ }
349
+ process.stdout.write([
350
+ "",
351
+ "Next steps:",
352
+ ` ${commandName} doctor ${result.recipeDir}`,
353
+ ` ${commandName} install ${result.recipeDir}`,
354
+ ` pi --recipe ${result.name}`,
355
+ ` ${commandName} publish ${result.recipeDir}`,
356
+ "",
357
+ ].join("\n"));
358
+ }
359
+ return 0;
360
+ }
361
+ if (args.command === "install") {
362
+ const source = requireOne(args, "<source>");
363
+ await ensurePiExtension(args);
364
+ const recipe = await addRecipe(source, { ...opts, force: args.force });
365
+ if (args.json) {
366
+ printJson(recipe);
367
+ }
368
+ else {
369
+ process.stdout.write(`Installed ${recipeDisplayName(recipe)}\n${recipe.path}\n`);
370
+ }
371
+ return 0;
372
+ }
373
+ if (args.command === "customize") {
374
+ const identifier = requireOne(args, "<recipe>");
375
+ const result = await customizeRecipe(identifier, { ...opts, force: args.force });
376
+ if (args.json) {
377
+ printJson(result);
378
+ }
379
+ else {
380
+ const identifier = recipePreferredIdentifier(result.recipe);
381
+ const heading = result.overwritten
382
+ ? `Updated editable copy for ${identifier}`
383
+ : `Created editable copy for ${identifier}`;
384
+ process.stdout.write([
385
+ heading,
386
+ "",
387
+ "Edit this folder:",
388
+ ` ${result.path}`,
389
+ "",
390
+ "Then check and run it:",
391
+ ` ${commandName} doctor ${identifier}`,
392
+ ` pi --recipe ${identifier}`,
393
+ "",
394
+ ].join("\n"));
395
+ }
396
+ return 0;
397
+ }
398
+ if (args.command === "list") {
399
+ const recipes = listRecipes(opts);
400
+ if (args.json) {
401
+ printJson(recipes);
402
+ }
403
+ else {
404
+ printRecipeList(recipes, args.storeDir ?? defaultRecipeStoreDir(process.env));
405
+ }
406
+ return 0;
407
+ }
408
+ if (args.command === "remove") {
409
+ const identifier = requireOne(args, "<recipe>");
410
+ const recipe = removeRecipe(identifier, opts);
411
+ if (!recipe)
412
+ throw new Error(`Recipe not found: ${identifier}`);
413
+ if (args.json) {
414
+ printJson(recipe);
415
+ }
416
+ else {
417
+ process.stdout.write(`Removed ${recipeDisplayName(recipe)}\n`);
418
+ }
419
+ return 0;
420
+ }
421
+ if (args.command === "path") {
422
+ const identifier = requireOne(args, "<recipe|path>");
423
+ const path = resolveRecipeDirectory(identifier, opts);
424
+ if (!existsSync(path))
425
+ throw new Error(`Recipe not found: ${identifier}`);
426
+ if (args.json) {
427
+ printJson({ path });
428
+ }
429
+ else {
430
+ process.stdout.write(`${path}\n`);
431
+ }
432
+ return 0;
433
+ }
434
+ if (args.command === "doctor") {
435
+ const identifier = args.values[0] ?? ".";
436
+ const path = resolveRecipeDirectory(identifier, opts);
437
+ readPiPackageManifest(path);
438
+ const report = validateRecipeDirectory(path);
439
+ if (args.json) {
440
+ printJson(report);
441
+ }
442
+ else {
443
+ printDoctorReport(report);
444
+ }
445
+ return report.valid ? 0 : 1;
446
+ }
447
+ if (args.command === "publish") {
448
+ const identifier = args.values[0] ?? ".";
449
+ if (!args.github) {
450
+ throw new Error("publish requires --github owner/repo");
451
+ }
452
+ if (!args.visibility) {
453
+ throw new Error("publish requires --visibility public or --visibility private");
454
+ }
455
+ const result = await publishRecipe(identifier, {
456
+ ...opts,
457
+ github: args.github,
458
+ message: args.message,
459
+ visibility: args.visibility,
460
+ force: args.force,
461
+ });
462
+ if (args.json) {
463
+ printJson(result);
464
+ }
465
+ else {
466
+ printPublishedRecipe(result);
467
+ }
468
+ return 0;
469
+ }
470
+ throw new Error(`Unknown command: ${args.command}\n\n${usage(commandName)}`);
471
+ }
472
+ main(process.argv.slice(2)).then((code) => {
473
+ process.exitCode = code;
474
+ }, (err) => {
475
+ const message = err instanceof Error ? err.message : String(err);
476
+ process.stderr.write(`${message}\n`);
477
+ process.exitCode = 1;
478
+ });
479
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,7 @@
1
+ export * from "./pi-extension.js";
2
+ export * from "./recipe-agent.js";
3
+ export * from "./recipe-dev.js";
4
+ export * from "./recipe-package.js";
5
+ export * from "./recipe-publish.js";
6
+ export * from "./recipe-store.js";
7
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./pi-extension.js";
2
+ export * from "./recipe-agent.js";
3
+ export * from "./recipe-dev.js";
4
+ export * from "./recipe-package.js";
5
+ export * from "./recipe-publish.js";
6
+ export * from "./recipe-store.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,26 @@
1
+ import { type AuthStorage, type ExtensionFactory, type ModelRegistry } from "@earendil-works/pi-coding-agent";
2
+ import { type RecipeChildToolEvent } from "./child-agent.js";
3
+ export interface PiRecipesExtensionOptions {
4
+ env?: NodeJS.ProcessEnv;
5
+ createChildAgentRunner?: CreateRecipeChildAgentRunner;
6
+ }
7
+ interface RecipeChildAgentRunner {
8
+ start(): Promise<void>;
9
+ prompt(task: string): Promise<unknown>;
10
+ cancel(): Promise<void>;
11
+ shutdown(): Promise<void>;
12
+ }
13
+ type CreateRecipeChildAgentRunner = (opts: {
14
+ recipeDir: string;
15
+ workspaceDir: string;
16
+ agentName: string;
17
+ env?: NodeJS.ProcessEnv;
18
+ authStorage?: AuthStorage;
19
+ modelRegistry?: ModelRegistry;
20
+ onAssistantMessage?: (text: string, stream: "delta" | "final") => void;
21
+ onToolEvent?: (event: RecipeChildToolEvent) => void;
22
+ }) => RecipeChildAgentRunner;
23
+ export declare function createPiRecipesExtension(opts?: PiRecipesExtensionOptions): ExtensionFactory;
24
+ declare const _default: ExtensionFactory;
25
+ export default _default;
26
+ //# sourceMappingURL=pi-extension.d.ts.map