planmode 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 +2 -0
- package/dist/index.js +1293 -0
- package/package.json +46 -0
- package/src/commands/info.ts +61 -0
- package/src/commands/init.ts +85 -0
- package/src/commands/install.ts +29 -0
- package/src/commands/list.ts +27 -0
- package/src/commands/login.ts +56 -0
- package/src/commands/publish.ts +204 -0
- package/src/commands/run.ts +87 -0
- package/src/commands/search.ts +45 -0
- package/src/commands/uninstall.ts +17 -0
- package/src/commands/update.ts +49 -0
- package/src/index.ts +31 -0
- package/src/lib/claude-md.ts +74 -0
- package/src/lib/config.ts +64 -0
- package/src/lib/git.ts +121 -0
- package/src/lib/installer.ts +204 -0
- package/src/lib/lockfile.ts +63 -0
- package/src/lib/logger.ts +53 -0
- package/src/lib/manifest.ts +119 -0
- package/src/lib/registry.ts +135 -0
- package/src/lib/resolver.ts +120 -0
- package/src/lib/template.ts +110 -0
- package/src/types/index.ts +144 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "planmode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The open source package manager for AI plans, rules, and prompts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"planmode": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"lint": "eslint src/",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^13.1.0",
|
|
19
|
+
"handlebars": "^4.7.8",
|
|
20
|
+
"simple-git": "^3.27.0",
|
|
21
|
+
"yaml": "^2.7.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.13.0",
|
|
25
|
+
"tsup": "^8.4.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/kaihannonen/planmode.org"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"planmode",
|
|
39
|
+
"ai",
|
|
40
|
+
"plans",
|
|
41
|
+
"prompts",
|
|
42
|
+
"rules",
|
|
43
|
+
"claude",
|
|
44
|
+
"package-manager"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { fetchPackageMetadata } from "../lib/registry.js";
|
|
3
|
+
import { logger } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
export const infoCommand = new Command("info")
|
|
6
|
+
.description("Show detailed info about a package")
|
|
7
|
+
.argument("<package>", "Package name")
|
|
8
|
+
.action(async (packageName: string) => {
|
|
9
|
+
try {
|
|
10
|
+
const meta = await fetchPackageMetadata(packageName);
|
|
11
|
+
|
|
12
|
+
logger.blank();
|
|
13
|
+
logger.bold(`${meta.name}@${meta.latest_version}`);
|
|
14
|
+
logger.blank();
|
|
15
|
+
|
|
16
|
+
console.log(` Description: ${meta.description}`);
|
|
17
|
+
console.log(` Type: ${meta.type}`);
|
|
18
|
+
console.log(` Author: ${meta.author}`);
|
|
19
|
+
console.log(` License: ${meta.license}`);
|
|
20
|
+
console.log(` Category: ${meta.category}`);
|
|
21
|
+
console.log(` Downloads: ${meta.downloads.toLocaleString()}`);
|
|
22
|
+
console.log(` Repository: ${meta.repository}`);
|
|
23
|
+
|
|
24
|
+
if (meta.models && meta.models.length > 0) {
|
|
25
|
+
console.log(` Models: ${meta.models.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (meta.tags && meta.tags.length > 0) {
|
|
29
|
+
console.log(` Tags: ${meta.tags.join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(` Versions: ${meta.versions.join(", ")}`);
|
|
33
|
+
|
|
34
|
+
if (meta.dependencies) {
|
|
35
|
+
if (meta.dependencies.rules && meta.dependencies.rules.length > 0) {
|
|
36
|
+
console.log(` Dep (rules): ${meta.dependencies.rules.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
if (meta.dependencies.plans && meta.dependencies.plans.length > 0) {
|
|
39
|
+
console.log(` Dep (plans): ${meta.dependencies.plans.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (meta.variables) {
|
|
44
|
+
logger.blank();
|
|
45
|
+
logger.bold(" Variables:");
|
|
46
|
+
for (const [name, def] of Object.entries(meta.variables)) {
|
|
47
|
+
const required = def.required ? " (required)" : "";
|
|
48
|
+
const defaultVal = def.default !== undefined ? ` [default: ${def.default}]` : "";
|
|
49
|
+
console.log(` ${name}: ${def.type}${required}${defaultVal} — ${def.description}`);
|
|
50
|
+
if (def.options) {
|
|
51
|
+
console.log(` options: ${def.options.join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.blank();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.error((err as Error).message);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { stringify } from "yaml";
|
|
5
|
+
import { logger } from "../lib/logger.js";
|
|
6
|
+
import type { PackageType, Category } from "../types/index.js";
|
|
7
|
+
|
|
8
|
+
async function prompt(question: string): Promise<string> {
|
|
9
|
+
const { createInterface } = await import("node:readline");
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
rl.question(question, (answer) => {
|
|
13
|
+
rl.close();
|
|
14
|
+
resolve(answer.trim());
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const initCommand = new Command("init")
|
|
20
|
+
.description("Initialize a new package in the current directory")
|
|
21
|
+
.action(async () => {
|
|
22
|
+
try {
|
|
23
|
+
logger.blank();
|
|
24
|
+
logger.bold("Initialize a new Planmode package");
|
|
25
|
+
logger.blank();
|
|
26
|
+
|
|
27
|
+
const name = await prompt("Package name: ");
|
|
28
|
+
if (!name) {
|
|
29
|
+
logger.error("Package name is required.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const typeInput = await prompt("Type (plan/rule/prompt) [plan]: ");
|
|
34
|
+
const type = (typeInput || "plan") as PackageType;
|
|
35
|
+
|
|
36
|
+
const description = await prompt("Description: ");
|
|
37
|
+
const author = await prompt("Author (GitHub username): ");
|
|
38
|
+
const license = await prompt("License [MIT]: ") || "MIT";
|
|
39
|
+
const tagsInput = await prompt("Tags (comma-separated): ");
|
|
40
|
+
const tags = tagsInput ? tagsInput.split(",").map((t) => t.trim().toLowerCase()) : [];
|
|
41
|
+
const category = (await prompt("Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: ") || "other") as Category;
|
|
42
|
+
|
|
43
|
+
// Build manifest
|
|
44
|
+
const manifest: Record<string, unknown> = {
|
|
45
|
+
name,
|
|
46
|
+
version: "1.0.0",
|
|
47
|
+
type,
|
|
48
|
+
description,
|
|
49
|
+
author,
|
|
50
|
+
license,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (tags.length > 0) manifest["tags"] = tags;
|
|
54
|
+
manifest["category"] = category;
|
|
55
|
+
|
|
56
|
+
const contentFile = `${type}.md`;
|
|
57
|
+
manifest["content_file"] = contentFile;
|
|
58
|
+
|
|
59
|
+
// Write planmode.yaml
|
|
60
|
+
const yamlContent = stringify(manifest);
|
|
61
|
+
fs.writeFileSync(path.join(process.cwd(), "planmode.yaml"), yamlContent, "utf-8");
|
|
62
|
+
logger.success("Created planmode.yaml");
|
|
63
|
+
|
|
64
|
+
// Write stub content file
|
|
65
|
+
const stubs: Record<string, string> = {
|
|
66
|
+
plan: `# ${name}\n\n1. First step\n2. Second step\n3. Third step\n`,
|
|
67
|
+
rule: `- Rule one\n- Rule two\n- Rule three\n`,
|
|
68
|
+
prompt: `Write your prompt here.\n\nUse {{variable_name}} for template variables.\n`,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
fs.writeFileSync(
|
|
72
|
+
path.join(process.cwd(), contentFile),
|
|
73
|
+
stubs[type] ?? stubs["plan"]!,
|
|
74
|
+
"utf-8",
|
|
75
|
+
);
|
|
76
|
+
logger.success(`Created ${contentFile}`);
|
|
77
|
+
|
|
78
|
+
logger.blank();
|
|
79
|
+
logger.info(`Edit ${contentFile}, then run \`planmode publish\` when ready.`);
|
|
80
|
+
logger.blank();
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.error((err as Error).message);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { installPackage } from "../lib/installer.js";
|
|
3
|
+
import { logger } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
export const installCommand = new Command("install")
|
|
6
|
+
.description("Install a package into the current project")
|
|
7
|
+
.argument("<package>", "Package name (e.g., nextjs-tailwind-starter)")
|
|
8
|
+
.option("-v, --version <version>", "Install specific version")
|
|
9
|
+
.option("--rule", "Force install as a rule to .claude/rules/")
|
|
10
|
+
.option("--no-input", "Fail if any required variable is missing")
|
|
11
|
+
.action(
|
|
12
|
+
async (
|
|
13
|
+
packageName: string,
|
|
14
|
+
options: { version?: string; rule?: boolean; input?: boolean },
|
|
15
|
+
) => {
|
|
16
|
+
try {
|
|
17
|
+
logger.blank();
|
|
18
|
+
await installPackage(packageName, {
|
|
19
|
+
version: options.version,
|
|
20
|
+
forceRule: options.rule,
|
|
21
|
+
noInput: options.input === false,
|
|
22
|
+
});
|
|
23
|
+
logger.blank();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
logger.error((err as Error).message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readLockfile } from "../lib/lockfile.js";
|
|
3
|
+
import { logger } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
export const listCommand = new Command("list")
|
|
6
|
+
.description("List all installed packages")
|
|
7
|
+
.action(() => {
|
|
8
|
+
const lockfile = readLockfile();
|
|
9
|
+
const entries = Object.entries(lockfile.packages);
|
|
10
|
+
|
|
11
|
+
if (entries.length === 0) {
|
|
12
|
+
logger.info("No packages installed. Run `planmode install <package>` to get started.");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
logger.blank();
|
|
17
|
+
logger.table(
|
|
18
|
+
["name", "type", "version", "location"],
|
|
19
|
+
entries.map(([name, entry]) => [
|
|
20
|
+
name,
|
|
21
|
+
entry.type,
|
|
22
|
+
entry.version,
|
|
23
|
+
entry.installed_to,
|
|
24
|
+
]),
|
|
25
|
+
);
|
|
26
|
+
logger.blank();
|
|
27
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { setGitHubToken, getGitHubToken } from "../lib/config.js";
|
|
4
|
+
import { logger } from "../lib/logger.js";
|
|
5
|
+
|
|
6
|
+
export const loginCommand = new Command("login")
|
|
7
|
+
.description("Configure GitHub authentication")
|
|
8
|
+
.option("--token <token>", "GitHub personal access token")
|
|
9
|
+
.option("--gh", "Read token from GitHub CLI (gh auth token)")
|
|
10
|
+
.action(async (options: { token?: string; gh?: boolean }) => {
|
|
11
|
+
let token: string | undefined;
|
|
12
|
+
|
|
13
|
+
if (options.token) {
|
|
14
|
+
token = options.token;
|
|
15
|
+
} else if (options.gh) {
|
|
16
|
+
try {
|
|
17
|
+
token = execSync("gh auth token", { encoding: "utf-8" }).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
logger.error("Failed to read token from GitHub CLI. Make sure `gh` is installed and authenticated.");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
// Interactive prompt via stdin
|
|
24
|
+
const { createInterface } = await import("node:readline");
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
token = await new Promise<string>((resolve) => {
|
|
27
|
+
rl.question("GitHub personal access token: ", (answer) => {
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve(answer.trim());
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!token) {
|
|
35
|
+
logger.error("No token provided.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate token
|
|
40
|
+
logger.info("Validating token...");
|
|
41
|
+
const response = await fetch("https://api.github.com/user", {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${token}`,
|
|
44
|
+
"User-Agent": "planmode-cli",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
logger.error("Invalid token. GitHub API returned: " + response.status);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const user = (await response.json()) as { login: string };
|
|
54
|
+
setGitHubToken(token);
|
|
55
|
+
logger.success(`Authenticated as ${user.login}`);
|
|
56
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readManifest, validateManifest } from "../lib/manifest.js";
|
|
3
|
+
import { getGitHubToken } from "../lib/config.js";
|
|
4
|
+
import { getRemoteUrl, getHeadSha, createTag, pushTag } from "../lib/git.js";
|
|
5
|
+
import { logger } from "../lib/logger.js";
|
|
6
|
+
|
|
7
|
+
export const publishCommand = new Command("publish")
|
|
8
|
+
.description("Publish the current directory as a package to the registry")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
try {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
|
|
13
|
+
// Check auth
|
|
14
|
+
const token = getGitHubToken();
|
|
15
|
+
if (!token) {
|
|
16
|
+
logger.error("Not authenticated. Run `planmode login` first.");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Read and validate manifest
|
|
21
|
+
logger.info("Reading planmode.yaml...");
|
|
22
|
+
const manifest = readManifest(cwd);
|
|
23
|
+
const errors = validateManifest(manifest, true);
|
|
24
|
+
if (errors.length > 0) {
|
|
25
|
+
logger.error("Invalid manifest:");
|
|
26
|
+
for (const err of errors) {
|
|
27
|
+
console.log(` - ${err}`);
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check git remote
|
|
33
|
+
const remoteUrl = await getRemoteUrl(cwd);
|
|
34
|
+
if (!remoteUrl) {
|
|
35
|
+
logger.error("No git remote found. Push your code to GitHub first.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sha = await getHeadSha(cwd);
|
|
40
|
+
const tag = `v${manifest.version}`;
|
|
41
|
+
|
|
42
|
+
// Create and push tag
|
|
43
|
+
logger.info(`Creating tag ${tag}...`);
|
|
44
|
+
try {
|
|
45
|
+
await createTag(cwd, tag);
|
|
46
|
+
} catch {
|
|
47
|
+
logger.dim(`Tag ${tag} already exists, using existing`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await pushTag(cwd, tag);
|
|
52
|
+
logger.success(`Pushed tag ${tag}`);
|
|
53
|
+
} catch {
|
|
54
|
+
logger.dim(`Tag ${tag} already pushed`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Fork registry and create PR via GitHub API
|
|
58
|
+
logger.info("Submitting to registry...");
|
|
59
|
+
|
|
60
|
+
const headers = {
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
Accept: "application/vnd.github.v3+json",
|
|
63
|
+
"User-Agent": "planmode-cli",
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Fork the registry repo (idempotent)
|
|
68
|
+
await fetch("https://api.github.com/repos/planmode/registry/forks", {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Get authenticated user
|
|
74
|
+
const userRes = await fetch("https://api.github.com/user", { headers });
|
|
75
|
+
const user = (await userRes.json()) as { login: string };
|
|
76
|
+
|
|
77
|
+
// Create metadata files content
|
|
78
|
+
const metadataContent = JSON.stringify(
|
|
79
|
+
{
|
|
80
|
+
name: manifest.name,
|
|
81
|
+
description: manifest.description,
|
|
82
|
+
author: manifest.author,
|
|
83
|
+
license: manifest.license,
|
|
84
|
+
repository: remoteUrl
|
|
85
|
+
.replace(/^https?:\/\//, "")
|
|
86
|
+
.replace(/\.git$/, ""),
|
|
87
|
+
category: manifest.category ?? "other",
|
|
88
|
+
tags: manifest.tags ?? [],
|
|
89
|
+
type: manifest.type,
|
|
90
|
+
models: manifest.models ?? [],
|
|
91
|
+
latest_version: manifest.version,
|
|
92
|
+
versions: [manifest.version],
|
|
93
|
+
downloads: 0,
|
|
94
|
+
created_at: new Date().toISOString(),
|
|
95
|
+
updated_at: new Date().toISOString(),
|
|
96
|
+
dependencies: manifest.dependencies,
|
|
97
|
+
variables: manifest.variables,
|
|
98
|
+
},
|
|
99
|
+
null,
|
|
100
|
+
2,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const versionContent = JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
version: manifest.version,
|
|
106
|
+
published_at: new Date().toISOString(),
|
|
107
|
+
source: {
|
|
108
|
+
repository: remoteUrl
|
|
109
|
+
.replace(/^https?:\/\//, "")
|
|
110
|
+
.replace(/\.git$/, ""),
|
|
111
|
+
tag,
|
|
112
|
+
sha,
|
|
113
|
+
},
|
|
114
|
+
files: ["planmode.yaml", manifest.content_file ?? "inline"],
|
|
115
|
+
content_hash: `sha256:${sha.slice(0, 16)}`,
|
|
116
|
+
},
|
|
117
|
+
null,
|
|
118
|
+
2,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Create branch on fork
|
|
122
|
+
const branchName = `add-${manifest.name}-${manifest.version}`;
|
|
123
|
+
|
|
124
|
+
// Get main branch ref
|
|
125
|
+
const refRes = await fetch(
|
|
126
|
+
`https://api.github.com/repos/${user.login}/registry/git/ref/heads/main`,
|
|
127
|
+
{ headers },
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!refRes.ok) {
|
|
131
|
+
logger.error("Failed to access registry fork. Make sure the fork exists.");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const refData = (await refRes.json()) as { object: { sha: string } };
|
|
136
|
+
const baseSha = refData.object.sha;
|
|
137
|
+
|
|
138
|
+
// Create branch
|
|
139
|
+
await fetch(`https://api.github.com/repos/${user.login}/registry/git/refs`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers,
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
ref: `refs/heads/${branchName}`,
|
|
144
|
+
sha: baseSha,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Create metadata.json
|
|
149
|
+
await fetch(
|
|
150
|
+
`https://api.github.com/repos/${user.login}/registry/contents/packages/${manifest.name}/metadata.json`,
|
|
151
|
+
{
|
|
152
|
+
method: "PUT",
|
|
153
|
+
headers,
|
|
154
|
+
body: JSON.stringify({
|
|
155
|
+
message: `Add ${manifest.name}@${manifest.version}`,
|
|
156
|
+
content: Buffer.from(metadataContent).toString("base64"),
|
|
157
|
+
branch: branchName,
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Create version file
|
|
163
|
+
await fetch(
|
|
164
|
+
`https://api.github.com/repos/${user.login}/registry/contents/packages/${manifest.name}/versions/${manifest.version}.json`,
|
|
165
|
+
{
|
|
166
|
+
method: "PUT",
|
|
167
|
+
headers,
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
message: `Add ${manifest.name}@${manifest.version} version metadata`,
|
|
170
|
+
content: Buffer.from(versionContent).toString("base64"),
|
|
171
|
+
branch: branchName,
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Create PR
|
|
177
|
+
const prRes = await fetch("https://api.github.com/repos/planmode/registry/pulls", {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers,
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
title: `Add ${manifest.name}@${manifest.version}`,
|
|
182
|
+
head: `${user.login}:${branchName}`,
|
|
183
|
+
base: "main",
|
|
184
|
+
body: `## New package: ${manifest.name}\n\n- **Type:** ${manifest.type}\n- **Version:** ${manifest.version}\n- **Description:** ${manifest.description}\n- **Author:** ${manifest.author}\n\nSubmitted via \`planmode publish\`.`,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (prRes.ok) {
|
|
189
|
+
const pr = (await prRes.json()) as { html_url: string };
|
|
190
|
+
logger.blank();
|
|
191
|
+
logger.success(`Published ${manifest.name}@${manifest.version}`);
|
|
192
|
+
logger.info(`PR: ${pr.html_url}`);
|
|
193
|
+
} else {
|
|
194
|
+
const err = await prRes.text();
|
|
195
|
+
logger.error(`Failed to create PR: ${err}`);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
logger.blank();
|
|
200
|
+
} catch (err) {
|
|
201
|
+
logger.error((err as Error).message);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseManifest, readPackageContent } from "../lib/manifest.js";
|
|
5
|
+
import { renderTemplate, collectVariableValues, resolveVariable } from "../lib/template.js";
|
|
6
|
+
import { logger } from "../lib/logger.js";
|
|
7
|
+
|
|
8
|
+
export const runCommand = new Command("run")
|
|
9
|
+
.description("Run a templated prompt and output to stdout")
|
|
10
|
+
.argument("<prompt>", "Prompt package name")
|
|
11
|
+
.option("--no-input", "Fail if any required variable is missing")
|
|
12
|
+
.option("--json", "Output as JSON")
|
|
13
|
+
.allowUnknownOption(true)
|
|
14
|
+
.action(async (promptName: string, options: { input?: boolean; json?: boolean }, cmd: Command) => {
|
|
15
|
+
try {
|
|
16
|
+
// Parse dynamic --var flags from raw args
|
|
17
|
+
const vars: Record<string, string> = {};
|
|
18
|
+
const rawArgs = cmd.args.slice(0);
|
|
19
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
20
|
+
const arg = rawArgs[i]!;
|
|
21
|
+
if (arg.startsWith("--") && arg !== "--no-input" && arg !== "--json") {
|
|
22
|
+
const key = arg.slice(2);
|
|
23
|
+
const value = rawArgs[i + 1];
|
|
24
|
+
if (value && !value.startsWith("--")) {
|
|
25
|
+
vars[key] = value;
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Look for prompt locally first
|
|
32
|
+
const localPath = path.join(process.cwd(), "prompts", `${promptName}.md`);
|
|
33
|
+
const localManifestPath = path.join(process.cwd(), "prompts", promptName, "planmode.yaml");
|
|
34
|
+
|
|
35
|
+
let content: string;
|
|
36
|
+
let manifest: ReturnType<typeof parseManifest> | undefined;
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(localManifestPath)) {
|
|
39
|
+
const raw = fs.readFileSync(localManifestPath, "utf-8");
|
|
40
|
+
manifest = parseManifest(raw);
|
|
41
|
+
const dir = path.join(process.cwd(), "prompts", promptName);
|
|
42
|
+
content = readPackageContent(dir, manifest);
|
|
43
|
+
} else if (fs.existsSync(localPath)) {
|
|
44
|
+
content = fs.readFileSync(localPath, "utf-8");
|
|
45
|
+
} else {
|
|
46
|
+
logger.error(`Prompt '${promptName}' not found locally. Install it first: planmode install ${promptName}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Resolve variables
|
|
51
|
+
if (manifest?.variables && Object.keys(manifest.variables).length > 0) {
|
|
52
|
+
const values: Record<string, string | number | boolean> = {};
|
|
53
|
+
|
|
54
|
+
// First pass: resolve non-resolved variables
|
|
55
|
+
for (const [name, def] of Object.entries(manifest.variables)) {
|
|
56
|
+
if (def.type === "resolved") continue;
|
|
57
|
+
if (vars[name] !== undefined) {
|
|
58
|
+
values[name] = vars[name]!;
|
|
59
|
+
} else if (def.default !== undefined) {
|
|
60
|
+
values[name] = def.default;
|
|
61
|
+
} else if (def.required && options.input === false) {
|
|
62
|
+
logger.error(`Missing required variable: --${name}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Second pass: resolve dynamic variables
|
|
68
|
+
for (const [name, def] of Object.entries(manifest.variables)) {
|
|
69
|
+
if (def.type !== "resolved") continue;
|
|
70
|
+
values[name] = await resolveVariable(def, values);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
content = renderTemplate(content, values);
|
|
74
|
+
|
|
75
|
+
if (options.json) {
|
|
76
|
+
console.log(JSON.stringify({ rendered: content, variables: values }, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Output rendered content
|
|
82
|
+
process.stdout.write(content);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.error((err as Error).message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { searchPackages } from "../lib/registry.js";
|
|
3
|
+
import { logger } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
export const searchCommand = new Command("search")
|
|
6
|
+
.description("Search the registry for packages")
|
|
7
|
+
.argument("<query>", "Search query")
|
|
8
|
+
.option("--type <type>", "Filter by type (prompt, rule, plan)")
|
|
9
|
+
.option("--category <category>", "Filter by category")
|
|
10
|
+
.option("--json", "Output as JSON")
|
|
11
|
+
.action(async (query: string, options: { type?: string; category?: string; json?: boolean }) => {
|
|
12
|
+
try {
|
|
13
|
+
const results = await searchPackages(query, {
|
|
14
|
+
type: options.type,
|
|
15
|
+
category: options.category,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (results.length === 0) {
|
|
19
|
+
logger.info("No packages found matching your query.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.json) {
|
|
24
|
+
console.log(JSON.stringify(results, null, 2));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.blank();
|
|
29
|
+
logger.table(
|
|
30
|
+
["name", "type", "version", "description"],
|
|
31
|
+
results.map((pkg) => [
|
|
32
|
+
pkg.name,
|
|
33
|
+
pkg.type,
|
|
34
|
+
pkg.version,
|
|
35
|
+
pkg.description.length > 50
|
|
36
|
+
? pkg.description.slice(0, 50) + "..."
|
|
37
|
+
: pkg.description,
|
|
38
|
+
]),
|
|
39
|
+
);
|
|
40
|
+
logger.blank();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
logger.error((err as Error).message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { uninstallPackage } from "../lib/installer.js";
|
|
3
|
+
import { logger } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
export const uninstallCommand = new Command("uninstall")
|
|
6
|
+
.description("Remove an installed package")
|
|
7
|
+
.argument("<package>", "Package name")
|
|
8
|
+
.action(async (packageName: string) => {
|
|
9
|
+
try {
|
|
10
|
+
logger.blank();
|
|
11
|
+
await uninstallPackage(packageName);
|
|
12
|
+
logger.blank();
|
|
13
|
+
} catch (err) {
|
|
14
|
+
logger.error((err as Error).message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
});
|