@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/README.md +116 -0
- package/dist/child-agent.d.ts +40 -0
- package/dist/child-agent.js +235 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +479 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/pi-extension.d.ts +26 -0
- package/dist/pi-extension.js +809 -0
- package/dist/recipe-agent.d.ts +51 -0
- package/dist/recipe-agent.js +359 -0
- package/dist/recipe-dev.d.ts +30 -0
- package/dist/recipe-dev.js +180 -0
- package/dist/recipe-package.d.ts +38 -0
- package/dist/recipe-package.js +231 -0
- package/dist/recipe-publish.d.ts +30 -0
- package/dist/recipe-publish.js +192 -0
- package/dist/recipe-store.d.ts +66 -0
- package/dist/recipe-store.js +691 -0
- package/docs/pi-extension.md +413 -0
- package/docs/recipe-cli.md +529 -0
- package/docs/recipe-flow.md +148 -0
- package/package.json +92 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
const RESOURCE_KEYS = [
|
|
4
|
+
"agents",
|
|
5
|
+
"extensions",
|
|
6
|
+
"skills",
|
|
7
|
+
"prompts",
|
|
8
|
+
"themes",
|
|
9
|
+
];
|
|
10
|
+
function stringArray(value) {
|
|
11
|
+
return Array.isArray(value)
|
|
12
|
+
? value.filter((item) => typeof item === "string")
|
|
13
|
+
: [];
|
|
14
|
+
}
|
|
15
|
+
function emptyResources() {
|
|
16
|
+
return {
|
|
17
|
+
agents: [],
|
|
18
|
+
extensions: [],
|
|
19
|
+
skills: [],
|
|
20
|
+
prompts: [],
|
|
21
|
+
themes: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeResourcePath(path) {
|
|
25
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
26
|
+
}
|
|
27
|
+
function hasTraversalSegment(path) {
|
|
28
|
+
return normalizeResourcePath(path)
|
|
29
|
+
.split("/")
|
|
30
|
+
.some((segment) => segment === "." || segment === "..");
|
|
31
|
+
}
|
|
32
|
+
function assertPackageResourcePath(pkg, key, resource, resolved) {
|
|
33
|
+
if (isAbsolute(resource) || hasTraversalSegment(resource)) {
|
|
34
|
+
throw new RecipePackageError(`Recipe ${pkg.name} declares ${key} resource outside the package: ${resource}`);
|
|
35
|
+
}
|
|
36
|
+
const relativePath = relative(resolve(pkg.path), resolved);
|
|
37
|
+
if (relativePath === ".." ||
|
|
38
|
+
relativePath.startsWith("../") ||
|
|
39
|
+
relativePath.startsWith("..\\") ||
|
|
40
|
+
isAbsolute(relativePath)) {
|
|
41
|
+
throw new RecipePackageError(`Recipe ${pkg.name} declares ${key} resource outside the package: ${resource}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function hasGlob(value) {
|
|
45
|
+
return /[*?[\]{}]/.test(value);
|
|
46
|
+
}
|
|
47
|
+
function escapeRegExp(value) {
|
|
48
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
49
|
+
}
|
|
50
|
+
function globToRegExp(glob) {
|
|
51
|
+
const normalized = normalizeResourcePath(glob);
|
|
52
|
+
let pattern = "^";
|
|
53
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
54
|
+
const char = normalized[index];
|
|
55
|
+
const next = normalized[index + 1];
|
|
56
|
+
if (char === "*" && next === "*") {
|
|
57
|
+
const after = normalized[index + 2];
|
|
58
|
+
if (after === "/") {
|
|
59
|
+
pattern += "(?:.*/)?";
|
|
60
|
+
index += 2;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
pattern += ".*";
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (char === "*") {
|
|
68
|
+
pattern += "[^/]*";
|
|
69
|
+
}
|
|
70
|
+
else if (char === "?") {
|
|
71
|
+
pattern += "[^/]";
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
pattern += escapeRegExp(char);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
pattern += "$";
|
|
78
|
+
return new RegExp(pattern);
|
|
79
|
+
}
|
|
80
|
+
function listPackageEntries(root) {
|
|
81
|
+
const entries = [];
|
|
82
|
+
function visit(dir, relativeDir = "") {
|
|
83
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
84
|
+
const relative = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
85
|
+
const fullPath = join(dir, entry.name);
|
|
86
|
+
entries.push(relative);
|
|
87
|
+
if (entry.isDirectory())
|
|
88
|
+
visit(fullPath, relative);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
visit(root);
|
|
92
|
+
return entries;
|
|
93
|
+
}
|
|
94
|
+
function finding(severity, code, message, packageName) {
|
|
95
|
+
return {
|
|
96
|
+
severity,
|
|
97
|
+
code,
|
|
98
|
+
message,
|
|
99
|
+
packageName,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export class RecipePackageError extends Error {
|
|
103
|
+
constructor(message) {
|
|
104
|
+
super(message);
|
|
105
|
+
this.name = "RecipePackageError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function asRecord(value) {
|
|
109
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
110
|
+
? value
|
|
111
|
+
: {};
|
|
112
|
+
}
|
|
113
|
+
function readJsonFile(path) {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
116
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
117
|
+
? parsed
|
|
118
|
+
: {};
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw new RecipePackageError(`Recipe package at ${path} has invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function stringValue(value) {
|
|
125
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
126
|
+
}
|
|
127
|
+
function packageJsonPath(packageDir) {
|
|
128
|
+
const packagePath = join(packageDir, "package.json");
|
|
129
|
+
return existsSync(packagePath) ? packagePath : undefined;
|
|
130
|
+
}
|
|
131
|
+
function piPackageManifestBlock(raw) {
|
|
132
|
+
const pi = asRecord(raw.pi);
|
|
133
|
+
return Object.keys(pi).length > 0 ? pi : undefined;
|
|
134
|
+
}
|
|
135
|
+
function piPackageManifestPath(packageDir) {
|
|
136
|
+
const manifestPath = packageJsonPath(packageDir);
|
|
137
|
+
if (!manifestPath)
|
|
138
|
+
return undefined;
|
|
139
|
+
const raw = readJsonFile(manifestPath);
|
|
140
|
+
return piPackageManifestBlock(raw) ? manifestPath : undefined;
|
|
141
|
+
}
|
|
142
|
+
export function readPiPackageManifest(packageDir) {
|
|
143
|
+
const packagePath = packageJsonPath(packageDir);
|
|
144
|
+
if (!packagePath) {
|
|
145
|
+
throw new RecipePackageError(`Recipe package at ${packageDir} is missing package.json with a pi manifest`);
|
|
146
|
+
}
|
|
147
|
+
const raw = readJsonFile(packagePath);
|
|
148
|
+
const pi = piPackageManifestBlock(raw);
|
|
149
|
+
if (!pi) {
|
|
150
|
+
throw new RecipePackageError(`Recipe package at ${packageDir} is missing package.json pi manifest`);
|
|
151
|
+
}
|
|
152
|
+
const resources = emptyResources();
|
|
153
|
+
for (const key of RESOURCE_KEYS) {
|
|
154
|
+
resources[key] = stringArray(pi[key]).map(normalizeResourcePath);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name: stringValue(raw.name) ?? "",
|
|
158
|
+
version: stringValue(raw.version) ?? "0.0.0",
|
|
159
|
+
...(stringValue(raw.description) ? { description: stringValue(raw.description) } : {}),
|
|
160
|
+
path: packageDir,
|
|
161
|
+
resources,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function resolvePiPackageResourcePaths(pkg, key, opts = {}) {
|
|
165
|
+
const globs = pkg.resources[key];
|
|
166
|
+
const resolved = new Set();
|
|
167
|
+
const entries = globs.some(hasGlob) ? listPackageEntries(pkg.path) : [];
|
|
168
|
+
for (const glob of globs) {
|
|
169
|
+
if (!glob.trim())
|
|
170
|
+
continue;
|
|
171
|
+
assertPackageResourcePath(pkg, key, glob, resolve(pkg.path, glob));
|
|
172
|
+
if (!hasGlob(glob)) {
|
|
173
|
+
const direct = resolve(pkg.path, glob);
|
|
174
|
+
if (!existsSync(direct)) {
|
|
175
|
+
throw new RecipePackageError(`Recipe ${pkg.name} declares missing ${key} resource: ${glob}`);
|
|
176
|
+
}
|
|
177
|
+
resolved.add(direct);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const matcher = globToRegExp(glob);
|
|
181
|
+
const matches = entries
|
|
182
|
+
.filter((entry) => matcher.test(normalizeResourcePath(entry)))
|
|
183
|
+
.map((entry) => resolve(pkg.path, entry));
|
|
184
|
+
if (matches.length === 0) {
|
|
185
|
+
if (opts.allowEmptyGlobMatches)
|
|
186
|
+
continue;
|
|
187
|
+
throw new RecipePackageError(`Recipe ${pkg.name} declares ${key} glob with no matches: ${glob}`);
|
|
188
|
+
}
|
|
189
|
+
for (const match of matches)
|
|
190
|
+
resolved.add(match);
|
|
191
|
+
}
|
|
192
|
+
return [...resolved].sort();
|
|
193
|
+
}
|
|
194
|
+
export function defaultPiPackageResourcePaths(pkg, key) {
|
|
195
|
+
const defaults = {
|
|
196
|
+
agents: [join(pkg.path, "agents")],
|
|
197
|
+
skills: [join(pkg.path, "skills")],
|
|
198
|
+
prompts: [join(pkg.path, "prompts")],
|
|
199
|
+
themes: [join(pkg.path, "themes")],
|
|
200
|
+
};
|
|
201
|
+
return (defaults[key] ?? []).filter((path) => existsSync(path));
|
|
202
|
+
}
|
|
203
|
+
export function packageResourcePaths(pkg, key) {
|
|
204
|
+
if (pkg.resources[key].length > 0) {
|
|
205
|
+
return resolvePiPackageResourcePaths(pkg, key, {
|
|
206
|
+
allowEmptyGlobMatches: key === "extensions" ||
|
|
207
|
+
key === "skills" ||
|
|
208
|
+
key === "prompts" ||
|
|
209
|
+
key === "themes",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return defaultPiPackageResourcePaths(pkg, key);
|
|
213
|
+
}
|
|
214
|
+
export function validatePiPackageManifest(pkg) {
|
|
215
|
+
const findings = [];
|
|
216
|
+
if (!pkg.name.trim()) {
|
|
217
|
+
findings.push(finding("error", "package.name_missing", "Package is missing name"));
|
|
218
|
+
}
|
|
219
|
+
if (!piPackageManifestPath(pkg.path)) {
|
|
220
|
+
findings.push(finding("error", "package.manifest_missing", "Package is missing package.json with a pi manifest", pkg.name));
|
|
221
|
+
}
|
|
222
|
+
if (pkg.resources.agents.length === 0 &&
|
|
223
|
+
!existsSync(join(pkg.path, "agents"))) {
|
|
224
|
+
findings.push(finding("warning", "package.no_agents", "Package declares no agents and has no agents directory", pkg.name));
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
valid: findings.every((item) => item.severity !== "error"),
|
|
228
|
+
findings,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=recipe-package.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type InstalledRecipe, type RecipeStoreOptions } from "./recipe-store.js";
|
|
2
|
+
export type RecipePublishVisibility = "public" | "private";
|
|
3
|
+
export interface RecipePublishCommandResult {
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
}
|
|
7
|
+
export type RecipePublishCommandRunner = (command: string, args: string[], opts: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}) => Promise<RecipePublishCommandResult>;
|
|
11
|
+
export interface RecipePublishOptions extends RecipeStoreOptions {
|
|
12
|
+
github: string;
|
|
13
|
+
visibility: RecipePublishVisibility;
|
|
14
|
+
message?: string;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
commandRunner?: RecipePublishCommandRunner;
|
|
17
|
+
}
|
|
18
|
+
export interface PublishedRecipe {
|
|
19
|
+
recipe: InstalledRecipe;
|
|
20
|
+
recipeDir: string;
|
|
21
|
+
github: string;
|
|
22
|
+
packageName: string;
|
|
23
|
+
shortName: string;
|
|
24
|
+
scopedName: string;
|
|
25
|
+
createdRepository: boolean;
|
|
26
|
+
committed: boolean;
|
|
27
|
+
pushed: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function publishRecipe(target: string, opts: RecipePublishOptions): Promise<PublishedRecipe>;
|
|
30
|
+
//# sourceMappingURL=recipe-publish.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { validateRecipeDirectory } from "./recipe-dev.js";
|
|
6
|
+
import { readPiPackageManifest } from "./recipe-package.js";
|
|
7
|
+
import { addRecipe, customizeRecipe, defaultRecipeStoreDir, findInstalledRecipe, recipePreferredIdentifier, recipeScopedIdentifier, } from "./recipe-store.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
function isSafeGithubName(value) {
|
|
10
|
+
return /^[a-zA-Z0-9_.-]+$/.test(value) && value !== "." && value !== "..";
|
|
11
|
+
}
|
|
12
|
+
function parseGithubTarget(input) {
|
|
13
|
+
const trimmed = input.trim().replace(/^github:/, "").replace(/^@/, "");
|
|
14
|
+
const parts = trimmed.split("/").filter(Boolean);
|
|
15
|
+
const [owner, repo] = parts;
|
|
16
|
+
if (parts.length !== 2 || !owner || !repo || !isSafeGithubName(owner) || !isSafeGithubName(repo)) {
|
|
17
|
+
throw new Error("--github must be in owner/repo form");
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
owner,
|
|
21
|
+
repo,
|
|
22
|
+
fullName: `${owner}/${repo}`,
|
|
23
|
+
packageName: `@${owner}/${repo}`,
|
|
24
|
+
gitUrl: `https://github.com/${owner}/${repo}.git`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function runCommand(command, args, opts, runner) {
|
|
28
|
+
if (runner)
|
|
29
|
+
return runner(command, args, opts);
|
|
30
|
+
try {
|
|
31
|
+
return await execFileAsync(command, args, {
|
|
32
|
+
cwd: opts.cwd,
|
|
33
|
+
env: opts.env,
|
|
34
|
+
maxBuffer: 1024 * 1024,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err && typeof err === "object") {
|
|
39
|
+
const maybe = err;
|
|
40
|
+
const detail = [maybe.stderr, maybe.stdout, maybe.message]
|
|
41
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
42
|
+
.join("\n")
|
|
43
|
+
.trim();
|
|
44
|
+
if (detail)
|
|
45
|
+
throw new Error(detail);
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function commandSucceeds(command, args, opts, runner) {
|
|
51
|
+
try {
|
|
52
|
+
await runCommand(command, args, opts, runner);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readPackageJson(recipeDir) {
|
|
60
|
+
return JSON.parse(readFileSync(join(recipeDir, "package.json"), "utf8"));
|
|
61
|
+
}
|
|
62
|
+
function writePackageName(recipeDir, packageName) {
|
|
63
|
+
const packagePath = join(recipeDir, "package.json");
|
|
64
|
+
const pkg = readPackageJson(recipeDir);
|
|
65
|
+
if (pkg.name === packageName)
|
|
66
|
+
return false;
|
|
67
|
+
pkg.name = packageName;
|
|
68
|
+
writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function ensureGitignore(recipeDir) {
|
|
72
|
+
const gitignorePath = join(recipeDir, ".gitignore");
|
|
73
|
+
const desired = ["node_modules/", "dist/", ".DS_Store", ".env", ".env.*"];
|
|
74
|
+
const existing = existsSync(gitignorePath)
|
|
75
|
+
? readFileSync(gitignorePath, "utf8").split(/\r?\n/)
|
|
76
|
+
: [];
|
|
77
|
+
const existingSet = new Set(existing.map((line) => line.trim()).filter(Boolean));
|
|
78
|
+
const missing = desired.filter((line) => !existingSet.has(line));
|
|
79
|
+
if (missing.length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
const prefix = existing.length > 0 && existing.some((line) => line.trim()) ? "\n" : "";
|
|
82
|
+
writeFileSync(gitignorePath, `${existing.join("\n").replace(/\n*$/, "")}${prefix}${missing.join("\n")}\n`);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
function localPathForInput(input, cwd) {
|
|
86
|
+
const expanded = input === "~"
|
|
87
|
+
? process.env.HOME ?? input
|
|
88
|
+
: input.startsWith("~/")
|
|
89
|
+
? join(process.env.HOME ?? "", input.slice(2))
|
|
90
|
+
: input;
|
|
91
|
+
return isAbsolute(expanded) ? expanded : resolve(cwd, expanded);
|
|
92
|
+
}
|
|
93
|
+
async function editableRecipeDir(target, opts) {
|
|
94
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
95
|
+
const storeDir = opts.storeDir ?? defaultRecipeStoreDir(opts.env);
|
|
96
|
+
const installed = findInstalledRecipe(target, opts);
|
|
97
|
+
if (!installed) {
|
|
98
|
+
const path = localPathForInput(target, cwd);
|
|
99
|
+
if (!existsSync(path))
|
|
100
|
+
throw new Error(`Recipe not found: ${target}`);
|
|
101
|
+
return path;
|
|
102
|
+
}
|
|
103
|
+
if (installed.id.startsWith("local:"))
|
|
104
|
+
return installed.path;
|
|
105
|
+
const customized = await customizeRecipe(target, {
|
|
106
|
+
storeDir,
|
|
107
|
+
cwd,
|
|
108
|
+
env: opts.env,
|
|
109
|
+
force: opts.force,
|
|
110
|
+
});
|
|
111
|
+
return customized.path;
|
|
112
|
+
}
|
|
113
|
+
async function ensureGitRepository(recipeDir, env, runner) {
|
|
114
|
+
if (!existsSync(join(recipeDir, ".git"))) {
|
|
115
|
+
await runCommand("git", ["init"], { cwd: recipeDir, env }, runner);
|
|
116
|
+
}
|
|
117
|
+
const hasHead = await commandSucceeds("git", ["rev-parse", "--verify", "HEAD"], { cwd: recipeDir, env }, runner);
|
|
118
|
+
if (!hasHead) {
|
|
119
|
+
await runCommand("git", ["branch", "-M", "main"], { cwd: recipeDir, env }, runner);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function commitRecipe(recipeDir, message, env, runner) {
|
|
123
|
+
await runCommand("git", ["add", "-A"], { cwd: recipeDir, env }, runner);
|
|
124
|
+
const hasChanges = !(await commandSucceeds("git", ["diff", "--cached", "--quiet"], { cwd: recipeDir, env }, runner));
|
|
125
|
+
if (!hasChanges)
|
|
126
|
+
return false;
|
|
127
|
+
await runCommand("git", ["commit", "-m", message], { cwd: recipeDir, env }, runner);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
async function ensureGithubRepository(target, visibility, recipeDir, env, runner) {
|
|
131
|
+
const exists = await commandSucceeds("gh", ["repo", "view", target.fullName, "--json", "nameWithOwner"], { cwd: recipeDir, env }, runner);
|
|
132
|
+
if (exists)
|
|
133
|
+
return false;
|
|
134
|
+
try {
|
|
135
|
+
await runCommand("gh", ["repo", "create", target.fullName, `--${visibility}`], { cwd: recipeDir, env }, runner);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
140
|
+
throw new Error([
|
|
141
|
+
`Failed to create GitHub repository ${target.fullName}.`,
|
|
142
|
+
message,
|
|
143
|
+
"",
|
|
144
|
+
"Make sure GitHub CLI is installed and authenticated:",
|
|
145
|
+
" gh auth login",
|
|
146
|
+
].join("\n"));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function setOrigin(target, recipeDir, env, runner) {
|
|
150
|
+
const hasOrigin = await commandSucceeds("git", ["remote", "get-url", "origin"], { cwd: recipeDir, env }, runner);
|
|
151
|
+
if (hasOrigin) {
|
|
152
|
+
await runCommand("git", ["remote", "set-url", "origin", target.gitUrl], { cwd: recipeDir, env }, runner);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
await runCommand("git", ["remote", "add", "origin", target.gitUrl], { cwd: recipeDir, env }, runner);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function publishRecipe(target, opts) {
|
|
159
|
+
const github = parseGithubTarget(opts.github);
|
|
160
|
+
const recipeDir = await editableRecipeDir(target, opts);
|
|
161
|
+
mkdirSync(dirname(recipeDir), { recursive: true });
|
|
162
|
+
readPiPackageManifest(recipeDir);
|
|
163
|
+
const report = validateRecipeDirectory(recipeDir);
|
|
164
|
+
const errors = report.findings.filter((finding) => finding.severity === "error");
|
|
165
|
+
if (errors.length > 0) {
|
|
166
|
+
throw new Error([
|
|
167
|
+
`Recipe ${report.manifest.name} is not ready to publish.`,
|
|
168
|
+
...errors.map((finding) => `- ${finding.message}`),
|
|
169
|
+
].join("\n"));
|
|
170
|
+
}
|
|
171
|
+
writePackageName(recipeDir, github.packageName);
|
|
172
|
+
ensureGitignore(recipeDir);
|
|
173
|
+
const env = opts.env ?? process.env;
|
|
174
|
+
await ensureGitRepository(recipeDir, env, opts.commandRunner);
|
|
175
|
+
const committed = await commitRecipe(recipeDir, opts.message ?? `Publish ${github.packageName}`, env, opts.commandRunner);
|
|
176
|
+
const createdRepository = await ensureGithubRepository(github, opts.visibility, recipeDir, env, opts.commandRunner);
|
|
177
|
+
await setOrigin(github, recipeDir, env, opts.commandRunner);
|
|
178
|
+
await runCommand("git", ["push", "-u", "origin", "HEAD:main"], { cwd: recipeDir, env }, opts.commandRunner);
|
|
179
|
+
const recipe = await addRecipe(recipeDir, opts);
|
|
180
|
+
return {
|
|
181
|
+
recipe,
|
|
182
|
+
recipeDir,
|
|
183
|
+
github: github.fullName,
|
|
184
|
+
packageName: github.packageName,
|
|
185
|
+
shortName: recipePreferredIdentifier(recipe),
|
|
186
|
+
scopedName: recipeScopedIdentifier(recipe),
|
|
187
|
+
createdRepository,
|
|
188
|
+
committed,
|
|
189
|
+
pushed: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=recipe-publish.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface RecipeStoreOptions {
|
|
2
|
+
storeDir?: string;
|
|
3
|
+
cwd?: string;
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
}
|
|
6
|
+
export interface InstalledRecipe {
|
|
7
|
+
id: string;
|
|
8
|
+
source: string;
|
|
9
|
+
path: string;
|
|
10
|
+
installedAt: string;
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RecipeStoreFile {
|
|
16
|
+
version: 1;
|
|
17
|
+
recipes: InstalledRecipe[];
|
|
18
|
+
}
|
|
19
|
+
export interface CustomizedRecipe {
|
|
20
|
+
original: InstalledRecipe;
|
|
21
|
+
recipe: InstalledRecipe;
|
|
22
|
+
path: string;
|
|
23
|
+
overwritten: boolean;
|
|
24
|
+
}
|
|
25
|
+
interface GithubRecipeSource {
|
|
26
|
+
kind: "github";
|
|
27
|
+
input: string;
|
|
28
|
+
host: "github.com";
|
|
29
|
+
owner: string;
|
|
30
|
+
repo: string;
|
|
31
|
+
ref?: string;
|
|
32
|
+
subdir?: string;
|
|
33
|
+
}
|
|
34
|
+
interface GitRecipeSource {
|
|
35
|
+
kind: "git";
|
|
36
|
+
input: string;
|
|
37
|
+
url: string;
|
|
38
|
+
ref?: string;
|
|
39
|
+
}
|
|
40
|
+
interface LocalRecipeSource {
|
|
41
|
+
kind: "local";
|
|
42
|
+
input: string;
|
|
43
|
+
path: string;
|
|
44
|
+
}
|
|
45
|
+
export type RecipeSource = GithubRecipeSource | GitRecipeSource | LocalRecipeSource;
|
|
46
|
+
export declare function defaultRecipeStoreDir(env?: NodeJS.ProcessEnv): string;
|
|
47
|
+
export declare function recipeStoreFilePath(storeDir?: string): string;
|
|
48
|
+
export declare function parseRecipeSource(input: string, opts?: RecipeStoreOptions): RecipeSource;
|
|
49
|
+
export declare function recipeSourceId(source: RecipeSource): string;
|
|
50
|
+
export declare function readRecipeStore(storeDir?: string): RecipeStoreFile;
|
|
51
|
+
export declare function addRecipe(input: string, opts?: RecipeStoreOptions & {
|
|
52
|
+
force?: boolean;
|
|
53
|
+
}): Promise<InstalledRecipe>;
|
|
54
|
+
export declare function listRecipes(opts?: RecipeStoreOptions): InstalledRecipe[];
|
|
55
|
+
export declare function recipeScopedIdentifier(recipe: InstalledRecipe): string;
|
|
56
|
+
export declare function recipePreferredIdentifier(recipe: InstalledRecipe): string;
|
|
57
|
+
export declare function findInstalledRecipe(identifier: string, opts?: RecipeStoreOptions): InstalledRecipe | undefined;
|
|
58
|
+
export declare function removeRecipe(identifier: string, opts?: RecipeStoreOptions): InstalledRecipe | undefined;
|
|
59
|
+
export declare function customizeRecipe(identifier: string, opts?: RecipeStoreOptions & {
|
|
60
|
+
force?: boolean;
|
|
61
|
+
}): Promise<CustomizedRecipe>;
|
|
62
|
+
export declare function resolveRecipeDirectory(input: string, opts?: RecipeStoreOptions): string;
|
|
63
|
+
export declare function recipeDisplayName(recipe: InstalledRecipe): string;
|
|
64
|
+
export declare function recipeBasename(path: string): string;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=recipe-store.d.ts.map
|