planmode 0.2.2 → 0.3.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 +1720 -758
- package/dist/mcp.js +315 -155
- package/package.json +2 -1
- package/src/commands/doctor.ts +46 -14
- package/src/commands/init.ts +95 -47
- package/src/commands/install.ts +17 -2
- package/src/commands/interactive.ts +449 -0
- package/src/commands/login.ts +50 -23
- package/src/commands/publish.ts +15 -3
- package/src/commands/record.ts +32 -8
- package/src/commands/run.ts +6 -15
- package/src/commands/search.ts +89 -18
- package/src/commands/snapshot.ts +33 -9
- package/src/commands/test.ts +43 -13
- package/src/commands/update.ts +57 -15
- package/src/index.ts +9 -2
- package/src/lib/installer.ts +57 -29
- package/src/lib/prompts.ts +159 -0
- package/src/lib/publisher.ts +176 -144
package/src/lib/installer.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { parseManifest, readPackageContent } from "./manifest.js";
|
|
|
11
11
|
import { renderTemplate, collectVariableValues } from "./template.js";
|
|
12
12
|
import { logger } from "./logger.js";
|
|
13
13
|
import { trackDownload } from "./analytics.js";
|
|
14
|
+
import { isInteractive, promptForVariables, withSpinner } from "./prompts.js";
|
|
14
15
|
|
|
15
16
|
function getInstallDir(type: PackageType): string {
|
|
16
17
|
switch (type) {
|
|
@@ -37,6 +38,7 @@ export interface InstallOptions {
|
|
|
37
38
|
noInput?: boolean;
|
|
38
39
|
variables?: Record<string, string>;
|
|
39
40
|
projectDir?: string;
|
|
41
|
+
interactive?: boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export async function installPackage(
|
|
@@ -44,6 +46,7 @@ export async function installPackage(
|
|
|
44
46
|
options: InstallOptions = {},
|
|
45
47
|
): Promise<void> {
|
|
46
48
|
const projectDir = options.projectDir ?? process.cwd();
|
|
49
|
+
const interactive = options.interactive ?? (isInteractive() && !options.noInput);
|
|
47
50
|
|
|
48
51
|
// Check lockfile first
|
|
49
52
|
const locked = getLockedVersion(packageName, projectDir);
|
|
@@ -53,44 +56,68 @@ export async function installPackage(
|
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
// Resolve version
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
content = await fetchFileAtTag(
|
|
59
|
+
const resolveAndFetch = async () => {
|
|
60
|
+
const { version, metadata } = await resolveVersion(packageName, options.version);
|
|
61
|
+
const versionMeta = await fetchVersionMetadata(packageName, version);
|
|
62
|
+
return { version, metadata, versionMeta };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const { version, metadata, versionMeta } = interactive
|
|
66
|
+
? await withSpinner(
|
|
67
|
+
`Resolving ${packageName}...`,
|
|
68
|
+
resolveAndFetch,
|
|
69
|
+
`Resolved ${packageName}`,
|
|
70
|
+
)
|
|
71
|
+
: await (async () => {
|
|
72
|
+
logger.info(`Resolving ${packageName}...`);
|
|
73
|
+
return resolveAndFetch();
|
|
74
|
+
})();
|
|
75
|
+
|
|
76
|
+
// Fetch manifest and content
|
|
77
|
+
const fetchContent = async () => {
|
|
78
|
+
const basePath = versionMeta.source.path ? `${versionMeta.source.path}/` : "";
|
|
79
|
+
const manifestRaw = await fetchFileAtTag(
|
|
78
80
|
versionMeta.source.repository,
|
|
79
81
|
versionMeta.source.tag,
|
|
80
|
-
`${basePath}
|
|
82
|
+
`${basePath}planmode.yaml`,
|
|
81
83
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const manifest = parseManifest(manifestRaw);
|
|
85
|
+
|
|
86
|
+
let content: string;
|
|
87
|
+
if (manifest.content) {
|
|
88
|
+
content = manifest.content;
|
|
89
|
+
} else if (manifest.content_file) {
|
|
90
|
+
content = await fetchFileAtTag(
|
|
91
|
+
versionMeta.source.repository,
|
|
92
|
+
versionMeta.source.tag,
|
|
93
|
+
`${basePath}${manifest.content_file}`,
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error("Package has no content or content_file");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { manifest, content };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const { manifest, content: rawContent } = interactive
|
|
103
|
+
? await withSpinner(
|
|
104
|
+
`Fetching ${packageName}@${version}...`,
|
|
105
|
+
fetchContent,
|
|
106
|
+
`Fetched ${packageName}@${version}`,
|
|
107
|
+
)
|
|
108
|
+
: await (async () => {
|
|
109
|
+
logger.info(`Fetching ${packageName}@${version}...`);
|
|
110
|
+
return fetchContent();
|
|
111
|
+
})();
|
|
85
112
|
|
|
86
113
|
// Process variables if templated
|
|
114
|
+
let content = rawContent;
|
|
87
115
|
if (manifest.variables && Object.keys(manifest.variables).length > 0) {
|
|
88
116
|
const provided = options.variables ?? {};
|
|
89
|
-
if (
|
|
90
|
-
const values =
|
|
117
|
+
if (interactive) {
|
|
118
|
+
const values = await promptForVariables(manifest.variables, provided, false);
|
|
91
119
|
content = renderTemplate(content, values);
|
|
92
120
|
} else {
|
|
93
|
-
// Use defaults for non-provided values
|
|
94
121
|
const values = collectVariableValues(manifest.variables, provided);
|
|
95
122
|
content = renderTemplate(content, values);
|
|
96
123
|
}
|
|
@@ -170,6 +197,7 @@ export async function installPackage(
|
|
|
170
197
|
version: range === "*" ? undefined : range,
|
|
171
198
|
projectDir,
|
|
172
199
|
noInput: options.noInput,
|
|
200
|
+
interactive: options.interactive,
|
|
173
201
|
});
|
|
174
202
|
}
|
|
175
203
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import type { VariableDefinition } from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the CLI is running in an interactive terminal.
|
|
6
|
+
* False when piped, in CI, or when --no-input is set.
|
|
7
|
+
*/
|
|
8
|
+
export function isInteractive(): boolean {
|
|
9
|
+
return Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a clack prompt result — if the user cancels (Ctrl+C),
|
|
14
|
+
* prints a cancel message and exits cleanly.
|
|
15
|
+
*/
|
|
16
|
+
export function handleCancel<T>(value: T | symbol): T {
|
|
17
|
+
if (p.isCancel(value)) {
|
|
18
|
+
p.cancel("Cancelled.");
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
return value as T;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompts for a single manifest variable using the appropriate clack widget.
|
|
26
|
+
*/
|
|
27
|
+
async function promptForVariable(
|
|
28
|
+
name: string,
|
|
29
|
+
def: VariableDefinition,
|
|
30
|
+
): Promise<string | number | boolean> {
|
|
31
|
+
switch (def.type) {
|
|
32
|
+
case "enum": {
|
|
33
|
+
const value = await p.select({
|
|
34
|
+
message: def.description || name,
|
|
35
|
+
options: (def.options ?? []).map((opt) => ({
|
|
36
|
+
value: opt,
|
|
37
|
+
label: opt,
|
|
38
|
+
})),
|
|
39
|
+
initialValue: def.default !== undefined ? String(def.default) : undefined,
|
|
40
|
+
});
|
|
41
|
+
return handleCancel(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case "boolean": {
|
|
45
|
+
const value = await p.confirm({
|
|
46
|
+
message: def.description || name,
|
|
47
|
+
initialValue: def.default !== undefined ? Boolean(def.default) : false,
|
|
48
|
+
});
|
|
49
|
+
return handleCancel(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case "number": {
|
|
53
|
+
const value = await p.text({
|
|
54
|
+
message: def.description || name,
|
|
55
|
+
placeholder: def.default !== undefined ? String(def.default) : undefined,
|
|
56
|
+
defaultValue: def.default !== undefined ? String(def.default) : undefined,
|
|
57
|
+
validate(input) {
|
|
58
|
+
if (isNaN(Number(input))) return "Must be a number";
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return Number(handleCancel(value));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "string":
|
|
65
|
+
default: {
|
|
66
|
+
const value = await p.text({
|
|
67
|
+
message: def.description || name,
|
|
68
|
+
placeholder: def.default !== undefined ? String(def.default) : undefined,
|
|
69
|
+
defaultValue: def.default !== undefined ? String(def.default) : undefined,
|
|
70
|
+
validate(input) {
|
|
71
|
+
if (def.required && !input) return `${name} is required`;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
return handleCancel(value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Collects all missing variable values interactively.
|
|
81
|
+
* Merges with already-provided values.
|
|
82
|
+
* Falls back to defaults or throws if not interactive.
|
|
83
|
+
*/
|
|
84
|
+
export async function promptForVariables(
|
|
85
|
+
variableDefs: Record<string, VariableDefinition>,
|
|
86
|
+
provided: Record<string, string>,
|
|
87
|
+
noInput: boolean = false,
|
|
88
|
+
): Promise<Record<string, string | number | boolean>> {
|
|
89
|
+
const values: Record<string, string | number | boolean> = {};
|
|
90
|
+
|
|
91
|
+
for (const [name, def] of Object.entries(variableDefs)) {
|
|
92
|
+
if (def.type === "resolved") continue;
|
|
93
|
+
|
|
94
|
+
if (provided[name] !== undefined) {
|
|
95
|
+
values[name] = coerceValue(provided[name]!, def);
|
|
96
|
+
} else if (def.default !== undefined) {
|
|
97
|
+
if (isInteractive() && !noInput) {
|
|
98
|
+
// In interactive mode, let user confirm/change defaults
|
|
99
|
+
values[name] = await promptForVariable(name, def);
|
|
100
|
+
} else {
|
|
101
|
+
values[name] = def.default;
|
|
102
|
+
}
|
|
103
|
+
} else if (def.required) {
|
|
104
|
+
if (isInteractive() && !noInput) {
|
|
105
|
+
values[name] = await promptForVariable(name, def);
|
|
106
|
+
} else {
|
|
107
|
+
throw new Error(`Missing required variable: ${name} -- ${def.description}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return values;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function coerceValue(
|
|
116
|
+
raw: string,
|
|
117
|
+
def: VariableDefinition,
|
|
118
|
+
): string | number | boolean {
|
|
119
|
+
switch (def.type) {
|
|
120
|
+
case "number":
|
|
121
|
+
return Number(raw);
|
|
122
|
+
case "boolean":
|
|
123
|
+
return raw === "true" || raw === "1" || raw === "yes";
|
|
124
|
+
case "enum":
|
|
125
|
+
if (def.options && !def.options.includes(raw)) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Invalid value "${raw}" for enum variable. Options: ${def.options.join(", ")}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return raw;
|
|
131
|
+
default:
|
|
132
|
+
return raw;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Wraps an async operation with a clack spinner.
|
|
138
|
+
* Only shows spinner when interactive.
|
|
139
|
+
*/
|
|
140
|
+
export async function withSpinner<T>(
|
|
141
|
+
message: string,
|
|
142
|
+
fn: () => Promise<T>,
|
|
143
|
+
successMessage?: string,
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
if (!isInteractive()) {
|
|
146
|
+
return fn();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const s = p.spinner();
|
|
150
|
+
s.start(message);
|
|
151
|
+
try {
|
|
152
|
+
const result = await fn();
|
|
153
|
+
s.stop(successMessage ?? message);
|
|
154
|
+
return result;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
s.stop(`Failed: ${message}`);
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/lib/publisher.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { readManifest, validateManifest } from "./manifest.js";
|
|
|
2
2
|
import { getGitHubToken } from "./config.js";
|
|
3
3
|
import { getRemoteUrl, getHeadSha, createTag, pushTag } from "./git.js";
|
|
4
4
|
import { logger } from "./logger.js";
|
|
5
|
+
import { withSpinner } from "./prompts.js";
|
|
5
6
|
|
|
6
7
|
export interface PublishOptions {
|
|
7
8
|
projectDir?: string;
|
|
8
9
|
token?: string;
|
|
10
|
+
interactive?: boolean;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export interface PublishResult {
|
|
@@ -16,6 +18,7 @@ export interface PublishResult {
|
|
|
16
18
|
|
|
17
19
|
export async function publishPackage(options: PublishOptions = {}): Promise<PublishResult> {
|
|
18
20
|
const cwd = options.projectDir ?? process.cwd();
|
|
21
|
+
const interactive = options.interactive ?? false;
|
|
19
22
|
|
|
20
23
|
// Check auth
|
|
21
24
|
const token = options.token ?? getGitHubToken();
|
|
@@ -24,12 +27,21 @@ export async function publishPackage(options: PublishOptions = {}): Promise<Publ
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
// Read and validate manifest
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const doValidate = async () => {
|
|
31
|
+
const manifest = readManifest(cwd);
|
|
32
|
+
const errors = validateManifest(manifest, true);
|
|
33
|
+
if (errors.length > 0) {
|
|
34
|
+
throw new Error(`Invalid manifest:\n${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
35
|
+
}
|
|
36
|
+
return manifest;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const manifest = interactive
|
|
40
|
+
? await withSpinner("Validating manifest...", doValidate, "Manifest valid")
|
|
41
|
+
: await (async () => {
|
|
42
|
+
logger.info("Reading planmode.yaml...");
|
|
43
|
+
return doValidate();
|
|
44
|
+
})();
|
|
33
45
|
|
|
34
46
|
// Check git remote
|
|
35
47
|
const remoteUrl = await getRemoteUrl(cwd);
|
|
@@ -41,162 +53,182 @@ export async function publishPackage(options: PublishOptions = {}): Promise<Publ
|
|
|
41
53
|
const tag = `v${manifest.version}`;
|
|
42
54
|
|
|
43
55
|
// Create and push tag
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
const doTag = async () => {
|
|
57
|
+
try {
|
|
58
|
+
await createTag(cwd, tag);
|
|
59
|
+
} catch {
|
|
60
|
+
// Tag already exists
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await pushTag(cwd, tag);
|
|
64
|
+
} catch {
|
|
65
|
+
// Tag already pushed
|
|
66
|
+
}
|
|
67
|
+
};
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
await
|
|
69
|
+
if (interactive) {
|
|
70
|
+
await withSpinner(`Creating tag ${tag}...`, doTag, `Tag ${tag} ready`);
|
|
71
|
+
} else {
|
|
72
|
+
logger.info(`Creating tag ${tag}...`);
|
|
73
|
+
await doTag();
|
|
53
74
|
logger.success(`Pushed tag ${tag}`);
|
|
54
|
-
} catch {
|
|
55
|
-
logger.dim(`Tag ${tag} already pushed`);
|
|
56
75
|
}
|
|
57
76
|
|
|
58
77
|
// Fork registry and create PR via GitHub API
|
|
59
|
-
|
|
78
|
+
const doSubmit = async () => {
|
|
79
|
+
const headers = {
|
|
80
|
+
Authorization: `Bearer ${token}`,
|
|
81
|
+
Accept: "application/vnd.github.v3+json",
|
|
82
|
+
"User-Agent": "planmode-cli",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Fork the registry repo (idempotent)
|
|
87
|
+
await fetch("https://api.github.com/repos/kaihannonen/planmode.org/forks", {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Get authenticated user
|
|
93
|
+
const userRes = await fetch("https://api.github.com/user", { headers });
|
|
94
|
+
if (!userRes.ok) {
|
|
95
|
+
throw new Error("Failed to authenticate with GitHub. Check your token.");
|
|
96
|
+
}
|
|
97
|
+
const user = (await userRes.json()) as { login: string };
|
|
98
|
+
|
|
99
|
+
// Create metadata files content
|
|
100
|
+
const repoPath = remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
|
101
|
+
|
|
102
|
+
const metadataContent = JSON.stringify(
|
|
103
|
+
{
|
|
104
|
+
name: manifest.name,
|
|
105
|
+
description: manifest.description,
|
|
106
|
+
author: manifest.author,
|
|
107
|
+
license: manifest.license,
|
|
108
|
+
repository: repoPath,
|
|
109
|
+
category: manifest.category ?? "other",
|
|
110
|
+
tags: manifest.tags ?? [],
|
|
111
|
+
type: manifest.type,
|
|
112
|
+
models: manifest.models ?? [],
|
|
113
|
+
latest_version: manifest.version,
|
|
114
|
+
versions: [manifest.version],
|
|
115
|
+
downloads: 0,
|
|
116
|
+
created_at: new Date().toISOString(),
|
|
117
|
+
updated_at: new Date().toISOString(),
|
|
118
|
+
dependencies: manifest.dependencies,
|
|
119
|
+
variables: manifest.variables,
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
2,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const versionContent = JSON.stringify(
|
|
126
|
+
{
|
|
127
|
+
version: manifest.version,
|
|
128
|
+
published_at: new Date().toISOString(),
|
|
129
|
+
source: {
|
|
130
|
+
repository: repoPath,
|
|
131
|
+
tag,
|
|
132
|
+
sha,
|
|
133
|
+
},
|
|
134
|
+
files: ["planmode.yaml", manifest.content_file ?? "inline"],
|
|
135
|
+
content_hash: `sha256:${sha.slice(0, 16)}`,
|
|
136
|
+
},
|
|
137
|
+
null,
|
|
138
|
+
2,
|
|
139
|
+
);
|
|
60
140
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Accept: "application/vnd.github.v3+json",
|
|
64
|
-
"User-Agent": "planmode-cli",
|
|
65
|
-
"Content-Type": "application/json",
|
|
66
|
-
};
|
|
141
|
+
// Create branch on fork
|
|
142
|
+
const branchName = `add-${manifest.name}-${manifest.version}`;
|
|
67
143
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
144
|
+
// Get main branch ref
|
|
145
|
+
const refRes = await fetch(
|
|
146
|
+
`https://api.github.com/repos/${user.login}/planmode.org/git/ref/heads/main`,
|
|
147
|
+
{ headers },
|
|
148
|
+
);
|
|
73
149
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
}
|
|
150
|
+
if (!refRes.ok) {
|
|
151
|
+
throw new Error("Failed to access registry fork. Make sure the fork exists.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const refData = (await refRes.json()) as { object: { sha: string } };
|
|
155
|
+
const baseSha = refData.object.sha;
|
|
135
156
|
|
|
136
|
-
|
|
137
|
-
|
|
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",
|
|
157
|
+
// Create branch
|
|
158
|
+
await fetch(`https://api.github.com/repos/${user.login}/planmode.org/git/refs`, {
|
|
159
|
+
method: "POST",
|
|
154
160
|
headers,
|
|
155
161
|
body: JSON.stringify({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
branch: branchName,
|
|
162
|
+
ref: `refs/heads/${branchName}`,
|
|
163
|
+
sha: baseSha,
|
|
159
164
|
}),
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Create metadata.json
|
|
168
|
+
await fetch(
|
|
169
|
+
`https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/metadata.json`,
|
|
170
|
+
{
|
|
171
|
+
method: "PUT",
|
|
172
|
+
headers,
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
message: `Add ${manifest.name}@${manifest.version}`,
|
|
175
|
+
content: Buffer.from(metadataContent).toString("base64"),
|
|
176
|
+
branch: branchName,
|
|
177
|
+
}),
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Create version file
|
|
182
|
+
await fetch(
|
|
183
|
+
`https://api.github.com/repos/${user.login}/planmode.org/contents/registry/packages/${manifest.name}/versions/${manifest.version}.json`,
|
|
184
|
+
{
|
|
185
|
+
method: "PUT",
|
|
186
|
+
headers,
|
|
187
|
+
body: JSON.stringify({
|
|
188
|
+
message: `Add ${manifest.name}@${manifest.version} version metadata`,
|
|
189
|
+
content: Buffer.from(versionContent).toString("base64"),
|
|
190
|
+
branch: branchName,
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Create PR
|
|
196
|
+
const prRes = await fetch("https://api.github.com/repos/kaihannonen/planmode.org/pulls", {
|
|
197
|
+
method: "POST",
|
|
168
198
|
headers,
|
|
169
199
|
body: JSON.stringify({
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
title: `Add ${manifest.name}@${manifest.version}`,
|
|
201
|
+
head: `${user.login}:${branchName}`,
|
|
202
|
+
base: "main",
|
|
203
|
+
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\`.`,
|
|
173
204
|
}),
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!prRes.ok) {
|
|
208
|
+
const err = await prRes.text();
|
|
209
|
+
throw new Error(`Failed to create PR: ${err}`);
|
|
210
|
+
}
|
|
193
211
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
const pr = (await prRes.json()) as { html_url: string };
|
|
213
|
+
return pr.html_url;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
let prUrl: string;
|
|
217
|
+
if (interactive) {
|
|
218
|
+
prUrl = await withSpinner(
|
|
219
|
+
"Submitting to registry...",
|
|
220
|
+
doSubmit,
|
|
221
|
+
"Submitted to registry",
|
|
222
|
+
);
|
|
223
|
+
} else {
|
|
224
|
+
logger.info("Submitting to registry...");
|
|
225
|
+
prUrl = await doSubmit();
|
|
226
|
+
logger.success(`Published ${manifest.name}@${manifest.version}`);
|
|
227
|
+
logger.info(`PR: ${prUrl}`);
|
|
228
|
+
}
|
|
197
229
|
|
|
198
230
|
return {
|
|
199
|
-
prUrl
|
|
231
|
+
prUrl,
|
|
200
232
|
packageName: manifest.name,
|
|
201
233
|
version: manifest.version,
|
|
202
234
|
};
|