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/dist/index.js
ADDED
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/install.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/lib/installer.ts
|
|
10
|
+
import fs7 from "fs";
|
|
11
|
+
import path7 from "path";
|
|
12
|
+
import crypto from "crypto";
|
|
13
|
+
|
|
14
|
+
// src/lib/registry.ts
|
|
15
|
+
import fs2 from "fs";
|
|
16
|
+
import path2 from "path";
|
|
17
|
+
|
|
18
|
+
// src/lib/config.ts
|
|
19
|
+
import fs from "fs";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import os from "os";
|
|
22
|
+
import { parse, stringify } from "yaml";
|
|
23
|
+
var CONFIG_DIR = path.join(os.homedir(), ".planmode");
|
|
24
|
+
var CONFIG_PATH = path.join(CONFIG_DIR, "config");
|
|
25
|
+
var CACHE_DIR = path.join(CONFIG_DIR, "cache");
|
|
26
|
+
function getCacheDir() {
|
|
27
|
+
const config = readConfig();
|
|
28
|
+
return config.cache?.dir ?? CACHE_DIR;
|
|
29
|
+
}
|
|
30
|
+
function getCacheTTL() {
|
|
31
|
+
const config = readConfig();
|
|
32
|
+
return config.cache?.ttl ?? 3600;
|
|
33
|
+
}
|
|
34
|
+
function readConfig() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
37
|
+
return parse(raw) ?? {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeConfig(config) {
|
|
43
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
+
fs.writeFileSync(CONFIG_PATH, stringify(config), "utf-8");
|
|
45
|
+
}
|
|
46
|
+
function getGitHubToken() {
|
|
47
|
+
const envToken = process.env["PLANMODE_GITHUB_TOKEN"];
|
|
48
|
+
if (envToken) return envToken;
|
|
49
|
+
const config = readConfig();
|
|
50
|
+
return config.auth?.github_token;
|
|
51
|
+
}
|
|
52
|
+
function setGitHubToken(token) {
|
|
53
|
+
const config = readConfig();
|
|
54
|
+
config.auth = { ...config.auth, github_token: token };
|
|
55
|
+
writeConfig(config);
|
|
56
|
+
}
|
|
57
|
+
function getRegistries() {
|
|
58
|
+
const config = readConfig();
|
|
59
|
+
return {
|
|
60
|
+
default: "github.com/planmode/registry",
|
|
61
|
+
...config.registries
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/lib/registry.ts
|
|
66
|
+
var INDEX_CACHE_FILE = "index.json";
|
|
67
|
+
function getHeaders() {
|
|
68
|
+
const headers = {
|
|
69
|
+
"Accept": "application/vnd.github.v3.raw+json",
|
|
70
|
+
"User-Agent": "planmode-cli"
|
|
71
|
+
};
|
|
72
|
+
const token = getGitHubToken();
|
|
73
|
+
if (token) {
|
|
74
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
75
|
+
}
|
|
76
|
+
return headers;
|
|
77
|
+
}
|
|
78
|
+
function registryRawUrl(registryUrl, filePath) {
|
|
79
|
+
const match = registryUrl.match(/^github\.com\/([^/]+)\/([^/]+)$/);
|
|
80
|
+
if (!match) {
|
|
81
|
+
throw new Error(`Invalid registry URL: ${registryUrl}`);
|
|
82
|
+
}
|
|
83
|
+
return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/main/${filePath}`;
|
|
84
|
+
}
|
|
85
|
+
function resolveRegistry(packageName) {
|
|
86
|
+
const registries = getRegistries();
|
|
87
|
+
if (packageName.startsWith("@")) {
|
|
88
|
+
const scope = packageName.split("/")[0].slice(1);
|
|
89
|
+
const registryUrl = registries[scope];
|
|
90
|
+
if (!registryUrl) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`No registry configured for scope "@${scope}". Run: planmode registry add ${scope} <url>`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return registryUrl;
|
|
96
|
+
}
|
|
97
|
+
return registries["default"];
|
|
98
|
+
}
|
|
99
|
+
async function fetchIndex(registryUrl) {
|
|
100
|
+
const url = registryUrl ?? getRegistries()["default"];
|
|
101
|
+
const cacheDir = getCacheDir();
|
|
102
|
+
const cachePath = path2.join(cacheDir, INDEX_CACHE_FILE);
|
|
103
|
+
const ttl = getCacheTTL();
|
|
104
|
+
try {
|
|
105
|
+
const stat = fs2.statSync(cachePath);
|
|
106
|
+
const ageSeconds = (Date.now() - stat.mtimeMs) / 1e3;
|
|
107
|
+
if (ageSeconds < ttl) {
|
|
108
|
+
const cached = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
|
|
109
|
+
return cached;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
const rawUrl = registryRawUrl(url, "index.json");
|
|
114
|
+
const response = await fetch(rawUrl, { headers: getHeaders() });
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Failed to fetch registry index: ${response.status} ${response.statusText}`);
|
|
117
|
+
}
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
120
|
+
fs2.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
|
|
121
|
+
return data;
|
|
122
|
+
}
|
|
123
|
+
async function searchPackages(query, options) {
|
|
124
|
+
const index = await fetchIndex();
|
|
125
|
+
const q = query.toLowerCase();
|
|
126
|
+
let results = index.packages.filter((pkg) => {
|
|
127
|
+
const searchable = [pkg.name, pkg.description, pkg.author, ...pkg.tags].join(" ").toLowerCase();
|
|
128
|
+
return searchable.includes(q);
|
|
129
|
+
});
|
|
130
|
+
if (options?.type) {
|
|
131
|
+
results = results.filter((pkg) => pkg.type === options.type);
|
|
132
|
+
}
|
|
133
|
+
if (options?.category) {
|
|
134
|
+
results = results.filter((pkg) => pkg.category === options.category);
|
|
135
|
+
}
|
|
136
|
+
return results.sort((a, b) => b.downloads - a.downloads);
|
|
137
|
+
}
|
|
138
|
+
async function fetchPackageMetadata(packageName) {
|
|
139
|
+
const registryUrl = resolveRegistry(packageName);
|
|
140
|
+
const name = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
|
|
141
|
+
const rawUrl = registryRawUrl(registryUrl, `packages/${name}/metadata.json`);
|
|
142
|
+
const response = await fetch(rawUrl, { headers: getHeaders() });
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
if (response.status === 404) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Package '${packageName}' not found in registry. Run \`planmode search <query>\` to find packages.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
throw new Error(`Failed to fetch package metadata: ${response.status}`);
|
|
150
|
+
}
|
|
151
|
+
return await response.json();
|
|
152
|
+
}
|
|
153
|
+
async function fetchVersionMetadata(packageName, version) {
|
|
154
|
+
const registryUrl = resolveRegistry(packageName);
|
|
155
|
+
const name = packageName.startsWith("@") ? packageName.split("/")[1] : packageName;
|
|
156
|
+
const rawUrl = registryRawUrl(registryUrl, `packages/${name}/versions/${version}.json`);
|
|
157
|
+
const response = await fetch(rawUrl, { headers: getHeaders() });
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
if (response.status === 404) {
|
|
160
|
+
throw new Error(`Version '${version}' not found for '${packageName}'.`);
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`Failed to fetch version metadata: ${response.status}`);
|
|
163
|
+
}
|
|
164
|
+
return await response.json();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/lib/resolver.ts
|
|
168
|
+
function parseSemver(version) {
|
|
169
|
+
const parts = version.split(".").map(Number);
|
|
170
|
+
return { major: parts[0], minor: parts[1], patch: parts[2] };
|
|
171
|
+
}
|
|
172
|
+
function parseVersionRange(range) {
|
|
173
|
+
if (range === "*") {
|
|
174
|
+
return { operator: "any", major: 0, minor: 0, patch: 0 };
|
|
175
|
+
}
|
|
176
|
+
if (range.startsWith("^")) {
|
|
177
|
+
const { major: major2, minor: minor2, patch: patch2 } = parseSemver(range.slice(1));
|
|
178
|
+
return { operator: "caret", major: major2, minor: minor2, patch: patch2 };
|
|
179
|
+
}
|
|
180
|
+
if (range.startsWith("~")) {
|
|
181
|
+
const { major: major2, minor: minor2, patch: patch2 } = parseSemver(range.slice(1));
|
|
182
|
+
return { operator: "tilde", major: major2, minor: minor2, patch: patch2 };
|
|
183
|
+
}
|
|
184
|
+
if (range.startsWith(">=")) {
|
|
185
|
+
const { major: major2, minor: minor2, patch: patch2 } = parseSemver(range.slice(2));
|
|
186
|
+
return { operator: "gte", major: major2, minor: minor2, patch: patch2 };
|
|
187
|
+
}
|
|
188
|
+
const { major, minor, patch } = parseSemver(range);
|
|
189
|
+
return { operator: "exact", major, minor, patch };
|
|
190
|
+
}
|
|
191
|
+
function satisfies(version, range) {
|
|
192
|
+
const v = parseSemver(version);
|
|
193
|
+
switch (range.operator) {
|
|
194
|
+
case "any":
|
|
195
|
+
return true;
|
|
196
|
+
case "exact":
|
|
197
|
+
return v.major === range.major && v.minor === range.minor && v.patch === range.patch;
|
|
198
|
+
case "caret":
|
|
199
|
+
if (v.major !== range.major) return false;
|
|
200
|
+
if (v.minor > range.minor) return true;
|
|
201
|
+
if (v.minor === range.minor) return v.patch >= range.patch;
|
|
202
|
+
return false;
|
|
203
|
+
case "tilde":
|
|
204
|
+
if (v.major !== range.major) return false;
|
|
205
|
+
if (v.minor !== range.minor) return false;
|
|
206
|
+
return v.patch >= range.patch;
|
|
207
|
+
case "gte":
|
|
208
|
+
if (v.major > range.major) return true;
|
|
209
|
+
if (v.major < range.major) return false;
|
|
210
|
+
if (v.minor > range.minor) return true;
|
|
211
|
+
if (v.minor < range.minor) return false;
|
|
212
|
+
return v.patch >= range.patch;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function compareVersions(a, b) {
|
|
216
|
+
const va = parseSemver(a);
|
|
217
|
+
const vb = parseSemver(b);
|
|
218
|
+
if (va.major !== vb.major) return va.major - vb.major;
|
|
219
|
+
if (va.minor !== vb.minor) return va.minor - vb.minor;
|
|
220
|
+
return va.patch - vb.patch;
|
|
221
|
+
}
|
|
222
|
+
function parseDepString(dep) {
|
|
223
|
+
const atIndex = dep.lastIndexOf("@");
|
|
224
|
+
if (atIndex > 0) {
|
|
225
|
+
return {
|
|
226
|
+
name: dep.slice(0, atIndex),
|
|
227
|
+
range: dep.slice(atIndex + 1)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return { name: dep, range: "*" };
|
|
231
|
+
}
|
|
232
|
+
async function resolveVersion(packageName, versionRange) {
|
|
233
|
+
const metadata = await fetchPackageMetadata(packageName);
|
|
234
|
+
if (!versionRange || versionRange === "latest") {
|
|
235
|
+
return { version: metadata.latest_version, metadata };
|
|
236
|
+
}
|
|
237
|
+
const range = parseVersionRange(versionRange);
|
|
238
|
+
const matching = metadata.versions.filter((v) => satisfies(v, range)).sort(compareVersions);
|
|
239
|
+
if (matching.length === 0) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Version '${versionRange}' not found for '${packageName}'. Available: ${metadata.versions.join(", ")}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const version = matching[matching.length - 1];
|
|
245
|
+
return { version, metadata };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/lib/git.ts
|
|
249
|
+
import fs3 from "fs";
|
|
250
|
+
import path3 from "path";
|
|
251
|
+
import os2 from "os";
|
|
252
|
+
import { simpleGit } from "simple-git";
|
|
253
|
+
function repoCloneUrl(repoUrl) {
|
|
254
|
+
const token = getGitHubToken();
|
|
255
|
+
const match = repoUrl.match(/^github\.com\/(.+)$/);
|
|
256
|
+
if (!match) return `https://${repoUrl}.git`;
|
|
257
|
+
if (token) {
|
|
258
|
+
return `https://${token}@github.com/${match[1]}.git`;
|
|
259
|
+
}
|
|
260
|
+
return `https://github.com/${match[1]}.git`;
|
|
261
|
+
}
|
|
262
|
+
async function cloneAtTag(repoUrl, tag, targetDir) {
|
|
263
|
+
const cloneUrl = repoCloneUrl(repoUrl);
|
|
264
|
+
const git = simpleGit();
|
|
265
|
+
await git.clone(cloneUrl, targetDir, [
|
|
266
|
+
"--depth",
|
|
267
|
+
"1",
|
|
268
|
+
"--branch",
|
|
269
|
+
tag,
|
|
270
|
+
"--single-branch"
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
async function fetchFileAtTag(repoUrl, tag, filePath) {
|
|
274
|
+
const match = repoUrl.match(/^github\.com\/([^/]+)\/([^/]+)$/);
|
|
275
|
+
if (match) {
|
|
276
|
+
const token = getGitHubToken();
|
|
277
|
+
const rawUrl = `https://raw.githubusercontent.com/${match[1]}/${match[2]}/${tag}/${filePath}`;
|
|
278
|
+
const headers = { "User-Agent": "planmode-cli" };
|
|
279
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
280
|
+
const response = await fetch(rawUrl, { headers });
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
throw new Error(`Failed to fetch ${filePath} from ${repoUrl}@${tag}: ${response.status}`);
|
|
283
|
+
}
|
|
284
|
+
return response.text();
|
|
285
|
+
}
|
|
286
|
+
const tmpDir = fs3.mkdtempSync(path3.join(os2.tmpdir(), "planmode-"));
|
|
287
|
+
try {
|
|
288
|
+
await cloneAtTag(repoUrl, tag, tmpDir);
|
|
289
|
+
const fullPath = path3.join(tmpDir, filePath);
|
|
290
|
+
if (!fs3.existsSync(fullPath)) {
|
|
291
|
+
throw new Error(`File not found: ${filePath} in ${repoUrl}@${tag}`);
|
|
292
|
+
}
|
|
293
|
+
return fs3.readFileSync(fullPath, "utf-8");
|
|
294
|
+
} finally {
|
|
295
|
+
fs3.rmSync(tmpDir, { recursive: true, force: true });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function createTag(dir, tag) {
|
|
299
|
+
const git = simpleGit(dir);
|
|
300
|
+
await git.addTag(tag);
|
|
301
|
+
}
|
|
302
|
+
async function pushTag(dir, tag) {
|
|
303
|
+
const git = simpleGit(dir);
|
|
304
|
+
await git.push("origin", tag);
|
|
305
|
+
}
|
|
306
|
+
async function getRemoteUrl(dir) {
|
|
307
|
+
try {
|
|
308
|
+
const git = simpleGit(dir);
|
|
309
|
+
const remotes = await git.getRemotes(true);
|
|
310
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
311
|
+
return origin?.refs?.fetch ?? null;
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function getHeadSha(dir) {
|
|
317
|
+
const git = simpleGit(dir);
|
|
318
|
+
const log = await git.log({ n: 1 });
|
|
319
|
+
return log.latest?.hash ?? "";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/lib/lockfile.ts
|
|
323
|
+
import fs4 from "fs";
|
|
324
|
+
import path4 from "path";
|
|
325
|
+
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
326
|
+
var LOCKFILE_NAME = "planmode.lock";
|
|
327
|
+
function getLockfilePath(projectDir = process.cwd()) {
|
|
328
|
+
return path4.join(projectDir, LOCKFILE_NAME);
|
|
329
|
+
}
|
|
330
|
+
function readLockfile(projectDir = process.cwd()) {
|
|
331
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
332
|
+
try {
|
|
333
|
+
const raw = fs4.readFileSync(lockfilePath, "utf-8");
|
|
334
|
+
const data = parse2(raw);
|
|
335
|
+
return data ?? { lockfile_version: 1, packages: {} };
|
|
336
|
+
} catch {
|
|
337
|
+
return { lockfile_version: 1, packages: {} };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function writeLockfile(lockfile, projectDir = process.cwd()) {
|
|
341
|
+
const lockfilePath = getLockfilePath(projectDir);
|
|
342
|
+
fs4.writeFileSync(lockfilePath, stringify2(lockfile), "utf-8");
|
|
343
|
+
}
|
|
344
|
+
function addToLockfile(packageName, entry, projectDir = process.cwd()) {
|
|
345
|
+
const lockfile = readLockfile(projectDir);
|
|
346
|
+
lockfile.packages[packageName] = entry;
|
|
347
|
+
writeLockfile(lockfile, projectDir);
|
|
348
|
+
}
|
|
349
|
+
function removeFromLockfile(packageName, projectDir = process.cwd()) {
|
|
350
|
+
const lockfile = readLockfile(projectDir);
|
|
351
|
+
delete lockfile.packages[packageName];
|
|
352
|
+
writeLockfile(lockfile, projectDir);
|
|
353
|
+
}
|
|
354
|
+
function getLockedVersion(packageName, projectDir = process.cwd()) {
|
|
355
|
+
const lockfile = readLockfile(projectDir);
|
|
356
|
+
return lockfile.packages[packageName];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/lib/claude-md.ts
|
|
360
|
+
import fs5 from "fs";
|
|
361
|
+
import path5 from "path";
|
|
362
|
+
var CLAUDE_MD = "CLAUDE.md";
|
|
363
|
+
var PLANMODE_SECTION = "# Planmode";
|
|
364
|
+
function getClaudeMdPath(projectDir = process.cwd()) {
|
|
365
|
+
return path5.join(projectDir, CLAUDE_MD);
|
|
366
|
+
}
|
|
367
|
+
function addImport(planName, projectDir = process.cwd()) {
|
|
368
|
+
const claudeMdPath = getClaudeMdPath(projectDir);
|
|
369
|
+
const importLine = `- @plans/${planName}.md`;
|
|
370
|
+
if (!fs5.existsSync(claudeMdPath)) {
|
|
371
|
+
const content2 = `${PLANMODE_SECTION}
|
|
372
|
+
${importLine}
|
|
373
|
+
`;
|
|
374
|
+
fs5.writeFileSync(claudeMdPath, content2, "utf-8");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const content = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
378
|
+
if (content.includes(importLine)) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (content.includes(PLANMODE_SECTION)) {
|
|
382
|
+
const updated = content.replace(PLANMODE_SECTION, `${PLANMODE_SECTION}
|
|
383
|
+
${importLine}`);
|
|
384
|
+
fs5.writeFileSync(claudeMdPath, updated, "utf-8");
|
|
385
|
+
} else {
|
|
386
|
+
const separator = content.endsWith("\n") ? "\n" : "\n\n";
|
|
387
|
+
const updated = content + separator + `${PLANMODE_SECTION}
|
|
388
|
+
${importLine}
|
|
389
|
+
`;
|
|
390
|
+
fs5.writeFileSync(claudeMdPath, updated, "utf-8");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
function removeImport(planName, projectDir = process.cwd()) {
|
|
394
|
+
const claudeMdPath = getClaudeMdPath(projectDir);
|
|
395
|
+
if (!fs5.existsSync(claudeMdPath)) return;
|
|
396
|
+
const content = fs5.readFileSync(claudeMdPath, "utf-8");
|
|
397
|
+
const importLine = `- @plans/${planName}.md`;
|
|
398
|
+
const updated = content.split("\n").filter((line) => line.trim() !== importLine).join("\n");
|
|
399
|
+
fs5.writeFileSync(claudeMdPath, updated, "utf-8");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/lib/manifest.ts
|
|
403
|
+
import fs6 from "fs";
|
|
404
|
+
import path6 from "path";
|
|
405
|
+
import { parse as parse3 } from "yaml";
|
|
406
|
+
var NAME_REGEX = /^(@[a-z0-9-]+\/)?[a-z0-9][a-z0-9-]*$/;
|
|
407
|
+
var SEMVER_REGEX = /^\d+\.\d+\.\d+$/;
|
|
408
|
+
var VALID_TYPES = ["prompt", "rule", "plan"];
|
|
409
|
+
var VALID_VAR_TYPES = ["string", "number", "boolean", "enum", "resolved"];
|
|
410
|
+
var VALID_CATEGORIES = [
|
|
411
|
+
"frontend",
|
|
412
|
+
"backend",
|
|
413
|
+
"devops",
|
|
414
|
+
"database",
|
|
415
|
+
"testing",
|
|
416
|
+
"mobile",
|
|
417
|
+
"ai-ml",
|
|
418
|
+
"design",
|
|
419
|
+
"security",
|
|
420
|
+
"other"
|
|
421
|
+
];
|
|
422
|
+
function parseManifest(raw) {
|
|
423
|
+
const data = parse3(raw);
|
|
424
|
+
if (!data || typeof data !== "object") {
|
|
425
|
+
throw new Error("Invalid YAML: manifest must be an object");
|
|
426
|
+
}
|
|
427
|
+
return data;
|
|
428
|
+
}
|
|
429
|
+
function readManifest(dir) {
|
|
430
|
+
const manifestPath = path6.join(dir, "planmode.yaml");
|
|
431
|
+
if (!fs6.existsSync(manifestPath)) {
|
|
432
|
+
throw new Error(`No planmode.yaml found in ${dir}`);
|
|
433
|
+
}
|
|
434
|
+
const raw = fs6.readFileSync(manifestPath, "utf-8");
|
|
435
|
+
return parseManifest(raw);
|
|
436
|
+
}
|
|
437
|
+
function validateManifest(manifest, requirePublishFields = false) {
|
|
438
|
+
const errors = [];
|
|
439
|
+
if (!manifest.name) {
|
|
440
|
+
errors.push("Missing required field: name");
|
|
441
|
+
} else if (!NAME_REGEX.test(manifest.name)) {
|
|
442
|
+
errors.push(`Invalid name "${manifest.name}": must match ${NAME_REGEX}`);
|
|
443
|
+
} else if (manifest.name.length > 100) {
|
|
444
|
+
errors.push("Name must be 100 characters or fewer");
|
|
445
|
+
}
|
|
446
|
+
if (!manifest.version) {
|
|
447
|
+
errors.push("Missing required field: version");
|
|
448
|
+
} else if (!SEMVER_REGEX.test(manifest.version)) {
|
|
449
|
+
errors.push(`Invalid version "${manifest.version}": must be valid semver (X.Y.Z)`);
|
|
450
|
+
}
|
|
451
|
+
if (!manifest.type) {
|
|
452
|
+
errors.push("Missing required field: type");
|
|
453
|
+
} else if (!VALID_TYPES.includes(manifest.type)) {
|
|
454
|
+
errors.push(`Invalid type "${manifest.type}": must be one of ${VALID_TYPES.join(", ")}`);
|
|
455
|
+
}
|
|
456
|
+
if (requirePublishFields) {
|
|
457
|
+
if (!manifest.description) errors.push("Missing required field: description");
|
|
458
|
+
if (manifest.description && manifest.description.length > 200) {
|
|
459
|
+
errors.push("Description must be 200 characters or fewer");
|
|
460
|
+
}
|
|
461
|
+
if (!manifest.author) errors.push("Missing required field: author");
|
|
462
|
+
if (!manifest.license) errors.push("Missing required field: license");
|
|
463
|
+
}
|
|
464
|
+
if (manifest.tags) {
|
|
465
|
+
if (manifest.tags.length > 10) {
|
|
466
|
+
errors.push("Maximum 10 tags allowed");
|
|
467
|
+
}
|
|
468
|
+
for (const tag of manifest.tags) {
|
|
469
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(tag)) {
|
|
470
|
+
errors.push(`Invalid tag "${tag}": must be lowercase alphanumeric with hyphens`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (manifest.category && !VALID_CATEGORIES.includes(manifest.category)) {
|
|
475
|
+
errors.push(`Invalid category "${manifest.category}": must be one of ${VALID_CATEGORIES.join(", ")}`);
|
|
476
|
+
}
|
|
477
|
+
if (manifest.dependencies && manifest.type === "prompt") {
|
|
478
|
+
errors.push("Dependencies are not allowed for prompt type packages");
|
|
479
|
+
}
|
|
480
|
+
if (manifest.variables) {
|
|
481
|
+
for (const [varName, varDef] of Object.entries(manifest.variables)) {
|
|
482
|
+
if (!varDef.type || !VALID_VAR_TYPES.includes(varDef.type)) {
|
|
483
|
+
errors.push(`Variable "${varName}" has invalid type: must be one of ${VALID_VAR_TYPES.join(", ")}`);
|
|
484
|
+
}
|
|
485
|
+
if (varDef.type === "enum" && (!varDef.options || varDef.options.length === 0)) {
|
|
486
|
+
errors.push(`Variable "${varName}" of type enum must have options`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (manifest.content && manifest.content_file) {
|
|
491
|
+
errors.push("Cannot specify both content and content_file");
|
|
492
|
+
}
|
|
493
|
+
return errors;
|
|
494
|
+
}
|
|
495
|
+
function readPackageContent(dir, manifest) {
|
|
496
|
+
if (manifest.content) {
|
|
497
|
+
return manifest.content;
|
|
498
|
+
}
|
|
499
|
+
if (manifest.content_file) {
|
|
500
|
+
const contentPath = path6.join(dir, manifest.content_file);
|
|
501
|
+
if (!fs6.existsSync(contentPath)) {
|
|
502
|
+
throw new Error(`Content file not found: ${manifest.content_file}`);
|
|
503
|
+
}
|
|
504
|
+
return fs6.readFileSync(contentPath, "utf-8");
|
|
505
|
+
}
|
|
506
|
+
throw new Error("Package must specify either content or content_file");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/lib/template.ts
|
|
510
|
+
import Handlebars from "handlebars";
|
|
511
|
+
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
512
|
+
function renderTemplate(content, variables) {
|
|
513
|
+
const template = Handlebars.compile(content);
|
|
514
|
+
return template(variables);
|
|
515
|
+
}
|
|
516
|
+
function collectVariableValues(variableDefs, provided) {
|
|
517
|
+
const values = {};
|
|
518
|
+
for (const [name, def] of Object.entries(variableDefs)) {
|
|
519
|
+
const rawValue = provided[name];
|
|
520
|
+
if (rawValue !== void 0) {
|
|
521
|
+
values[name] = coerceValue(rawValue, def);
|
|
522
|
+
} else if (def.default !== void 0) {
|
|
523
|
+
values[name] = def.default;
|
|
524
|
+
} else if (def.required) {
|
|
525
|
+
throw new Error(`Missing required variable: ${name} \u2014 ${def.description}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return values;
|
|
529
|
+
}
|
|
530
|
+
function coerceValue(raw, def) {
|
|
531
|
+
switch (def.type) {
|
|
532
|
+
case "number":
|
|
533
|
+
return Number(raw);
|
|
534
|
+
case "boolean":
|
|
535
|
+
return raw === "true" || raw === "1" || raw === "yes";
|
|
536
|
+
case "enum":
|
|
537
|
+
if (def.options && !def.options.includes(raw)) {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Invalid value "${raw}" for enum variable. Options: ${def.options.join(", ")}`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return raw;
|
|
543
|
+
default:
|
|
544
|
+
return raw;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function resolveVariable(def, currentValues) {
|
|
548
|
+
if (def.type !== "resolved" || !def.source) {
|
|
549
|
+
throw new Error("resolveVariable called on non-resolved variable");
|
|
550
|
+
}
|
|
551
|
+
const sourceUrl = renderTemplate(def.source, currentValues);
|
|
552
|
+
const response = await fetch(sourceUrl);
|
|
553
|
+
if (!response.ok) {
|
|
554
|
+
throw new Error(`Failed to resolve variable from ${sourceUrl}: ${response.status}`);
|
|
555
|
+
}
|
|
556
|
+
const data = await response.json();
|
|
557
|
+
if (def.extract) {
|
|
558
|
+
return extractPath(data, def.extract);
|
|
559
|
+
}
|
|
560
|
+
return String(data);
|
|
561
|
+
}
|
|
562
|
+
function extractPath(obj, pathStr) {
|
|
563
|
+
const parts = pathStr.match(/[^.[\]]+/g);
|
|
564
|
+
if (!parts) return String(obj);
|
|
565
|
+
let current = obj;
|
|
566
|
+
for (const part of parts) {
|
|
567
|
+
if (current === null || current === void 0) return "";
|
|
568
|
+
if (typeof current === "object") {
|
|
569
|
+
current = current[part];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return String(current ?? "");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/lib/logger.ts
|
|
576
|
+
var RESET = "\x1B[0m";
|
|
577
|
+
var RED = "\x1B[31m";
|
|
578
|
+
var GREEN = "\x1B[32m";
|
|
579
|
+
var YELLOW = "\x1B[33m";
|
|
580
|
+
var CYAN = "\x1B[36m";
|
|
581
|
+
var DIM = "\x1B[2m";
|
|
582
|
+
var BOLD = "\x1B[1m";
|
|
583
|
+
var logger = {
|
|
584
|
+
info(msg) {
|
|
585
|
+
console.log(`${CYAN}info${RESET} ${msg}`);
|
|
586
|
+
},
|
|
587
|
+
success(msg) {
|
|
588
|
+
console.log(`${GREEN}\u2713${RESET} ${msg}`);
|
|
589
|
+
},
|
|
590
|
+
warn(msg) {
|
|
591
|
+
console.log(`${YELLOW}warn${RESET} ${msg}`);
|
|
592
|
+
},
|
|
593
|
+
error(msg) {
|
|
594
|
+
console.error(`${RED}error${RESET} ${msg}`);
|
|
595
|
+
},
|
|
596
|
+
dim(msg) {
|
|
597
|
+
console.log(`${DIM}${msg}${RESET}`);
|
|
598
|
+
},
|
|
599
|
+
bold(msg) {
|
|
600
|
+
console.log(`${BOLD}${msg}${RESET}`);
|
|
601
|
+
},
|
|
602
|
+
table(headers, rows) {
|
|
603
|
+
const colWidths = headers.map(
|
|
604
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
605
|
+
);
|
|
606
|
+
const header = headers.map((h, i) => h.toUpperCase().padEnd(colWidths[i])).join(" ");
|
|
607
|
+
console.log(` ${DIM}${header}${RESET}`);
|
|
608
|
+
for (const row of rows) {
|
|
609
|
+
const line = row.map((cell, i) => cell.padEnd(colWidths[i])).join(" ");
|
|
610
|
+
console.log(` ${line}`);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
blank() {
|
|
614
|
+
console.log();
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/lib/installer.ts
|
|
619
|
+
function getInstallDir(type) {
|
|
620
|
+
switch (type) {
|
|
621
|
+
case "plan":
|
|
622
|
+
return "plans";
|
|
623
|
+
case "rule":
|
|
624
|
+
return path7.join(".claude", "rules");
|
|
625
|
+
case "prompt":
|
|
626
|
+
return "prompts";
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function getInstallPath(name, type) {
|
|
630
|
+
return path7.join(getInstallDir(type), `${name}.md`);
|
|
631
|
+
}
|
|
632
|
+
function contentHash(content) {
|
|
633
|
+
return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
|
|
634
|
+
}
|
|
635
|
+
async function installPackage(packageName, options = {}) {
|
|
636
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
637
|
+
const locked = getLockedVersion(packageName, projectDir);
|
|
638
|
+
if (locked && !options.version) {
|
|
639
|
+
logger.dim(`${packageName}@${locked.version} already installed`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
logger.info(`Resolving ${packageName}...`);
|
|
643
|
+
const { version, metadata } = await resolveVersion(packageName, options.version);
|
|
644
|
+
const versionMeta = await fetchVersionMetadata(packageName, version);
|
|
645
|
+
logger.info(`Fetching ${packageName}@${version}...`);
|
|
646
|
+
const manifestRaw = await fetchFileAtTag(
|
|
647
|
+
versionMeta.source.repository,
|
|
648
|
+
versionMeta.source.tag,
|
|
649
|
+
"planmode.yaml"
|
|
650
|
+
);
|
|
651
|
+
const manifest = parseManifest(manifestRaw);
|
|
652
|
+
let content;
|
|
653
|
+
if (manifest.content) {
|
|
654
|
+
content = manifest.content;
|
|
655
|
+
} else if (manifest.content_file) {
|
|
656
|
+
content = await fetchFileAtTag(
|
|
657
|
+
versionMeta.source.repository,
|
|
658
|
+
versionMeta.source.tag,
|
|
659
|
+
manifest.content_file
|
|
660
|
+
);
|
|
661
|
+
} else {
|
|
662
|
+
throw new Error("Package has no content or content_file");
|
|
663
|
+
}
|
|
664
|
+
if (manifest.variables && Object.keys(manifest.variables).length > 0) {
|
|
665
|
+
const provided = options.variables ?? {};
|
|
666
|
+
if (options.noInput) {
|
|
667
|
+
const values = collectVariableValues(manifest.variables, provided);
|
|
668
|
+
content = renderTemplate(content, values);
|
|
669
|
+
} else {
|
|
670
|
+
const values = collectVariableValues(manifest.variables, provided);
|
|
671
|
+
content = renderTemplate(content, values);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const type = options.forceRule ? "rule" : manifest.type;
|
|
675
|
+
const installPath = getInstallPath(packageName, type);
|
|
676
|
+
const fullPath = path7.join(projectDir, installPath);
|
|
677
|
+
if (fs7.existsSync(fullPath)) {
|
|
678
|
+
const existingContent = fs7.readFileSync(fullPath, "utf-8");
|
|
679
|
+
const existingHash = contentHash(existingContent);
|
|
680
|
+
const newHash = contentHash(content);
|
|
681
|
+
if (existingHash === newHash) {
|
|
682
|
+
logger.dim(`${packageName} already installed (identical content)`);
|
|
683
|
+
} else {
|
|
684
|
+
logger.warn(`Overwriting ${installPath} with new content`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
fs7.mkdirSync(path7.dirname(fullPath), { recursive: true });
|
|
688
|
+
fs7.writeFileSync(fullPath, content, "utf-8");
|
|
689
|
+
logger.success(`Installed ${packageName}@${version} \u2192 ${installPath}`);
|
|
690
|
+
if (type === "plan") {
|
|
691
|
+
addImport(packageName, projectDir);
|
|
692
|
+
logger.dim(`Added @import to CLAUDE.md`);
|
|
693
|
+
}
|
|
694
|
+
const hash = contentHash(content);
|
|
695
|
+
const entry = {
|
|
696
|
+
version,
|
|
697
|
+
type,
|
|
698
|
+
source: versionMeta.source.repository,
|
|
699
|
+
tag: versionMeta.source.tag,
|
|
700
|
+
sha: versionMeta.source.sha,
|
|
701
|
+
content_hash: hash,
|
|
702
|
+
installed_to: installPath
|
|
703
|
+
};
|
|
704
|
+
addToLockfile(packageName, entry, projectDir);
|
|
705
|
+
if (manifest.dependencies) {
|
|
706
|
+
const deps = [
|
|
707
|
+
...(manifest.dependencies.rules ?? []).map((d) => ({ dep: d, type: "rule" })),
|
|
708
|
+
...(manifest.dependencies.plans ?? []).map((d) => ({ dep: d, type: "plan" }))
|
|
709
|
+
];
|
|
710
|
+
for (const { dep } of deps) {
|
|
711
|
+
const { name, range } = parseDepString(dep);
|
|
712
|
+
await installPackage(name, {
|
|
713
|
+
version: range === "*" ? void 0 : range,
|
|
714
|
+
projectDir,
|
|
715
|
+
noInput: options.noInput
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function uninstallPackage(packageName, projectDir = process.cwd()) {
|
|
721
|
+
const locked = getLockedVersion(packageName, projectDir);
|
|
722
|
+
if (!locked) {
|
|
723
|
+
throw new Error(`Package '${packageName}' is not installed.`);
|
|
724
|
+
}
|
|
725
|
+
const fullPath = path7.join(projectDir, locked.installed_to);
|
|
726
|
+
if (fs7.existsSync(fullPath)) {
|
|
727
|
+
fs7.unlinkSync(fullPath);
|
|
728
|
+
logger.success(`Removed ${locked.installed_to}`);
|
|
729
|
+
}
|
|
730
|
+
if (locked.type === "plan") {
|
|
731
|
+
removeImport(packageName, projectDir);
|
|
732
|
+
logger.dim(`Removed @import from CLAUDE.md`);
|
|
733
|
+
}
|
|
734
|
+
removeFromLockfile(packageName, projectDir);
|
|
735
|
+
logger.success(`Uninstalled ${packageName}`);
|
|
736
|
+
}
|
|
737
|
+
async function updatePackage(packageName, projectDir = process.cwd()) {
|
|
738
|
+
const locked = getLockedVersion(packageName, projectDir);
|
|
739
|
+
if (!locked) {
|
|
740
|
+
throw new Error(`Package '${packageName}' is not installed.`);
|
|
741
|
+
}
|
|
742
|
+
const { version, metadata } = await resolveVersion(packageName);
|
|
743
|
+
if (version === locked.version) {
|
|
744
|
+
logger.dim(`${packageName}@${version} is already up to date`);
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
logger.info(`Updating ${packageName}: ${locked.version} \u2192 ${version}`);
|
|
748
|
+
await uninstallPackage(packageName, projectDir);
|
|
749
|
+
await installPackage(packageName, { version, projectDir });
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/commands/install.ts
|
|
754
|
+
var installCommand = new Command("install").description("Install a package into the current project").argument("<package>", "Package name (e.g., nextjs-tailwind-starter)").option("-v, --version <version>", "Install specific version").option("--rule", "Force install as a rule to .claude/rules/").option("--no-input", "Fail if any required variable is missing").action(
|
|
755
|
+
async (packageName, options) => {
|
|
756
|
+
try {
|
|
757
|
+
logger.blank();
|
|
758
|
+
await installPackage(packageName, {
|
|
759
|
+
version: options.version,
|
|
760
|
+
forceRule: options.rule,
|
|
761
|
+
noInput: options.input === false
|
|
762
|
+
});
|
|
763
|
+
logger.blank();
|
|
764
|
+
} catch (err) {
|
|
765
|
+
logger.error(err.message);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
// src/commands/uninstall.ts
|
|
772
|
+
import { Command as Command2 } from "commander";
|
|
773
|
+
var uninstallCommand = new Command2("uninstall").description("Remove an installed package").argument("<package>", "Package name").action(async (packageName) => {
|
|
774
|
+
try {
|
|
775
|
+
logger.blank();
|
|
776
|
+
await uninstallPackage(packageName);
|
|
777
|
+
logger.blank();
|
|
778
|
+
} catch (err) {
|
|
779
|
+
logger.error(err.message);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// src/commands/search.ts
|
|
785
|
+
import { Command as Command3 } from "commander";
|
|
786
|
+
var searchCommand = new Command3("search").description("Search the registry for packages").argument("<query>", "Search query").option("--type <type>", "Filter by type (prompt, rule, plan)").option("--category <category>", "Filter by category").option("--json", "Output as JSON").action(async (query, options) => {
|
|
787
|
+
try {
|
|
788
|
+
const results = await searchPackages(query, {
|
|
789
|
+
type: options.type,
|
|
790
|
+
category: options.category
|
|
791
|
+
});
|
|
792
|
+
if (results.length === 0) {
|
|
793
|
+
logger.info("No packages found matching your query.");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (options.json) {
|
|
797
|
+
console.log(JSON.stringify(results, null, 2));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
logger.blank();
|
|
801
|
+
logger.table(
|
|
802
|
+
["name", "type", "version", "description"],
|
|
803
|
+
results.map((pkg) => [
|
|
804
|
+
pkg.name,
|
|
805
|
+
pkg.type,
|
|
806
|
+
pkg.version,
|
|
807
|
+
pkg.description.length > 50 ? pkg.description.slice(0, 50) + "..." : pkg.description
|
|
808
|
+
])
|
|
809
|
+
);
|
|
810
|
+
logger.blank();
|
|
811
|
+
} catch (err) {
|
|
812
|
+
logger.error(err.message);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// src/commands/run.ts
|
|
818
|
+
import { Command as Command4 } from "commander";
|
|
819
|
+
import fs8 from "fs";
|
|
820
|
+
import path8 from "path";
|
|
821
|
+
var runCommand = new Command4("run").description("Run a templated prompt and output to stdout").argument("<prompt>", "Prompt package name").option("--no-input", "Fail if any required variable is missing").option("--json", "Output as JSON").allowUnknownOption(true).action(async (promptName, options, cmd) => {
|
|
822
|
+
try {
|
|
823
|
+
const vars = {};
|
|
824
|
+
const rawArgs = cmd.args.slice(0);
|
|
825
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
826
|
+
const arg = rawArgs[i];
|
|
827
|
+
if (arg.startsWith("--") && arg !== "--no-input" && arg !== "--json") {
|
|
828
|
+
const key = arg.slice(2);
|
|
829
|
+
const value = rawArgs[i + 1];
|
|
830
|
+
if (value && !value.startsWith("--")) {
|
|
831
|
+
vars[key] = value;
|
|
832
|
+
i++;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const localPath = path8.join(process.cwd(), "prompts", `${promptName}.md`);
|
|
837
|
+
const localManifestPath = path8.join(process.cwd(), "prompts", promptName, "planmode.yaml");
|
|
838
|
+
let content;
|
|
839
|
+
let manifest;
|
|
840
|
+
if (fs8.existsSync(localManifestPath)) {
|
|
841
|
+
const raw = fs8.readFileSync(localManifestPath, "utf-8");
|
|
842
|
+
manifest = parseManifest(raw);
|
|
843
|
+
const dir = path8.join(process.cwd(), "prompts", promptName);
|
|
844
|
+
content = readPackageContent(dir, manifest);
|
|
845
|
+
} else if (fs8.existsSync(localPath)) {
|
|
846
|
+
content = fs8.readFileSync(localPath, "utf-8");
|
|
847
|
+
} else {
|
|
848
|
+
logger.error(`Prompt '${promptName}' not found locally. Install it first: planmode install ${promptName}`);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
if (manifest?.variables && Object.keys(manifest.variables).length > 0) {
|
|
852
|
+
const values = {};
|
|
853
|
+
for (const [name, def] of Object.entries(manifest.variables)) {
|
|
854
|
+
if (def.type === "resolved") continue;
|
|
855
|
+
if (vars[name] !== void 0) {
|
|
856
|
+
values[name] = vars[name];
|
|
857
|
+
} else if (def.default !== void 0) {
|
|
858
|
+
values[name] = def.default;
|
|
859
|
+
} else if (def.required && options.input === false) {
|
|
860
|
+
logger.error(`Missing required variable: --${name}`);
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
for (const [name, def] of Object.entries(manifest.variables)) {
|
|
865
|
+
if (def.type !== "resolved") continue;
|
|
866
|
+
values[name] = await resolveVariable(def, values);
|
|
867
|
+
}
|
|
868
|
+
content = renderTemplate(content, values);
|
|
869
|
+
if (options.json) {
|
|
870
|
+
console.log(JSON.stringify({ rendered: content, variables: values }, null, 2));
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
process.stdout.write(content);
|
|
875
|
+
} catch (err) {
|
|
876
|
+
logger.error(err.message);
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// src/commands/publish.ts
|
|
882
|
+
import { Command as Command5 } from "commander";
|
|
883
|
+
var publishCommand = new Command5("publish").description("Publish the current directory as a package to the registry").action(async () => {
|
|
884
|
+
try {
|
|
885
|
+
const cwd = process.cwd();
|
|
886
|
+
const token = getGitHubToken();
|
|
887
|
+
if (!token) {
|
|
888
|
+
logger.error("Not authenticated. Run `planmode login` first.");
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
logger.info("Reading planmode.yaml...");
|
|
892
|
+
const manifest = readManifest(cwd);
|
|
893
|
+
const errors = validateManifest(manifest, true);
|
|
894
|
+
if (errors.length > 0) {
|
|
895
|
+
logger.error("Invalid manifest:");
|
|
896
|
+
for (const err of errors) {
|
|
897
|
+
console.log(` - ${err}`);
|
|
898
|
+
}
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
const remoteUrl = await getRemoteUrl(cwd);
|
|
902
|
+
if (!remoteUrl) {
|
|
903
|
+
logger.error("No git remote found. Push your code to GitHub first.");
|
|
904
|
+
process.exit(1);
|
|
905
|
+
}
|
|
906
|
+
const sha = await getHeadSha(cwd);
|
|
907
|
+
const tag = `v${manifest.version}`;
|
|
908
|
+
logger.info(`Creating tag ${tag}...`);
|
|
909
|
+
try {
|
|
910
|
+
await createTag(cwd, tag);
|
|
911
|
+
} catch {
|
|
912
|
+
logger.dim(`Tag ${tag} already exists, using existing`);
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
await pushTag(cwd, tag);
|
|
916
|
+
logger.success(`Pushed tag ${tag}`);
|
|
917
|
+
} catch {
|
|
918
|
+
logger.dim(`Tag ${tag} already pushed`);
|
|
919
|
+
}
|
|
920
|
+
logger.info("Submitting to registry...");
|
|
921
|
+
const headers = {
|
|
922
|
+
Authorization: `Bearer ${token}`,
|
|
923
|
+
Accept: "application/vnd.github.v3+json",
|
|
924
|
+
"User-Agent": "planmode-cli",
|
|
925
|
+
"Content-Type": "application/json"
|
|
926
|
+
};
|
|
927
|
+
await fetch("https://api.github.com/repos/planmode/registry/forks", {
|
|
928
|
+
method: "POST",
|
|
929
|
+
headers
|
|
930
|
+
});
|
|
931
|
+
const userRes = await fetch("https://api.github.com/user", { headers });
|
|
932
|
+
const user = await userRes.json();
|
|
933
|
+
const metadataContent = JSON.stringify(
|
|
934
|
+
{
|
|
935
|
+
name: manifest.name,
|
|
936
|
+
description: manifest.description,
|
|
937
|
+
author: manifest.author,
|
|
938
|
+
license: manifest.license,
|
|
939
|
+
repository: remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, ""),
|
|
940
|
+
category: manifest.category ?? "other",
|
|
941
|
+
tags: manifest.tags ?? [],
|
|
942
|
+
type: manifest.type,
|
|
943
|
+
models: manifest.models ?? [],
|
|
944
|
+
latest_version: manifest.version,
|
|
945
|
+
versions: [manifest.version],
|
|
946
|
+
downloads: 0,
|
|
947
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
948
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
949
|
+
dependencies: manifest.dependencies,
|
|
950
|
+
variables: manifest.variables
|
|
951
|
+
},
|
|
952
|
+
null,
|
|
953
|
+
2
|
|
954
|
+
);
|
|
955
|
+
const versionContent = JSON.stringify(
|
|
956
|
+
{
|
|
957
|
+
version: manifest.version,
|
|
958
|
+
published_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
959
|
+
source: {
|
|
960
|
+
repository: remoteUrl.replace(/^https?:\/\//, "").replace(/\.git$/, ""),
|
|
961
|
+
tag,
|
|
962
|
+
sha
|
|
963
|
+
},
|
|
964
|
+
files: ["planmode.yaml", manifest.content_file ?? "inline"],
|
|
965
|
+
content_hash: `sha256:${sha.slice(0, 16)}`
|
|
966
|
+
},
|
|
967
|
+
null,
|
|
968
|
+
2
|
|
969
|
+
);
|
|
970
|
+
const branchName = `add-${manifest.name}-${manifest.version}`;
|
|
971
|
+
const refRes = await fetch(
|
|
972
|
+
`https://api.github.com/repos/${user.login}/registry/git/ref/heads/main`,
|
|
973
|
+
{ headers }
|
|
974
|
+
);
|
|
975
|
+
if (!refRes.ok) {
|
|
976
|
+
logger.error("Failed to access registry fork. Make sure the fork exists.");
|
|
977
|
+
process.exit(1);
|
|
978
|
+
}
|
|
979
|
+
const refData = await refRes.json();
|
|
980
|
+
const baseSha = refData.object.sha;
|
|
981
|
+
await fetch(`https://api.github.com/repos/${user.login}/registry/git/refs`, {
|
|
982
|
+
method: "POST",
|
|
983
|
+
headers,
|
|
984
|
+
body: JSON.stringify({
|
|
985
|
+
ref: `refs/heads/${branchName}`,
|
|
986
|
+
sha: baseSha
|
|
987
|
+
})
|
|
988
|
+
});
|
|
989
|
+
await fetch(
|
|
990
|
+
`https://api.github.com/repos/${user.login}/registry/contents/packages/${manifest.name}/metadata.json`,
|
|
991
|
+
{
|
|
992
|
+
method: "PUT",
|
|
993
|
+
headers,
|
|
994
|
+
body: JSON.stringify({
|
|
995
|
+
message: `Add ${manifest.name}@${manifest.version}`,
|
|
996
|
+
content: Buffer.from(metadataContent).toString("base64"),
|
|
997
|
+
branch: branchName
|
|
998
|
+
})
|
|
999
|
+
}
|
|
1000
|
+
);
|
|
1001
|
+
await fetch(
|
|
1002
|
+
`https://api.github.com/repos/${user.login}/registry/contents/packages/${manifest.name}/versions/${manifest.version}.json`,
|
|
1003
|
+
{
|
|
1004
|
+
method: "PUT",
|
|
1005
|
+
headers,
|
|
1006
|
+
body: JSON.stringify({
|
|
1007
|
+
message: `Add ${manifest.name}@${manifest.version} version metadata`,
|
|
1008
|
+
content: Buffer.from(versionContent).toString("base64"),
|
|
1009
|
+
branch: branchName
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1012
|
+
);
|
|
1013
|
+
const prRes = await fetch("https://api.github.com/repos/planmode/registry/pulls", {
|
|
1014
|
+
method: "POST",
|
|
1015
|
+
headers,
|
|
1016
|
+
body: JSON.stringify({
|
|
1017
|
+
title: `Add ${manifest.name}@${manifest.version}`,
|
|
1018
|
+
head: `${user.login}:${branchName}`,
|
|
1019
|
+
base: "main",
|
|
1020
|
+
body: `## New package: ${manifest.name}
|
|
1021
|
+
|
|
1022
|
+
- **Type:** ${manifest.type}
|
|
1023
|
+
- **Version:** ${manifest.version}
|
|
1024
|
+
- **Description:** ${manifest.description}
|
|
1025
|
+
- **Author:** ${manifest.author}
|
|
1026
|
+
|
|
1027
|
+
Submitted via \`planmode publish\`.`
|
|
1028
|
+
})
|
|
1029
|
+
});
|
|
1030
|
+
if (prRes.ok) {
|
|
1031
|
+
const pr = await prRes.json();
|
|
1032
|
+
logger.blank();
|
|
1033
|
+
logger.success(`Published ${manifest.name}@${manifest.version}`);
|
|
1034
|
+
logger.info(`PR: ${pr.html_url}`);
|
|
1035
|
+
} else {
|
|
1036
|
+
const err = await prRes.text();
|
|
1037
|
+
logger.error(`Failed to create PR: ${err}`);
|
|
1038
|
+
process.exit(1);
|
|
1039
|
+
}
|
|
1040
|
+
logger.blank();
|
|
1041
|
+
} catch (err) {
|
|
1042
|
+
logger.error(err.message);
|
|
1043
|
+
process.exit(1);
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// src/commands/update.ts
|
|
1048
|
+
import { Command as Command6 } from "commander";
|
|
1049
|
+
var updateCommand = new Command6("update").description("Update installed packages to latest compatible versions").argument("[package]", "Package name (omit to update all)").action(async (packageName) => {
|
|
1050
|
+
try {
|
|
1051
|
+
logger.blank();
|
|
1052
|
+
if (packageName) {
|
|
1053
|
+
const updated = await updatePackage(packageName);
|
|
1054
|
+
if (!updated) {
|
|
1055
|
+
logger.info("Already up to date.");
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
const lockfile = readLockfile();
|
|
1059
|
+
const names = Object.keys(lockfile.packages);
|
|
1060
|
+
if (names.length === 0) {
|
|
1061
|
+
logger.info("No packages installed.");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
let updatedCount = 0;
|
|
1065
|
+
for (const name of names) {
|
|
1066
|
+
try {
|
|
1067
|
+
const updated = await updatePackage(name);
|
|
1068
|
+
if (updated) updatedCount++;
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
logger.warn(`Failed to update ${name}: ${err.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (updatedCount === 0) {
|
|
1074
|
+
logger.info("All packages are up to date.");
|
|
1075
|
+
} else {
|
|
1076
|
+
logger.success(`Updated ${updatedCount} package${updatedCount > 1 ? "s" : ""}.`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
logger.blank();
|
|
1080
|
+
} catch (err) {
|
|
1081
|
+
logger.error(err.message);
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// src/commands/list.ts
|
|
1087
|
+
import { Command as Command7 } from "commander";
|
|
1088
|
+
var listCommand = new Command7("list").description("List all installed packages").action(() => {
|
|
1089
|
+
const lockfile = readLockfile();
|
|
1090
|
+
const entries = Object.entries(lockfile.packages);
|
|
1091
|
+
if (entries.length === 0) {
|
|
1092
|
+
logger.info("No packages installed. Run `planmode install <package>` to get started.");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
logger.blank();
|
|
1096
|
+
logger.table(
|
|
1097
|
+
["name", "type", "version", "location"],
|
|
1098
|
+
entries.map(([name, entry]) => [
|
|
1099
|
+
name,
|
|
1100
|
+
entry.type,
|
|
1101
|
+
entry.version,
|
|
1102
|
+
entry.installed_to
|
|
1103
|
+
])
|
|
1104
|
+
);
|
|
1105
|
+
logger.blank();
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
// src/commands/info.ts
|
|
1109
|
+
import { Command as Command8 } from "commander";
|
|
1110
|
+
var infoCommand = new Command8("info").description("Show detailed info about a package").argument("<package>", "Package name").action(async (packageName) => {
|
|
1111
|
+
try {
|
|
1112
|
+
const meta = await fetchPackageMetadata(packageName);
|
|
1113
|
+
logger.blank();
|
|
1114
|
+
logger.bold(`${meta.name}@${meta.latest_version}`);
|
|
1115
|
+
logger.blank();
|
|
1116
|
+
console.log(` Description: ${meta.description}`);
|
|
1117
|
+
console.log(` Type: ${meta.type}`);
|
|
1118
|
+
console.log(` Author: ${meta.author}`);
|
|
1119
|
+
console.log(` License: ${meta.license}`);
|
|
1120
|
+
console.log(` Category: ${meta.category}`);
|
|
1121
|
+
console.log(` Downloads: ${meta.downloads.toLocaleString()}`);
|
|
1122
|
+
console.log(` Repository: ${meta.repository}`);
|
|
1123
|
+
if (meta.models && meta.models.length > 0) {
|
|
1124
|
+
console.log(` Models: ${meta.models.join(", ")}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (meta.tags && meta.tags.length > 0) {
|
|
1127
|
+
console.log(` Tags: ${meta.tags.join(", ")}`);
|
|
1128
|
+
}
|
|
1129
|
+
console.log(` Versions: ${meta.versions.join(", ")}`);
|
|
1130
|
+
if (meta.dependencies) {
|
|
1131
|
+
if (meta.dependencies.rules && meta.dependencies.rules.length > 0) {
|
|
1132
|
+
console.log(` Dep (rules): ${meta.dependencies.rules.join(", ")}`);
|
|
1133
|
+
}
|
|
1134
|
+
if (meta.dependencies.plans && meta.dependencies.plans.length > 0) {
|
|
1135
|
+
console.log(` Dep (plans): ${meta.dependencies.plans.join(", ")}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (meta.variables) {
|
|
1139
|
+
logger.blank();
|
|
1140
|
+
logger.bold(" Variables:");
|
|
1141
|
+
for (const [name, def] of Object.entries(meta.variables)) {
|
|
1142
|
+
const required = def.required ? " (required)" : "";
|
|
1143
|
+
const defaultVal = def.default !== void 0 ? ` [default: ${def.default}]` : "";
|
|
1144
|
+
console.log(` ${name}: ${def.type}${required}${defaultVal} \u2014 ${def.description}`);
|
|
1145
|
+
if (def.options) {
|
|
1146
|
+
console.log(` options: ${def.options.join(", ")}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
logger.blank();
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
logger.error(err.message);
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// src/commands/init.ts
|
|
1158
|
+
import { Command as Command9 } from "commander";
|
|
1159
|
+
import fs9 from "fs";
|
|
1160
|
+
import path9 from "path";
|
|
1161
|
+
import { stringify as stringify3 } from "yaml";
|
|
1162
|
+
async function prompt(question) {
|
|
1163
|
+
const { createInterface } = await import("readline");
|
|
1164
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1165
|
+
return new Promise((resolve) => {
|
|
1166
|
+
rl.question(question, (answer) => {
|
|
1167
|
+
rl.close();
|
|
1168
|
+
resolve(answer.trim());
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
var initCommand = new Command9("init").description("Initialize a new package in the current directory").action(async () => {
|
|
1173
|
+
try {
|
|
1174
|
+
logger.blank();
|
|
1175
|
+
logger.bold("Initialize a new Planmode package");
|
|
1176
|
+
logger.blank();
|
|
1177
|
+
const name = await prompt("Package name: ");
|
|
1178
|
+
if (!name) {
|
|
1179
|
+
logger.error("Package name is required.");
|
|
1180
|
+
process.exit(1);
|
|
1181
|
+
}
|
|
1182
|
+
const typeInput = await prompt("Type (plan/rule/prompt) [plan]: ");
|
|
1183
|
+
const type = typeInput || "plan";
|
|
1184
|
+
const description = await prompt("Description: ");
|
|
1185
|
+
const author = await prompt("Author (GitHub username): ");
|
|
1186
|
+
const license = await prompt("License [MIT]: ") || "MIT";
|
|
1187
|
+
const tagsInput = await prompt("Tags (comma-separated): ");
|
|
1188
|
+
const tags = tagsInput ? tagsInput.split(",").map((t) => t.trim().toLowerCase()) : [];
|
|
1189
|
+
const category = await prompt("Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: ") || "other";
|
|
1190
|
+
const manifest = {
|
|
1191
|
+
name,
|
|
1192
|
+
version: "1.0.0",
|
|
1193
|
+
type,
|
|
1194
|
+
description,
|
|
1195
|
+
author,
|
|
1196
|
+
license
|
|
1197
|
+
};
|
|
1198
|
+
if (tags.length > 0) manifest["tags"] = tags;
|
|
1199
|
+
manifest["category"] = category;
|
|
1200
|
+
const contentFile = `${type}.md`;
|
|
1201
|
+
manifest["content_file"] = contentFile;
|
|
1202
|
+
const yamlContent = stringify3(manifest);
|
|
1203
|
+
fs9.writeFileSync(path9.join(process.cwd(), "planmode.yaml"), yamlContent, "utf-8");
|
|
1204
|
+
logger.success("Created planmode.yaml");
|
|
1205
|
+
const stubs = {
|
|
1206
|
+
plan: `# ${name}
|
|
1207
|
+
|
|
1208
|
+
1. First step
|
|
1209
|
+
2. Second step
|
|
1210
|
+
3. Third step
|
|
1211
|
+
`,
|
|
1212
|
+
rule: `- Rule one
|
|
1213
|
+
- Rule two
|
|
1214
|
+
- Rule three
|
|
1215
|
+
`,
|
|
1216
|
+
prompt: `Write your prompt here.
|
|
1217
|
+
|
|
1218
|
+
Use {{variable_name}} for template variables.
|
|
1219
|
+
`
|
|
1220
|
+
};
|
|
1221
|
+
fs9.writeFileSync(
|
|
1222
|
+
path9.join(process.cwd(), contentFile),
|
|
1223
|
+
stubs[type] ?? stubs["plan"],
|
|
1224
|
+
"utf-8"
|
|
1225
|
+
);
|
|
1226
|
+
logger.success(`Created ${contentFile}`);
|
|
1227
|
+
logger.blank();
|
|
1228
|
+
logger.info(`Edit ${contentFile}, then run \`planmode publish\` when ready.`);
|
|
1229
|
+
logger.blank();
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
logger.error(err.message);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// src/commands/login.ts
|
|
1237
|
+
import { Command as Command10 } from "commander";
|
|
1238
|
+
import { execSync } from "child_process";
|
|
1239
|
+
var loginCommand = new Command10("login").description("Configure GitHub authentication").option("--token <token>", "GitHub personal access token").option("--gh", "Read token from GitHub CLI (gh auth token)").action(async (options) => {
|
|
1240
|
+
let token;
|
|
1241
|
+
if (options.token) {
|
|
1242
|
+
token = options.token;
|
|
1243
|
+
} else if (options.gh) {
|
|
1244
|
+
try {
|
|
1245
|
+
token = execSync("gh auth token", { encoding: "utf-8" }).trim();
|
|
1246
|
+
} catch {
|
|
1247
|
+
logger.error("Failed to read token from GitHub CLI. Make sure `gh` is installed and authenticated.");
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
} else {
|
|
1251
|
+
const { createInterface } = await import("readline");
|
|
1252
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1253
|
+
token = await new Promise((resolve) => {
|
|
1254
|
+
rl.question("GitHub personal access token: ", (answer) => {
|
|
1255
|
+
rl.close();
|
|
1256
|
+
resolve(answer.trim());
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
if (!token) {
|
|
1261
|
+
logger.error("No token provided.");
|
|
1262
|
+
process.exit(1);
|
|
1263
|
+
}
|
|
1264
|
+
logger.info("Validating token...");
|
|
1265
|
+
const response = await fetch("https://api.github.com/user", {
|
|
1266
|
+
headers: {
|
|
1267
|
+
Authorization: `Bearer ${token}`,
|
|
1268
|
+
"User-Agent": "planmode-cli"
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
if (!response.ok) {
|
|
1272
|
+
logger.error("Invalid token. GitHub API returned: " + response.status);
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
const user = await response.json();
|
|
1276
|
+
setGitHubToken(token);
|
|
1277
|
+
logger.success(`Authenticated as ${user.login}`);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// src/index.ts
|
|
1281
|
+
var program = new Command11();
|
|
1282
|
+
program.name("planmode").description("The open source package manager for AI plans, rules, and prompts.").version("0.1.0");
|
|
1283
|
+
program.addCommand(installCommand);
|
|
1284
|
+
program.addCommand(uninstallCommand);
|
|
1285
|
+
program.addCommand(searchCommand);
|
|
1286
|
+
program.addCommand(runCommand);
|
|
1287
|
+
program.addCommand(publishCommand);
|
|
1288
|
+
program.addCommand(updateCommand);
|
|
1289
|
+
program.addCommand(listCommand);
|
|
1290
|
+
program.addCommand(infoCommand);
|
|
1291
|
+
program.addCommand(initCommand);
|
|
1292
|
+
program.addCommand(loginCommand);
|
|
1293
|
+
program.parse();
|