planmode 0.1.5 → 0.2.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 +1183 -193
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +2435 -0
- package/package.json +6 -3
- package/src/commands/doctor.ts +43 -0
- package/src/commands/init.ts +17 -36
- package/src/commands/mcp.ts +39 -0
- package/src/commands/publish.ts +3 -191
- package/src/commands/record.ts +76 -0
- package/src/commands/snapshot.ts +46 -0
- package/src/commands/test.ts +45 -0
- package/src/index.ts +11 -1
- package/src/lib/doctor.ts +123 -0
- package/src/lib/init.ts +71 -0
- package/src/lib/installer.ts +20 -1
- package/src/lib/logger.ts +74 -11
- package/src/lib/publisher.ts +203 -0
- package/src/lib/recorder.ts +195 -0
- package/src/lib/snapshot.ts +348 -0
- package/src/lib/templates.ts +60 -0
- package/src/lib/tester.ts +162 -0
- package/src/mcp.ts +853 -0
- package/src/types/index.ts +2 -0
- package/tsup.config.ts +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { readLockfile } from "./lockfile.js";
|
|
5
|
+
import { listImports } from "./claude-md.js";
|
|
6
|
+
|
|
7
|
+
export interface DiagnosticIssue {
|
|
8
|
+
severity: "error" | "warning";
|
|
9
|
+
message: string;
|
|
10
|
+
fix?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DoctorResult {
|
|
14
|
+
issues: DiagnosticIssue[];
|
|
15
|
+
packagesChecked: number;
|
|
16
|
+
healthy: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function computeHash(content: string): string {
|
|
20
|
+
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runDoctor(projectDir: string = process.cwd()): DoctorResult {
|
|
24
|
+
const issues: DiagnosticIssue[] = [];
|
|
25
|
+
const lockfile = readLockfile(projectDir);
|
|
26
|
+
const entries = Object.entries(lockfile.packages);
|
|
27
|
+
|
|
28
|
+
// Check each lockfile entry
|
|
29
|
+
for (const [name, entry] of entries) {
|
|
30
|
+
const fullPath = path.join(projectDir, entry.installed_to);
|
|
31
|
+
|
|
32
|
+
// File exists?
|
|
33
|
+
if (!fs.existsSync(fullPath)) {
|
|
34
|
+
issues.push({
|
|
35
|
+
severity: "error",
|
|
36
|
+
message: `Missing file for "${name}": ${entry.installed_to}`,
|
|
37
|
+
fix: `Run \`planmode install ${name}\` to reinstall`,
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Content hash matches?
|
|
43
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
44
|
+
const actualHash = computeHash(content);
|
|
45
|
+
if (actualHash !== entry.content_hash) {
|
|
46
|
+
issues.push({
|
|
47
|
+
severity: "warning",
|
|
48
|
+
message: `Content hash mismatch for "${name}" at ${entry.installed_to}`,
|
|
49
|
+
fix: "File was modified locally. Run `planmode update " + name + "` to restore, or ignore if intentional",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check CLAUDE.md imports match installed plans
|
|
55
|
+
const claudeMdPath = path.join(projectDir, "CLAUDE.md");
|
|
56
|
+
const imports = listImports(projectDir);
|
|
57
|
+
const installedPlans = entries
|
|
58
|
+
.filter(([, entry]) => entry.type === "plan")
|
|
59
|
+
.map(([name]) => name);
|
|
60
|
+
|
|
61
|
+
// Plans in lockfile but missing from CLAUDE.md
|
|
62
|
+
for (const planName of installedPlans) {
|
|
63
|
+
if (!imports.includes(planName)) {
|
|
64
|
+
issues.push({
|
|
65
|
+
severity: "error",
|
|
66
|
+
message: `Plan "${planName}" is installed but missing from CLAUDE.md imports`,
|
|
67
|
+
fix: `Add \`- @plans/${planName}.md\` to the # Planmode section of CLAUDE.md`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Imports in CLAUDE.md that aren't in the lockfile
|
|
73
|
+
for (const importName of imports) {
|
|
74
|
+
if (!installedPlans.includes(importName)) {
|
|
75
|
+
// Check if the file at least exists
|
|
76
|
+
const filePath = path.join(projectDir, "plans", `${importName}.md`);
|
|
77
|
+
if (!fs.existsSync(filePath)) {
|
|
78
|
+
issues.push({
|
|
79
|
+
severity: "error",
|
|
80
|
+
message: `CLAUDE.md imports "${importName}" but the file doesn't exist at plans/${importName}.md`,
|
|
81
|
+
fix: `Run \`planmode install ${importName}\` or remove the import from CLAUDE.md`,
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
issues.push({
|
|
85
|
+
severity: "warning",
|
|
86
|
+
message: `CLAUDE.md imports "${importName}" but it's not tracked in planmode.lock`,
|
|
87
|
+
fix: "This plan was added manually. No action needed unless you want lockfile tracking.",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check CLAUDE.md exists if there are plans
|
|
94
|
+
if (installedPlans.length > 0 && !fs.existsSync(claudeMdPath)) {
|
|
95
|
+
issues.push({
|
|
96
|
+
severity: "error",
|
|
97
|
+
message: "CLAUDE.md is missing but plans are installed",
|
|
98
|
+
fix: "Run `planmode install <any-plan>` to recreate it, or create it manually with a # Planmode section",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for orphaned files in plans/ that aren't tracked
|
|
103
|
+
const plansDir = path.join(projectDir, "plans");
|
|
104
|
+
if (fs.existsSync(plansDir)) {
|
|
105
|
+
const planFiles = fs.readdirSync(plansDir).filter((f) => f.endsWith(".md"));
|
|
106
|
+
for (const file of planFiles) {
|
|
107
|
+
const name = file.replace(/\.md$/, "");
|
|
108
|
+
if (!lockfile.packages[name]) {
|
|
109
|
+
issues.push({
|
|
110
|
+
severity: "warning",
|
|
111
|
+
message: `Untracked plan file: plans/${file}`,
|
|
112
|
+
fix: "This file isn't managed by planmode. Ignore if intentional.",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
issues,
|
|
120
|
+
packagesChecked: entries.length,
|
|
121
|
+
healthy: issues.filter((i) => i.severity === "error").length === 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
package/src/lib/init.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stringify } from "yaml";
|
|
4
|
+
import type { PackageType, Category } from "../types/index.js";
|
|
5
|
+
import { getPlanTemplate, getRuleTemplate, getPromptTemplate } from "./templates.js";
|
|
6
|
+
|
|
7
|
+
export interface InitOptions {
|
|
8
|
+
name: string;
|
|
9
|
+
type: PackageType;
|
|
10
|
+
description: string;
|
|
11
|
+
author: string;
|
|
12
|
+
license?: string;
|
|
13
|
+
tags?: string[];
|
|
14
|
+
category?: Category;
|
|
15
|
+
projectDir?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InitResult {
|
|
19
|
+
files: string[];
|
|
20
|
+
manifestPath: string;
|
|
21
|
+
contentPath: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createPackage(options: InitOptions): InitResult {
|
|
25
|
+
const {
|
|
26
|
+
name,
|
|
27
|
+
type,
|
|
28
|
+
description,
|
|
29
|
+
author,
|
|
30
|
+
license = "MIT",
|
|
31
|
+
tags = [],
|
|
32
|
+
category = "other",
|
|
33
|
+
projectDir = process.cwd(),
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const manifest: Record<string, unknown> = {
|
|
37
|
+
name,
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
type,
|
|
40
|
+
description,
|
|
41
|
+
author,
|
|
42
|
+
license,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (tags.length > 0) manifest["tags"] = tags;
|
|
46
|
+
manifest["category"] = category;
|
|
47
|
+
|
|
48
|
+
const contentFile = `${type}.md`;
|
|
49
|
+
manifest["content_file"] = contentFile;
|
|
50
|
+
|
|
51
|
+
// Write planmode.yaml
|
|
52
|
+
const yamlContent = stringify(manifest);
|
|
53
|
+
const manifestPath = path.join(projectDir, "planmode.yaml");
|
|
54
|
+
fs.writeFileSync(manifestPath, yamlContent, "utf-8");
|
|
55
|
+
|
|
56
|
+
// Write stub content file
|
|
57
|
+
const stubs: Record<string, string> = {
|
|
58
|
+
plan: getPlanTemplate(name),
|
|
59
|
+
rule: getRuleTemplate(name),
|
|
60
|
+
prompt: getPromptTemplate(name),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const contentPath = path.join(projectDir, contentFile);
|
|
64
|
+
fs.writeFileSync(contentPath, stubs[type] ?? stubs["plan"]!, "utf-8");
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
files: ["planmode.yaml", contentFile],
|
|
68
|
+
manifestPath,
|
|
69
|
+
contentPath,
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/lib/installer.ts
CHANGED
|
@@ -113,6 +113,16 @@ export async function installPackage(
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Verify content hash against registry
|
|
117
|
+
const computedHash = contentHash(content);
|
|
118
|
+
if (versionMeta.content_hash && computedHash !== versionMeta.content_hash) {
|
|
119
|
+
logger.warn(
|
|
120
|
+
`Content hash mismatch for ${packageName}@${version}. ` +
|
|
121
|
+
`Expected ${versionMeta.content_hash.slice(0, 20)}..., got ${computedHash.slice(0, 20)}... ` +
|
|
122
|
+
`The package content may have been modified after review.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
// Create directory and write file
|
|
117
127
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
118
128
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
@@ -122,7 +132,16 @@ export async function installPackage(
|
|
|
122
132
|
// Update CLAUDE.md for plans
|
|
123
133
|
if (type === "plan") {
|
|
124
134
|
addImport(packageName, projectDir);
|
|
125
|
-
logger.dim(`Added @
|
|
135
|
+
logger.dim(`Added @plans/${packageName}.md to CLAUDE.md`);
|
|
136
|
+
logger.dim(`Claude Code will automatically see this plan in your next conversation.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (type === "rule") {
|
|
140
|
+
logger.dim(`Rule is active — Claude Code auto-loads all files in .claude/rules/.`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type === "prompt") {
|
|
144
|
+
logger.dim(`Run it with: planmode run ${packageName}`);
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
// Update lockfile
|
package/src/lib/logger.ts
CHANGED
|
@@ -6,29 +6,80 @@ const CYAN = "\x1b[36m";
|
|
|
6
6
|
const DIM = "\x1b[2m";
|
|
7
7
|
const BOLD = "\x1b[1m";
|
|
8
8
|
|
|
9
|
+
function stripAnsi(str: string): string {
|
|
10
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let capturing = false;
|
|
14
|
+
let captured: string[] = [];
|
|
15
|
+
|
|
9
16
|
export const logger = {
|
|
17
|
+
capture() {
|
|
18
|
+
capturing = true;
|
|
19
|
+
captured = [];
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
flush(): string[] {
|
|
23
|
+
const messages = captured;
|
|
24
|
+
captured = [];
|
|
25
|
+
capturing = false;
|
|
26
|
+
return messages;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
isCapturing(): boolean {
|
|
30
|
+
return capturing;
|
|
31
|
+
},
|
|
32
|
+
|
|
10
33
|
info(msg: string) {
|
|
11
|
-
|
|
34
|
+
const text = `info ${msg}`;
|
|
35
|
+
if (capturing) {
|
|
36
|
+
captured.push(stripAnsi(text));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(`${CYAN}info${RESET} ${msg}`);
|
|
39
|
+
}
|
|
12
40
|
},
|
|
13
41
|
|
|
14
42
|
success(msg: string) {
|
|
15
|
-
|
|
43
|
+
const text = `✓ ${msg}`;
|
|
44
|
+
if (capturing) {
|
|
45
|
+
captured.push(stripAnsi(text));
|
|
46
|
+
} else {
|
|
47
|
+
console.log(`${GREEN}✓${RESET} ${msg}`);
|
|
48
|
+
}
|
|
16
49
|
},
|
|
17
50
|
|
|
18
51
|
warn(msg: string) {
|
|
19
|
-
|
|
52
|
+
const text = `warn ${msg}`;
|
|
53
|
+
if (capturing) {
|
|
54
|
+
captured.push(stripAnsi(text));
|
|
55
|
+
} else {
|
|
56
|
+
console.log(`${YELLOW}warn${RESET} ${msg}`);
|
|
57
|
+
}
|
|
20
58
|
},
|
|
21
59
|
|
|
22
60
|
error(msg: string) {
|
|
23
|
-
|
|
61
|
+
const text = `error ${msg}`;
|
|
62
|
+
if (capturing) {
|
|
63
|
+
captured.push(stripAnsi(text));
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`${RED}error${RESET} ${msg}`);
|
|
66
|
+
}
|
|
24
67
|
},
|
|
25
68
|
|
|
26
69
|
dim(msg: string) {
|
|
27
|
-
|
|
70
|
+
if (capturing) {
|
|
71
|
+
captured.push(msg);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`${DIM}${msg}${RESET}`);
|
|
74
|
+
}
|
|
28
75
|
},
|
|
29
76
|
|
|
30
77
|
bold(msg: string) {
|
|
31
|
-
|
|
78
|
+
if (capturing) {
|
|
79
|
+
captured.push(msg);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`${BOLD}${msg}${RESET}`);
|
|
82
|
+
}
|
|
32
83
|
},
|
|
33
84
|
|
|
34
85
|
table(headers: string[], rows: string[][]) {
|
|
@@ -39,15 +90,27 @@ export const logger = {
|
|
|
39
90
|
const header = headers
|
|
40
91
|
.map((h, i) => h.toUpperCase().padEnd(colWidths[i]!))
|
|
41
92
|
.join(" ");
|
|
42
|
-
console.log(` ${DIM}${header}${RESET}`);
|
|
43
93
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
94
|
+
if (capturing) {
|
|
95
|
+
captured.push(` ${header}`);
|
|
96
|
+
for (const row of rows) {
|
|
97
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths[i]!)).join(" ");
|
|
98
|
+
captured.push(` ${line}`);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log(` ${DIM}${header}${RESET}`);
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths[i]!)).join(" ");
|
|
104
|
+
console.log(` ${line}`);
|
|
105
|
+
}
|
|
47
106
|
}
|
|
48
107
|
},
|
|
49
108
|
|
|
50
109
|
blank() {
|
|
51
|
-
|
|
110
|
+
if (capturing) {
|
|
111
|
+
captured.push("");
|
|
112
|
+
} else {
|
|
113
|
+
console.log();
|
|
114
|
+
}
|
|
52
115
|
},
|
|
53
116
|
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { readManifest, validateManifest } from "./manifest.js";
|
|
2
|
+
import { getGitHubToken } from "./config.js";
|
|
3
|
+
import { getRemoteUrl, getHeadSha, createTag, pushTag } from "./git.js";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export interface PublishOptions {
|
|
7
|
+
projectDir?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PublishResult {
|
|
12
|
+
prUrl: string;
|
|
13
|
+
packageName: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function publishPackage(options: PublishOptions = {}): Promise<PublishResult> {
|
|
18
|
+
const cwd = options.projectDir ?? process.cwd();
|
|
19
|
+
|
|
20
|
+
// Check auth
|
|
21
|
+
const token = options.token ?? getGitHubToken();
|
|
22
|
+
if (!token) {
|
|
23
|
+
throw new Error("Not authenticated. Run `planmode login` first.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Read and validate manifest
|
|
27
|
+
logger.info("Reading planmode.yaml...");
|
|
28
|
+
const manifest = readManifest(cwd);
|
|
29
|
+
const errors = validateManifest(manifest, true);
|
|
30
|
+
if (errors.length > 0) {
|
|
31
|
+
throw new Error(`Invalid manifest:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check git remote
|
|
35
|
+
const remoteUrl = await getRemoteUrl(cwd);
|
|
36
|
+
if (!remoteUrl) {
|
|
37
|
+
throw new Error("No git remote found. Push your code to GitHub first.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sha = await getHeadSha(cwd);
|
|
41
|
+
const tag = `v${manifest.version}`;
|
|
42
|
+
|
|
43
|
+
// Create and push tag
|
|
44
|
+
logger.info(`Creating tag ${tag}...`);
|
|
45
|
+
try {
|
|
46
|
+
await createTag(cwd, tag);
|
|
47
|
+
} catch {
|
|
48
|
+
logger.dim(`Tag ${tag} already exists, using existing`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await pushTag(cwd, tag);
|
|
53
|
+
logger.success(`Pushed tag ${tag}`);
|
|
54
|
+
} catch {
|
|
55
|
+
logger.dim(`Tag ${tag} already pushed`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fork registry and create PR via GitHub API
|
|
59
|
+
logger.info("Submitting to registry...");
|
|
60
|
+
|
|
61
|
+
const headers = {
|
|
62
|
+
Authorization: `Bearer ${token}`,
|
|
63
|
+
Accept: "application/vnd.github.v3+json",
|
|
64
|
+
"User-Agent": "planmode-cli",
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Fork the registry repo (idempotent)
|
|
69
|
+
await fetch("https://api.github.com/repos/kaihannonen/planmode.org/forks", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Get authenticated user
|
|
75
|
+
const userRes = await fetch("https://api.github.com/user", { headers });
|
|
76
|
+
if (!userRes.ok) {
|
|
77
|
+
throw new Error("Failed to authenticate with GitHub. Check your token.");
|
|
78
|
+
}
|
|
79
|
+
const user = (await userRes.json()) as { login: string };
|
|
80
|
+
|
|
81
|
+
// Create metadata files content
|
|
82
|
+
const repoPath = remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
|
83
|
+
|
|
84
|
+
const metadataContent = JSON.stringify(
|
|
85
|
+
{
|
|
86
|
+
name: manifest.name,
|
|
87
|
+
description: manifest.description,
|
|
88
|
+
author: manifest.author,
|
|
89
|
+
license: manifest.license,
|
|
90
|
+
repository: repoPath,
|
|
91
|
+
category: manifest.category ?? "other",
|
|
92
|
+
tags: manifest.tags ?? [],
|
|
93
|
+
type: manifest.type,
|
|
94
|
+
models: manifest.models ?? [],
|
|
95
|
+
latest_version: manifest.version,
|
|
96
|
+
versions: [manifest.version],
|
|
97
|
+
downloads: 0,
|
|
98
|
+
created_at: new Date().toISOString(),
|
|
99
|
+
updated_at: new Date().toISOString(),
|
|
100
|
+
dependencies: manifest.dependencies,
|
|
101
|
+
variables: manifest.variables,
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const versionContent = JSON.stringify(
|
|
108
|
+
{
|
|
109
|
+
version: manifest.version,
|
|
110
|
+
published_at: new Date().toISOString(),
|
|
111
|
+
source: {
|
|
112
|
+
repository: repoPath,
|
|
113
|
+
tag,
|
|
114
|
+
sha,
|
|
115
|
+
},
|
|
116
|
+
files: ["planmode.yaml", manifest.content_file ?? "inline"],
|
|
117
|
+
content_hash: `sha256:${sha.slice(0, 16)}`,
|
|
118
|
+
},
|
|
119
|
+
null,
|
|
120
|
+
2,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Create branch on fork
|
|
124
|
+
const branchName = `add-${manifest.name}-${manifest.version}`;
|
|
125
|
+
|
|
126
|
+
// Get main branch ref
|
|
127
|
+
const refRes = await fetch(
|
|
128
|
+
`https://api.github.com/repos/${user.login}/planmode.org/git/ref/heads/main`,
|
|
129
|
+
{ headers },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!refRes.ok) {
|
|
133
|
+
throw new Error("Failed to access registry fork. Make sure the fork exists.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const refData = (await refRes.json()) as { object: { sha: string } };
|
|
137
|
+
const baseSha = refData.object.sha;
|
|
138
|
+
|
|
139
|
+
// Create branch
|
|
140
|
+
await fetch(`https://api.github.com/repos/${user.login}/planmode.org/git/refs`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers,
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
ref: `refs/heads/${branchName}`,
|
|
145
|
+
sha: baseSha,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Create metadata.json
|
|
150
|
+
await fetch(
|
|
151
|
+
`https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/metadata.json`,
|
|
152
|
+
{
|
|
153
|
+
method: "PUT",
|
|
154
|
+
headers,
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
message: `Add ${manifest.name}@${manifest.version}`,
|
|
157
|
+
content: Buffer.from(metadataContent).toString("base64"),
|
|
158
|
+
branch: branchName,
|
|
159
|
+
}),
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Create version file
|
|
164
|
+
await fetch(
|
|
165
|
+
`https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/versions/${manifest.version}.json`,
|
|
166
|
+
{
|
|
167
|
+
method: "PUT",
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
message: `Add ${manifest.name}@${manifest.version} version metadata`,
|
|
171
|
+
content: Buffer.from(versionContent).toString("base64"),
|
|
172
|
+
branch: branchName,
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Create PR
|
|
178
|
+
const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers,
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
title: `Add ${manifest.name}@${manifest.version}`,
|
|
183
|
+
head: `${user.login}:${branchName}`,
|
|
184
|
+
base: "main",
|
|
185
|
+
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\`.`,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (!prRes.ok) {
|
|
190
|
+
const err = await prRes.text();
|
|
191
|
+
throw new Error(`Failed to create PR: ${err}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const pr = (await prRes.json()) as { html_url: string };
|
|
195
|
+
logger.success(`Published ${manifest.name}@${manifest.version}`);
|
|
196
|
+
logger.info(`PR: ${pr.html_url}`);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
prUrl: pr.html_url,
|
|
200
|
+
packageName: manifest.name,
|
|
201
|
+
version: manifest.version,
|
|
202
|
+
};
|
|
203
|
+
}
|