planmode 0.2.2 → 0.4.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 +2080 -717
- package/dist/mcp.js +798 -262
- package/package.json +2 -1
- package/src/commands/context.ts +111 -0
- 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 +556 -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 +11 -2
- package/src/lib/context.ts +265 -0
- package/src/lib/installer.ts +57 -29
- package/src/lib/prompts.ts +159 -0
- package/src/lib/publisher.ts +176 -144
- package/src/mcp.ts +146 -0
- package/src/types/index.ts +28 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import { handleCancel, withSpinner } from "../lib/prompts.js";
|
|
6
|
+
import { searchPackages, fetchPackageMetadata, fetchIndex } from "../lib/registry.js";
|
|
7
|
+
import { installPackage } from "../lib/installer.js";
|
|
8
|
+
import { readLockfile } from "../lib/lockfile.js";
|
|
9
|
+
import { runDoctor } from "../lib/doctor.js";
|
|
10
|
+
import { addContextRepo, removeContextRepo, reindexContext, getContextSummary, formatSize, readContextIndex } from "../lib/context.js";
|
|
11
|
+
import type { PackageSummary } from "../types/index.js";
|
|
12
|
+
|
|
13
|
+
type Action =
|
|
14
|
+
| "search"
|
|
15
|
+
| "browse"
|
|
16
|
+
| "install"
|
|
17
|
+
| "create"
|
|
18
|
+
| "list"
|
|
19
|
+
| "context"
|
|
20
|
+
| "doctor"
|
|
21
|
+
| "exit";
|
|
22
|
+
|
|
23
|
+
type FirstRunAction =
|
|
24
|
+
| "browse"
|
|
25
|
+
| "search"
|
|
26
|
+
| "create";
|
|
27
|
+
|
|
28
|
+
const CATEGORIES = [
|
|
29
|
+
"frontend",
|
|
30
|
+
"backend",
|
|
31
|
+
"devops",
|
|
32
|
+
"database",
|
|
33
|
+
"testing",
|
|
34
|
+
"mobile",
|
|
35
|
+
"ai-ml",
|
|
36
|
+
"design",
|
|
37
|
+
"security",
|
|
38
|
+
"other",
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
function isFirstRun(): boolean {
|
|
42
|
+
const configPath = path.join(os.homedir(), ".planmode", "config");
|
|
43
|
+
const hasConfig = fs.existsSync(configPath);
|
|
44
|
+
const lockfile = readLockfile();
|
|
45
|
+
const hasPackages = Object.keys(lockfile.packages).length > 0;
|
|
46
|
+
return !hasConfig && !hasPackages;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runInteractiveMenu(): Promise<void> {
|
|
50
|
+
if (isFirstRun()) {
|
|
51
|
+
await firstRunFlow();
|
|
52
|
+
} else {
|
|
53
|
+
await mainMenu();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── First-run experience ──
|
|
58
|
+
|
|
59
|
+
async function firstRunFlow(): Promise<void> {
|
|
60
|
+
p.intro("planmode");
|
|
61
|
+
|
|
62
|
+
p.note(
|
|
63
|
+
[
|
|
64
|
+
"planmode installs AI plans, rules, and prompts into your project.",
|
|
65
|
+
"Plans work with Claude Code automatically via CLAUDE.md imports.",
|
|
66
|
+
"",
|
|
67
|
+
" Plans - step-by-step guides Claude follows to build things",
|
|
68
|
+
" Rules - always-on constraints that shape every AI interaction",
|
|
69
|
+
" Prompts - reusable templates you run once to get output",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
"Welcome",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const action = handleCancel(
|
|
75
|
+
await p.select<FirstRunAction>({
|
|
76
|
+
message: "Let's get you started. What would you like to do?",
|
|
77
|
+
options: [
|
|
78
|
+
{ value: "browse" as FirstRunAction, label: "Browse popular packages", hint: "see what's available" },
|
|
79
|
+
{ value: "search" as FirstRunAction, label: "Search for something specific" },
|
|
80
|
+
{ value: "create" as FirstRunAction, label: "Create your own package", hint: "start from scratch" },
|
|
81
|
+
],
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
switch (action) {
|
|
86
|
+
case "browse":
|
|
87
|
+
await featuredFlow();
|
|
88
|
+
break;
|
|
89
|
+
case "search":
|
|
90
|
+
await searchFlow();
|
|
91
|
+
break;
|
|
92
|
+
case "create": {
|
|
93
|
+
const { initInteractive } = await import("./init.js");
|
|
94
|
+
await initInteractive();
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// After first action, drop into the regular menu
|
|
100
|
+
const cont = handleCancel(
|
|
101
|
+
await p.confirm({
|
|
102
|
+
message: "Continue exploring?",
|
|
103
|
+
initialValue: true,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
if (cont) {
|
|
108
|
+
await mainMenu();
|
|
109
|
+
} else {
|
|
110
|
+
p.outro("Run `planmode` anytime to come back.");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function featuredFlow(): Promise<void> {
|
|
115
|
+
const index = await withSpinner(
|
|
116
|
+
"Loading packages...",
|
|
117
|
+
() => fetchIndex(),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Curate: show a mix of types, pick the most useful-looking ones
|
|
121
|
+
const plans = index.packages.filter((pkg) => pkg.type === "plan");
|
|
122
|
+
const rules = index.packages.filter((pkg) => pkg.type === "rule");
|
|
123
|
+
const prompts = index.packages.filter((pkg) => pkg.type === "prompt");
|
|
124
|
+
|
|
125
|
+
// Build a featured list: up to 5 plans, 3 rules, 3 prompts
|
|
126
|
+
const featured: PackageSummary[] = [
|
|
127
|
+
...plans.slice(0, 5),
|
|
128
|
+
...rules.slice(0, 3),
|
|
129
|
+
...prompts.slice(0, 3),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
if (featured.length === 0) {
|
|
133
|
+
p.log.warn("No packages in the registry yet.");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Group display
|
|
138
|
+
const planOptions = plans.slice(0, 5).map((pkg) => ({
|
|
139
|
+
value: pkg.name,
|
|
140
|
+
label: pkg.name,
|
|
141
|
+
hint: pkg.description.length > 55 ? pkg.description.slice(0, 55) + "..." : pkg.description,
|
|
142
|
+
}));
|
|
143
|
+
const ruleOptions = rules.slice(0, 3).map((pkg) => ({
|
|
144
|
+
value: pkg.name,
|
|
145
|
+
label: pkg.name,
|
|
146
|
+
hint: pkg.description.length > 55 ? pkg.description.slice(0, 55) + "..." : pkg.description,
|
|
147
|
+
}));
|
|
148
|
+
const promptOptions = prompts.slice(0, 3).map((pkg) => ({
|
|
149
|
+
value: pkg.name,
|
|
150
|
+
label: pkg.name,
|
|
151
|
+
hint: pkg.description.length > 55 ? pkg.description.slice(0, 55) + "..." : pkg.description,
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
// Show all in one select with separator-style labels
|
|
155
|
+
const allOptions: { value: string; label: string; hint?: string }[] = [];
|
|
156
|
+
|
|
157
|
+
if (planOptions.length > 0) {
|
|
158
|
+
allOptions.push(...planOptions);
|
|
159
|
+
}
|
|
160
|
+
if (ruleOptions.length > 0) {
|
|
161
|
+
allOptions.push(...ruleOptions);
|
|
162
|
+
}
|
|
163
|
+
if (promptOptions.length > 0) {
|
|
164
|
+
allOptions.push(...promptOptions);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const selected = handleCancel(
|
|
168
|
+
await p.select({
|
|
169
|
+
message: `${index.packages.length} packages available. Pick one to install:`,
|
|
170
|
+
options: [
|
|
171
|
+
...allOptions,
|
|
172
|
+
{ value: "__more__", label: "Browse by category..." },
|
|
173
|
+
],
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (selected === "__more__") {
|
|
178
|
+
await browseFlow();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await installOrDetailFlow(selected);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Main menu (returning users) ──
|
|
186
|
+
|
|
187
|
+
async function mainMenu(): Promise<void> {
|
|
188
|
+
p.intro("planmode");
|
|
189
|
+
|
|
190
|
+
while (true) {
|
|
191
|
+
const action = handleCancel(
|
|
192
|
+
await p.select<Action>({
|
|
193
|
+
message: "What would you like to do?",
|
|
194
|
+
options: [
|
|
195
|
+
{ value: "search" as Action, label: "Search packages", hint: "find packages by keyword" },
|
|
196
|
+
{ value: "browse" as Action, label: "Browse by category" },
|
|
197
|
+
{ value: "install" as Action, label: "Install a package", hint: "install by name" },
|
|
198
|
+
{ value: "create" as Action, label: "Create a new package" },
|
|
199
|
+
{ value: "list" as Action, label: "My installed packages" },
|
|
200
|
+
{ value: "context" as Action, label: "Manage context", hint: "document directories for AI" },
|
|
201
|
+
{ value: "doctor" as Action, label: "Health check" },
|
|
202
|
+
{ value: "exit" as Action, label: "Exit" },
|
|
203
|
+
],
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
switch (action) {
|
|
208
|
+
case "search":
|
|
209
|
+
await searchFlow();
|
|
210
|
+
break;
|
|
211
|
+
case "browse":
|
|
212
|
+
await browseFlow();
|
|
213
|
+
break;
|
|
214
|
+
case "install":
|
|
215
|
+
await installFlow();
|
|
216
|
+
break;
|
|
217
|
+
case "create": {
|
|
218
|
+
const { initInteractive } = await import("./init.js");
|
|
219
|
+
await initInteractive();
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "list":
|
|
223
|
+
listFlow();
|
|
224
|
+
break;
|
|
225
|
+
case "context":
|
|
226
|
+
await contextFlow();
|
|
227
|
+
break;
|
|
228
|
+
case "doctor":
|
|
229
|
+
doctorFlow();
|
|
230
|
+
break;
|
|
231
|
+
case "exit":
|
|
232
|
+
p.outro("Goodbye!");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Shared flows ──
|
|
239
|
+
|
|
240
|
+
async function searchFlow(): Promise<void> {
|
|
241
|
+
const query = handleCancel(
|
|
242
|
+
await p.text({
|
|
243
|
+
message: "Search for packages:",
|
|
244
|
+
placeholder: "e.g. nextjs, tailwind, auth",
|
|
245
|
+
validate(input) {
|
|
246
|
+
if (!input) return "Please enter a search query";
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const results = await withSpinner(
|
|
252
|
+
"Searching registry...",
|
|
253
|
+
() => searchPackages(query),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (results.length === 0) {
|
|
257
|
+
p.log.warn("No packages found matching your query.");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
p.log.info(`Found ${results.length} package(s)`);
|
|
262
|
+
|
|
263
|
+
await packageSelectionFlow(results.map((r) => ({
|
|
264
|
+
name: r.name,
|
|
265
|
+
type: r.type,
|
|
266
|
+
version: r.version,
|
|
267
|
+
description: r.description,
|
|
268
|
+
})));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function browseFlow(): Promise<void> {
|
|
272
|
+
const category = handleCancel(
|
|
273
|
+
await p.select({
|
|
274
|
+
message: "Select a category:",
|
|
275
|
+
options: CATEGORIES.map((cat) => ({
|
|
276
|
+
value: cat,
|
|
277
|
+
label: cat,
|
|
278
|
+
})),
|
|
279
|
+
}),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const results = await withSpinner(
|
|
283
|
+
`Loading ${category} packages...`,
|
|
284
|
+
() => searchPackages("", { category }),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (results.length === 0) {
|
|
288
|
+
p.log.warn(`No packages found in category "${category}".`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
p.log.info(`Found ${results.length} package(s) in "${category}"`);
|
|
293
|
+
|
|
294
|
+
await packageSelectionFlow(results.map((r) => ({
|
|
295
|
+
name: r.name,
|
|
296
|
+
type: r.type,
|
|
297
|
+
version: r.version,
|
|
298
|
+
description: r.description,
|
|
299
|
+
})));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface PackageOption {
|
|
303
|
+
name: string;
|
|
304
|
+
type: string;
|
|
305
|
+
version: string;
|
|
306
|
+
description: string;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function packageSelectionFlow(packages: PackageOption[]): Promise<void> {
|
|
310
|
+
const selected = handleCancel(
|
|
311
|
+
await p.select({
|
|
312
|
+
message: "Select a package:",
|
|
313
|
+
options: [
|
|
314
|
+
...packages.map((pkg) => ({
|
|
315
|
+
value: pkg.name,
|
|
316
|
+
label: `${pkg.name} (${pkg.type} v${pkg.version})`,
|
|
317
|
+
hint: pkg.description.length > 60
|
|
318
|
+
? pkg.description.slice(0, 60) + "..."
|
|
319
|
+
: pkg.description,
|
|
320
|
+
})),
|
|
321
|
+
{ value: "__back__", label: "Back" },
|
|
322
|
+
],
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
if (selected === "__back__") return;
|
|
327
|
+
|
|
328
|
+
await installOrDetailFlow(selected);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function installOrDetailFlow(packageName: string): Promise<void> {
|
|
332
|
+
const action = handleCancel(
|
|
333
|
+
await p.select({
|
|
334
|
+
message: `${packageName}:`,
|
|
335
|
+
options: [
|
|
336
|
+
{ value: "install", label: "Install" },
|
|
337
|
+
{ value: "details", label: "View details" },
|
|
338
|
+
{ value: "back", label: "Back" },
|
|
339
|
+
],
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (action === "install") {
|
|
344
|
+
try {
|
|
345
|
+
await installPackage(packageName, { interactive: true });
|
|
346
|
+
p.log.success(`Installed ${packageName}`);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
p.log.error((err as Error).message);
|
|
349
|
+
}
|
|
350
|
+
} else if (action === "details") {
|
|
351
|
+
try {
|
|
352
|
+
const meta = await withSpinner(
|
|
353
|
+
"Fetching package details...",
|
|
354
|
+
() => fetchPackageMetadata(packageName),
|
|
355
|
+
);
|
|
356
|
+
const lines = [
|
|
357
|
+
`Type: ${meta.type}`,
|
|
358
|
+
`Author: ${meta.author}`,
|
|
359
|
+
`License: ${meta.license}`,
|
|
360
|
+
`Category: ${meta.category}`,
|
|
361
|
+
`Downloads: ${meta.downloads.toLocaleString()}`,
|
|
362
|
+
`Versions: ${meta.versions.join(", ")}`,
|
|
363
|
+
`Repository: ${meta.repository}`,
|
|
364
|
+
];
|
|
365
|
+
if (meta.tags?.length) {
|
|
366
|
+
lines.push(`Tags: ${meta.tags.join(", ")}`);
|
|
367
|
+
}
|
|
368
|
+
if (meta.dependencies?.rules?.length) {
|
|
369
|
+
lines.push(`Dep (rules): ${meta.dependencies.rules.join(", ")}`);
|
|
370
|
+
}
|
|
371
|
+
if (meta.dependencies?.plans?.length) {
|
|
372
|
+
lines.push(`Dep (plans): ${meta.dependencies.plans.join(", ")}`);
|
|
373
|
+
}
|
|
374
|
+
p.note(lines.join("\n"), `${meta.name}@${meta.latest_version}`);
|
|
375
|
+
|
|
376
|
+
const nextAction = handleCancel(
|
|
377
|
+
await p.confirm({
|
|
378
|
+
message: "Install this package?",
|
|
379
|
+
initialValue: true,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (nextAction) {
|
|
384
|
+
try {
|
|
385
|
+
await installPackage(packageName, { interactive: true });
|
|
386
|
+
p.log.success(`Installed ${packageName}`);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
p.log.error((err as Error).message);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
} catch (err) {
|
|
392
|
+
p.log.error((err as Error).message);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function installFlow(): Promise<void> {
|
|
398
|
+
const packageName = handleCancel(
|
|
399
|
+
await p.text({
|
|
400
|
+
message: "Package name to install:",
|
|
401
|
+
placeholder: "e.g. nextjs-tailwind-starter",
|
|
402
|
+
validate(input) {
|
|
403
|
+
if (!input) return "Please enter a package name";
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
await installPackage(packageName, { interactive: true });
|
|
410
|
+
p.log.success(`Installed ${packageName}`);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
p.log.error((err as Error).message);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
type ContextAction = "add" | "remove" | "reindex" | "back";
|
|
417
|
+
|
|
418
|
+
async function contextFlow(): Promise<void> {
|
|
419
|
+
const summary = getContextSummary();
|
|
420
|
+
|
|
421
|
+
if (summary.totalRepos === 0) {
|
|
422
|
+
p.log.info("No context repos yet.");
|
|
423
|
+
} else {
|
|
424
|
+
const lines = summary.repos.map(
|
|
425
|
+
(r) => `${r.name} — ${r.fileCount} file(s), ${formatSize(r.totalSize)}`,
|
|
426
|
+
);
|
|
427
|
+
p.note(lines.join("\n"), `${summary.totalRepos} context repo(s)`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const action = handleCancel(
|
|
431
|
+
await p.select<ContextAction>({
|
|
432
|
+
message: "What would you like to do?",
|
|
433
|
+
options: [
|
|
434
|
+
{ value: "add" as ContextAction, label: "Add a directory" },
|
|
435
|
+
{ value: "remove" as ContextAction, label: "Remove a directory" },
|
|
436
|
+
{ value: "reindex" as ContextAction, label: "Re-index all" },
|
|
437
|
+
{ value: "back" as ContextAction, label: "Back" },
|
|
438
|
+
],
|
|
439
|
+
}),
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (action === "back") return;
|
|
443
|
+
|
|
444
|
+
if (action === "add") {
|
|
445
|
+
const dirPath = handleCancel(
|
|
446
|
+
await p.text({
|
|
447
|
+
message: "Path to document directory:",
|
|
448
|
+
placeholder: "e.g. docs, specs, ./reference",
|
|
449
|
+
validate(input) {
|
|
450
|
+
if (!input) return "Please enter a directory path";
|
|
451
|
+
},
|
|
452
|
+
}),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const name = handleCancel(
|
|
456
|
+
await p.text({
|
|
457
|
+
message: "Label (optional):",
|
|
458
|
+
placeholder: "e.g. Project Documentation",
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
await withSpinner(
|
|
464
|
+
"Indexing documents...",
|
|
465
|
+
async () => addContextRepo(dirPath, { name: name || undefined }),
|
|
466
|
+
"Indexing complete",
|
|
467
|
+
);
|
|
468
|
+
} catch (err) {
|
|
469
|
+
p.log.error((err as Error).message);
|
|
470
|
+
}
|
|
471
|
+
} else if (action === "remove") {
|
|
472
|
+
if (summary.totalRepos === 0) {
|
|
473
|
+
p.log.warn("No context repos to remove.");
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const selected = handleCancel(
|
|
478
|
+
await p.select({
|
|
479
|
+
message: "Select a repo to remove:",
|
|
480
|
+
options: [
|
|
481
|
+
...summary.repos.map((r) => ({
|
|
482
|
+
value: r.name,
|
|
483
|
+
label: r.name,
|
|
484
|
+
hint: `${r.fileCount} files, ${formatSize(r.totalSize)}`,
|
|
485
|
+
})),
|
|
486
|
+
{ value: "__back__", label: "Back" },
|
|
487
|
+
],
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
if (selected === "__back__") return;
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
removeContextRepo(selected);
|
|
495
|
+
p.log.success(`Removed "${selected}"`);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
p.log.error((err as Error).message);
|
|
498
|
+
}
|
|
499
|
+
} else if (action === "reindex") {
|
|
500
|
+
if (summary.totalRepos === 0) {
|
|
501
|
+
p.log.warn("No context repos to reindex.");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
await withSpinner(
|
|
507
|
+
"Re-scanning documents...",
|
|
508
|
+
async () => reindexContext(),
|
|
509
|
+
"Reindex complete",
|
|
510
|
+
);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
p.log.error((err as Error).message);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function listFlow(): void {
|
|
518
|
+
const lockfile = readLockfile();
|
|
519
|
+
const entries = Object.entries(lockfile.packages);
|
|
520
|
+
|
|
521
|
+
if (entries.length === 0) {
|
|
522
|
+
p.log.info("No packages installed. Select \"Install a package\" to get started.");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const lines = entries.map(
|
|
527
|
+
([name, entry]) =>
|
|
528
|
+
`${name} (${entry.type} v${entry.version}) -> ${entry.installed_to}`,
|
|
529
|
+
);
|
|
530
|
+
p.note(lines.join("\n"), "Installed packages");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function doctorFlow(): void {
|
|
534
|
+
const result = runDoctor();
|
|
535
|
+
|
|
536
|
+
if (result.issues.length === 0) {
|
|
537
|
+
p.log.success(`Checked ${result.packagesChecked} package(s) — no issues found.`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
542
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
543
|
+
|
|
544
|
+
for (const issue of errors) {
|
|
545
|
+
p.log.error(issue.message);
|
|
546
|
+
}
|
|
547
|
+
for (const issue of warnings) {
|
|
548
|
+
p.log.warn(issue.message);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (errors.length > 0) {
|
|
552
|
+
p.log.error(`${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
553
|
+
} else {
|
|
554
|
+
p.log.warn(`${warnings.length} warning(s)`);
|
|
555
|
+
}
|
|
556
|
+
}
|
package/src/commands/login.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
2
3
|
import { execSync } from "node:child_process";
|
|
3
4
|
import { setGitHubToken, getGitHubToken } from "../lib/config.js";
|
|
4
5
|
import { logger } from "../lib/logger.js";
|
|
6
|
+
import { isInteractive, handleCancel, withSpinner } from "../lib/prompts.js";
|
|
5
7
|
|
|
6
8
|
export const loginCommand = new Command("login")
|
|
7
9
|
.description("Configure GitHub authentication")
|
|
@@ -19,16 +21,18 @@ export const loginCommand = new Command("login")
|
|
|
19
21
|
logger.error("Failed to read token from GitHub CLI. Make sure `gh` is installed and authenticated.");
|
|
20
22
|
process.exit(1);
|
|
21
23
|
}
|
|
22
|
-
} else {
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
resolve(answer.trim());
|
|
30
|
-
});
|
|
24
|
+
} else if (isInteractive()) {
|
|
25
|
+
p.intro("planmode login");
|
|
26
|
+
const value = await p.password({
|
|
27
|
+
message: "GitHub personal access token:",
|
|
28
|
+
validate(input) {
|
|
29
|
+
if (!input) return "Token is required";
|
|
30
|
+
},
|
|
31
31
|
});
|
|
32
|
+
token = handleCancel(value);
|
|
33
|
+
} else {
|
|
34
|
+
logger.error("No token provided. Use --token <token> or --gh.");
|
|
35
|
+
process.exit(1);
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (!token) {
|
|
@@ -37,20 +41,43 @@ export const loginCommand = new Command("login")
|
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
// Validate token
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
const validateToken = async () => {
|
|
45
|
+
const response = await fetch("https://api.github.com/user", {
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${token}`,
|
|
48
|
+
"User-Agent": "planmode-cli",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error("Invalid token. GitHub API returned: " + response.status);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (await response.json()) as { login: string };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const user = await withSpinner(
|
|
61
|
+
"Validating token...",
|
|
62
|
+
validateToken,
|
|
63
|
+
"Token validated",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
setGitHubToken(token);
|
|
67
|
+
|
|
68
|
+
if (isInteractive()) {
|
|
69
|
+
p.log.success(`Authenticated as ${user.login}`);
|
|
70
|
+
p.outro("You're all set!");
|
|
71
|
+
} else {
|
|
72
|
+
logger.success(`Authenticated as ${user.login}`);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (isInteractive()) {
|
|
76
|
+
p.log.error((err as Error).message);
|
|
77
|
+
p.outro("Authentication failed.");
|
|
78
|
+
} else {
|
|
79
|
+
logger.error((err as Error).message);
|
|
80
|
+
}
|
|
50
81
|
process.exit(1);
|
|
51
82
|
}
|
|
52
|
-
|
|
53
|
-
const user = (await response.json()) as { login: string };
|
|
54
|
-
setGitHubToken(token);
|
|
55
|
-
logger.success(`Authenticated as ${user.login}`);
|
|
56
83
|
});
|
package/src/commands/publish.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
2
3
|
import { publishPackage } from "../lib/publisher.js";
|
|
3
4
|
import { logger } from "../lib/logger.js";
|
|
5
|
+
import { isInteractive } from "../lib/prompts.js";
|
|
4
6
|
|
|
5
7
|
export const publishCommand = new Command("publish")
|
|
6
8
|
.description("Publish the current directory as a package to the registry")
|
|
7
9
|
.action(async () => {
|
|
8
10
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
if (isInteractive()) {
|
|
12
|
+
p.intro("Publishing package");
|
|
13
|
+
} else {
|
|
14
|
+
logger.blank();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = await publishPackage({ interactive: isInteractive() });
|
|
18
|
+
|
|
19
|
+
if (isInteractive()) {
|
|
20
|
+
p.outro(`Published ${result.packageName}@${result.version} — PR: ${result.prUrl}`);
|
|
21
|
+
} else {
|
|
22
|
+
logger.blank();
|
|
23
|
+
}
|
|
12
24
|
} catch (err) {
|
|
13
25
|
logger.error((err as Error).message);
|
|
14
26
|
process.exit(1);
|