planmode 0.2.1 → 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/README.md +79 -4
- package/dist/index.js +1720 -758
- package/dist/mcp.js +316 -156
- 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/mcp.ts +1 -1
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);
|
package/src/commands/record.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { startRecordingAsync, stopRecording, isRecording } from "../lib/recorder.js";
|
|
5
6
|
import { logger } from "../lib/logger.js";
|
|
7
|
+
import { isInteractive, withSpinner } from "../lib/prompts.js";
|
|
6
8
|
|
|
7
9
|
export const recordCommand = new Command("record")
|
|
8
10
|
.description("Record git activity and generate a plan from commits");
|
|
@@ -31,13 +33,30 @@ recordCommand
|
|
|
31
33
|
.option("--dir <dir>", "Output directory for the generated package (default: current directory)")
|
|
32
34
|
.action(async (options: { name?: string; author?: string; dir?: string }) => {
|
|
33
35
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
const interactive = isInteractive();
|
|
37
|
+
|
|
38
|
+
if (interactive) {
|
|
39
|
+
p.intro("Generating plan from recording");
|
|
40
|
+
} else {
|
|
41
|
+
logger.blank();
|
|
42
|
+
}
|
|
36
43
|
|
|
37
|
-
const result =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
const result = interactive
|
|
45
|
+
? await withSpinner(
|
|
46
|
+
"Analyzing commits...",
|
|
47
|
+
() => stopRecording(process.cwd(), {
|
|
48
|
+
name: options.name,
|
|
49
|
+
author: options.author,
|
|
50
|
+
}),
|
|
51
|
+
"Analysis complete",
|
|
52
|
+
)
|
|
53
|
+
: await (async () => {
|
|
54
|
+
logger.info("Analyzing commits...");
|
|
55
|
+
return stopRecording(process.cwd(), {
|
|
56
|
+
name: options.name,
|
|
57
|
+
author: options.author,
|
|
58
|
+
});
|
|
59
|
+
})();
|
|
41
60
|
|
|
42
61
|
// Write to output directory
|
|
43
62
|
const outDir = options.dir ?? process.cwd();
|
|
@@ -56,8 +75,13 @@ recordCommand
|
|
|
56
75
|
|
|
57
76
|
logger.blank();
|
|
58
77
|
logger.success("Created planmode.yaml and plan.md");
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
|
|
79
|
+
if (interactive) {
|
|
80
|
+
p.outro("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
|
|
81
|
+
} else {
|
|
82
|
+
logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
|
|
83
|
+
logger.blank();
|
|
84
|
+
}
|
|
61
85
|
} catch (err) {
|
|
62
86
|
logger.error((err as Error).message);
|
|
63
87
|
process.exit(1);
|
package/src/commands/run.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { Command } from "commander";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { parseManifest, readPackageContent } from "../lib/manifest.js";
|
|
5
|
-
import { renderTemplate,
|
|
5
|
+
import { renderTemplate, resolveVariable } from "../lib/template.js";
|
|
6
6
|
import { logger } from "../lib/logger.js";
|
|
7
|
+
import { isInteractive, promptForVariables } from "../lib/prompts.js";
|
|
7
8
|
|
|
8
9
|
export const runCommand = new Command("run")
|
|
9
10
|
.description("Run a templated prompt and output to stdout")
|
|
@@ -49,22 +50,12 @@ export const runCommand = new Command("run")
|
|
|
49
50
|
|
|
50
51
|
// Resolve variables
|
|
51
52
|
if (manifest?.variables && Object.keys(manifest.variables).length > 0) {
|
|
52
|
-
const
|
|
53
|
+
const noInput = options.input === false;
|
|
53
54
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
if (def.type === "resolved") continue;
|
|
57
|
-
if (vars[name] !== undefined) {
|
|
58
|
-
values[name] = vars[name]!;
|
|
59
|
-
} else if (def.default !== undefined) {
|
|
60
|
-
values[name] = def.default;
|
|
61
|
-
} else if (def.required && options.input === false) {
|
|
62
|
-
logger.error(`Missing required variable: --${name}`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
55
|
+
// Collect non-resolved variables (interactive or from flags/defaults)
|
|
56
|
+
const values = await promptForVariables(manifest.variables, vars, noInput);
|
|
66
57
|
|
|
67
|
-
//
|
|
58
|
+
// Resolve dynamic variables
|
|
68
59
|
for (const [name, def] of Object.entries(manifest.variables)) {
|
|
69
60
|
if (def.type !== "resolved") continue;
|
|
70
61
|
values[name] = await resolveVariable(def, values);
|
package/src/commands/search.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { searchPackages, fetchPackageMetadata } from "../lib/registry.js";
|
|
4
|
+
import { installPackage } from "../lib/installer.js";
|
|
3
5
|
import { logger } from "../lib/logger.js";
|
|
6
|
+
import { isInteractive, handleCancel, withSpinner } from "../lib/prompts.js";
|
|
4
7
|
|
|
5
8
|
export const searchCommand = new Command("search")
|
|
6
9
|
.description("Search the registry for packages")
|
|
@@ -10,13 +13,20 @@ export const searchCommand = new Command("search")
|
|
|
10
13
|
.option("--json", "Output as JSON")
|
|
11
14
|
.action(async (query: string, options: { type?: string; category?: string; json?: boolean }) => {
|
|
12
15
|
try {
|
|
13
|
-
const results = await
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const results = await withSpinner(
|
|
17
|
+
"Searching registry...",
|
|
18
|
+
() => searchPackages(query, {
|
|
19
|
+
type: options.type,
|
|
20
|
+
category: options.category,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
17
23
|
|
|
18
24
|
if (results.length === 0) {
|
|
19
|
-
|
|
25
|
+
if (isInteractive() && !options.json) {
|
|
26
|
+
p.log.warn("No packages found matching your query.");
|
|
27
|
+
} else {
|
|
28
|
+
logger.info("No packages found matching your query.");
|
|
29
|
+
}
|
|
20
30
|
return;
|
|
21
31
|
}
|
|
22
32
|
|
|
@@ -25,19 +35,80 @@ export const searchCommand = new Command("search")
|
|
|
25
35
|
return;
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pkg
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Non-interactive: just show the table
|
|
39
|
+
if (!isInteractive()) {
|
|
40
|
+
logger.blank();
|
|
41
|
+
logger.table(
|
|
42
|
+
["name", "type", "version", "description"],
|
|
43
|
+
results.map((pkg) => [
|
|
44
|
+
pkg.name,
|
|
45
|
+
pkg.type,
|
|
46
|
+
pkg.version,
|
|
47
|
+
pkg.description.length > 50
|
|
48
|
+
? pkg.description.slice(0, 50) + "..."
|
|
49
|
+
: pkg.description,
|
|
50
|
+
]),
|
|
51
|
+
);
|
|
52
|
+
logger.blank();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Interactive: let user select a package
|
|
57
|
+
const selected = handleCancel(
|
|
58
|
+
await p.select({
|
|
59
|
+
message: `Found ${results.length} package(s). Select one:`,
|
|
60
|
+
options: [
|
|
61
|
+
...results.map((pkg) => ({
|
|
62
|
+
value: pkg.name,
|
|
63
|
+
label: `${pkg.name} (${pkg.type} v${pkg.version})`,
|
|
64
|
+
hint: pkg.description.length > 60
|
|
65
|
+
? pkg.description.slice(0, 60) + "..."
|
|
66
|
+
: pkg.description,
|
|
67
|
+
})),
|
|
68
|
+
{ value: "__none__", label: "Cancel" },
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
39
71
|
);
|
|
40
|
-
|
|
72
|
+
|
|
73
|
+
if (selected === "__none__") return;
|
|
74
|
+
|
|
75
|
+
const action = handleCancel(
|
|
76
|
+
await p.select({
|
|
77
|
+
message: `${selected}:`,
|
|
78
|
+
options: [
|
|
79
|
+
{ value: "install", label: "Install" },
|
|
80
|
+
{ value: "details", label: "View details" },
|
|
81
|
+
{ value: "back", label: "Cancel" },
|
|
82
|
+
],
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (action === "install") {
|
|
87
|
+
try {
|
|
88
|
+
await installPackage(selected, { interactive: true });
|
|
89
|
+
p.log.success(`Installed ${selected}`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
p.log.error((err as Error).message);
|
|
92
|
+
}
|
|
93
|
+
} else if (action === "details") {
|
|
94
|
+
const meta = await withSpinner(
|
|
95
|
+
"Fetching package details...",
|
|
96
|
+
() => fetchPackageMetadata(selected),
|
|
97
|
+
);
|
|
98
|
+
const lines = [
|
|
99
|
+
`Type: ${meta.type}`,
|
|
100
|
+
`Author: ${meta.author}`,
|
|
101
|
+
`License: ${meta.license}`,
|
|
102
|
+
`Category: ${meta.category}`,
|
|
103
|
+
`Downloads: ${meta.downloads.toLocaleString()}`,
|
|
104
|
+
`Versions: ${meta.versions.join(", ")}`,
|
|
105
|
+
`Repository: ${meta.repository}`,
|
|
106
|
+
];
|
|
107
|
+
if (meta.tags?.length) {
|
|
108
|
+
lines.push(`Tags: ${meta.tags.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
p.note(lines.join("\n"), `${meta.name}@${meta.latest_version}`);
|
|
111
|
+
}
|
|
41
112
|
} catch (err) {
|
|
42
113
|
logger.error((err as Error).message);
|
|
43
114
|
process.exit(1);
|
package/src/commands/snapshot.ts
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { takeSnapshot } from "../lib/snapshot.js";
|
|
5
6
|
import { logger } from "../lib/logger.js";
|
|
7
|
+
import { isInteractive, withSpinner } from "../lib/prompts.js";
|
|
6
8
|
|
|
7
9
|
export const snapshotCommand = new Command("snapshot")
|
|
8
10
|
.description("Analyze the current project and generate a plan that recreates this setup")
|
|
9
11
|
.option("--name <name>", "Package name (auto-inferred from project)")
|
|
10
12
|
.option("--author <author>", "Author GitHub username")
|
|
11
13
|
.option("--dir <dir>", "Output directory for the generated package (default: current directory)")
|
|
12
|
-
.action((options: { name?: string; author?: string; dir?: string }) => {
|
|
14
|
+
.action(async (options: { name?: string; author?: string; dir?: string }) => {
|
|
13
15
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
const interactive = isInteractive();
|
|
17
|
+
|
|
18
|
+
if (interactive) {
|
|
19
|
+
p.intro("Taking project snapshot");
|
|
20
|
+
} else {
|
|
21
|
+
logger.blank();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const doSnapshot = () => Promise.resolve(
|
|
25
|
+
takeSnapshot(process.cwd(), {
|
|
26
|
+
name: options.name,
|
|
27
|
+
author: options.author,
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
16
30
|
|
|
17
|
-
const result =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
const result = interactive
|
|
32
|
+
? await withSpinner("Analyzing project...", doSnapshot, "Analysis complete")
|
|
33
|
+
: (() => {
|
|
34
|
+
logger.info("Analyzing project...");
|
|
35
|
+
return takeSnapshot(process.cwd(), {
|
|
36
|
+
name: options.name,
|
|
37
|
+
author: options.author,
|
|
38
|
+
});
|
|
39
|
+
})();
|
|
21
40
|
|
|
22
41
|
// Write to output directory
|
|
23
42
|
const outDir = options.dir ?? process.cwd();
|
|
@@ -37,8 +56,13 @@ export const snapshotCommand = new Command("snapshot")
|
|
|
37
56
|
|
|
38
57
|
logger.blank();
|
|
39
58
|
logger.success("Created planmode.yaml and plan.md");
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
|
|
60
|
+
if (interactive) {
|
|
61
|
+
p.outro("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
|
|
62
|
+
} else {
|
|
63
|
+
logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
|
|
64
|
+
logger.blank();
|
|
65
|
+
}
|
|
42
66
|
} catch (err) {
|
|
43
67
|
logger.error((err as Error).message);
|
|
44
68
|
process.exit(1);
|
package/src/commands/test.ts
CHANGED
|
@@ -1,39 +1,69 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
2
3
|
import { testPackage } from "../lib/tester.js";
|
|
3
4
|
import { logger } from "../lib/logger.js";
|
|
5
|
+
import { isInteractive } from "../lib/prompts.js";
|
|
4
6
|
|
|
5
7
|
export const testCommand = new Command("test")
|
|
6
8
|
.description("Test the current package before publishing: validate manifest, render templates, check dependencies")
|
|
7
9
|
.action(async () => {
|
|
8
10
|
try {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const interactive = isInteractive();
|
|
12
|
+
|
|
13
|
+
if (interactive) {
|
|
14
|
+
p.intro("Testing package");
|
|
15
|
+
} else {
|
|
16
|
+
logger.blank();
|
|
17
|
+
logger.bold("Testing package...");
|
|
18
|
+
logger.blank();
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
const result = await testPackage();
|
|
14
22
|
|
|
15
23
|
for (const check of result.checks) {
|
|
16
24
|
if (check.passed) {
|
|
17
|
-
|
|
25
|
+
if (interactive) {
|
|
26
|
+
p.log.success(check.name);
|
|
27
|
+
} else {
|
|
28
|
+
logger.success(check.name);
|
|
29
|
+
}
|
|
18
30
|
} else {
|
|
19
31
|
const issue = result.issues.find((i) => i.check === check.name);
|
|
20
32
|
if (issue?.severity === "error") {
|
|
21
|
-
|
|
33
|
+
if (interactive) {
|
|
34
|
+
p.log.error(`${check.name}: ${issue.message}`);
|
|
35
|
+
} else {
|
|
36
|
+
logger.error(`${check.name}: ${issue.message}`);
|
|
37
|
+
}
|
|
22
38
|
} else if (issue) {
|
|
23
|
-
|
|
39
|
+
if (interactive) {
|
|
40
|
+
p.log.warn(`${check.name}: ${issue.message}`);
|
|
41
|
+
} else {
|
|
42
|
+
logger.warn(`${check.name}: ${issue.message}`);
|
|
43
|
+
}
|
|
24
44
|
}
|
|
25
45
|
}
|
|
26
46
|
}
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
if (interactive) {
|
|
49
|
+
if (result.passed) {
|
|
50
|
+
p.outro("All checks passed. Ready to publish.");
|
|
51
|
+
} else {
|
|
52
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
53
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
54
|
+
p.outro(`${errors.length} error(s), ${warnings.length} warning(s). Fix errors before publishing.`);
|
|
55
|
+
}
|
|
31
56
|
} else {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
logger.blank();
|
|
58
|
+
if (result.passed) {
|
|
59
|
+
logger.success(`All checks passed. Ready to publish.`);
|
|
60
|
+
} else {
|
|
61
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
62
|
+
const warnings = result.issues.filter((i) => i.severity === "warning");
|
|
63
|
+
logger.error(`${errors.length} error(s), ${warnings.length} warning(s). Fix errors before publishing.`);
|
|
64
|
+
}
|
|
65
|
+
logger.blank();
|
|
35
66
|
}
|
|
36
|
-
logger.blank();
|
|
37
67
|
|
|
38
68
|
if (!result.passed) {
|
|
39
69
|
process.exit(1);
|
package/src/commands/update.ts
CHANGED
|
@@ -1,47 +1,89 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
2
3
|
import { updatePackage } from "../lib/installer.js";
|
|
3
4
|
import { readLockfile } from "../lib/lockfile.js";
|
|
4
5
|
import { logger } from "../lib/logger.js";
|
|
6
|
+
import { isInteractive, withSpinner } from "../lib/prompts.js";
|
|
5
7
|
|
|
6
8
|
export const updateCommand = new Command("update")
|
|
7
9
|
.description("Update installed packages to latest compatible versions")
|
|
8
10
|
.argument("[package]", "Package name (omit to update all)")
|
|
9
11
|
.action(async (packageName?: string) => {
|
|
10
12
|
try {
|
|
11
|
-
|
|
13
|
+
const interactive = isInteractive();
|
|
14
|
+
|
|
15
|
+
if (interactive) {
|
|
16
|
+
p.intro("Updating packages");
|
|
17
|
+
} else {
|
|
18
|
+
logger.blank();
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
if (packageName) {
|
|
14
|
-
const updated =
|
|
22
|
+
const updated = interactive
|
|
23
|
+
? await withSpinner(
|
|
24
|
+
`Checking ${packageName} for updates...`,
|
|
25
|
+
() => updatePackage(packageName),
|
|
26
|
+
)
|
|
27
|
+
: await updatePackage(packageName);
|
|
28
|
+
|
|
15
29
|
if (!updated) {
|
|
16
|
-
|
|
30
|
+
if (interactive) {
|
|
31
|
+
p.log.info("Already up to date.");
|
|
32
|
+
} else {
|
|
33
|
+
logger.info("Already up to date.");
|
|
34
|
+
}
|
|
17
35
|
}
|
|
18
36
|
} else {
|
|
19
37
|
const lockfile = readLockfile();
|
|
20
38
|
const names = Object.keys(lockfile.packages);
|
|
21
39
|
|
|
22
40
|
if (names.length === 0) {
|
|
23
|
-
|
|
41
|
+
if (interactive) {
|
|
42
|
+
p.log.info("No packages installed.");
|
|
43
|
+
} else {
|
|
44
|
+
logger.info("No packages installed.");
|
|
45
|
+
}
|
|
46
|
+
if (interactive) p.outro("Nothing to update.");
|
|
24
47
|
return;
|
|
25
48
|
}
|
|
26
49
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
const doUpdate = async () => {
|
|
51
|
+
let updatedCount = 0;
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
try {
|
|
54
|
+
const updated = await updatePackage(name);
|
|
55
|
+
if (updated) updatedCount++;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.warn(`Failed to update ${name}: ${(err as Error).message}`);
|
|
58
|
+
}
|
|
34
59
|
}
|
|
35
|
-
|
|
60
|
+
return updatedCount;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const updatedCount = interactive
|
|
64
|
+
? await withSpinner("Checking for updates...", doUpdate)
|
|
65
|
+
: await doUpdate();
|
|
36
66
|
|
|
37
67
|
if (updatedCount === 0) {
|
|
38
|
-
|
|
68
|
+
if (interactive) {
|
|
69
|
+
p.log.info("All packages are up to date.");
|
|
70
|
+
} else {
|
|
71
|
+
logger.info("All packages are up to date.");
|
|
72
|
+
}
|
|
39
73
|
} else {
|
|
40
|
-
|
|
74
|
+
if (interactive) {
|
|
75
|
+
p.log.success(`Updated ${updatedCount} package${updatedCount > 1 ? "s" : ""}.`);
|
|
76
|
+
} else {
|
|
77
|
+
logger.success(`Updated ${updatedCount} package${updatedCount > 1 ? "s" : ""}.`);
|
|
78
|
+
}
|
|
41
79
|
}
|
|
42
80
|
}
|
|
43
81
|
|
|
44
|
-
|
|
82
|
+
if (interactive) {
|
|
83
|
+
p.outro("Done!");
|
|
84
|
+
} else {
|
|
85
|
+
logger.blank();
|
|
86
|
+
}
|
|
45
87
|
} catch (err) {
|
|
46
88
|
logger.error((err as Error).message);
|
|
47
89
|
process.exit(1);
|
package/src/index.ts
CHANGED
|
@@ -14,13 +14,14 @@ import { doctorCommand } from "./commands/doctor.js";
|
|
|
14
14
|
import { testCommand } from "./commands/test.js";
|
|
15
15
|
import { recordCommand } from "./commands/record.js";
|
|
16
16
|
import { snapshotCommand } from "./commands/snapshot.js";
|
|
17
|
+
import { isInteractive } from "./lib/prompts.js";
|
|
17
18
|
|
|
18
19
|
const program = new Command();
|
|
19
20
|
|
|
20
21
|
program
|
|
21
22
|
.name("planmode")
|
|
22
23
|
.description("The open source package manager for AI plans, rules, and prompts.")
|
|
23
|
-
.version("0.
|
|
24
|
+
.version("0.3.0");
|
|
24
25
|
|
|
25
26
|
program.addCommand(installCommand);
|
|
26
27
|
program.addCommand(uninstallCommand);
|
|
@@ -38,4 +39,10 @@ program.addCommand(testCommand);
|
|
|
38
39
|
program.addCommand(recordCommand);
|
|
39
40
|
program.addCommand(snapshotCommand);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
// If no args and interactive TTY, show the interactive menu
|
|
43
|
+
if (process.argv.length <= 2 && isInteractive()) {
|
|
44
|
+
const { runInteractiveMenu } = await import("./commands/interactive.js");
|
|
45
|
+
runInteractiveMenu();
|
|
46
|
+
} else {
|
|
47
|
+
program.parse();
|
|
48
|
+
}
|