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.
@@ -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
- logger.blank();
35
- logger.info("Analyzing commits...");
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 = await stopRecording(process.cwd(), {
38
- name: options.name,
39
- author: options.author,
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
- logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
60
- logger.blank();
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);
@@ -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, collectVariableValues, resolveVariable } from "../lib/template.js";
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 values: Record<string, string | number | boolean> = {};
53
+ const noInput = options.input === false;
53
54
 
54
- // First pass: resolve non-resolved variables
55
- for (const [name, def] of Object.entries(manifest.variables)) {
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
- // Second pass: resolve dynamic variables
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);
@@ -1,6 +1,9 @@
1
1
  import { Command } from "commander";
2
- import { searchPackages } from "../lib/registry.js";
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 searchPackages(query, {
14
- type: options.type,
15
- category: options.category,
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
- logger.info("No packages found matching your query.");
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
- logger.blank();
29
- logger.table(
30
- ["name", "type", "version", "description"],
31
- results.map((pkg) => [
32
- pkg.name,
33
- pkg.type,
34
- pkg.version,
35
- pkg.description.length > 50
36
- ? pkg.description.slice(0, 50) + "..."
37
- : pkg.description,
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
- logger.blank();
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);
@@ -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
- logger.blank();
15
- logger.info("Analyzing project...");
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 = takeSnapshot(process.cwd(), {
18
- name: options.name,
19
- author: options.author,
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
- logger.dim("Edit the generated plan, then run `planmode test` to validate and `planmode publish` when ready.");
41
- logger.blank();
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);
@@ -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
- logger.blank();
10
- logger.bold("Testing package...");
11
- logger.blank();
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
- logger.success(check.name);
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
- logger.error(`${check.name}: ${issue.message}`);
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
- logger.warn(`${check.name}: ${issue.message}`);
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
- logger.blank();
29
- if (result.passed) {
30
- logger.success(`All checks passed. Ready to publish.`);
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
- const errors = result.issues.filter((i) => i.severity === "error");
33
- const warnings = result.issues.filter((i) => i.severity === "warning");
34
- logger.error(`${errors.length} error(s), ${warnings.length} warning(s). Fix errors before publishing.`);
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);
@@ -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
- logger.blank();
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 = await updatePackage(packageName);
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
- logger.info("Already up to date.");
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
- logger.info("No packages installed.");
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
- let updatedCount = 0;
28
- for (const name of names) {
29
- try {
30
- const updated = await updatePackage(name);
31
- if (updated) updatedCount++;
32
- } catch (err) {
33
- logger.warn(`Failed to update ${name}: ${(err as Error).message}`);
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
- logger.info("All packages are up to date.");
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
- logger.success(`Updated ${updatedCount} package${updatedCount > 1 ? "s" : ""}.`);
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
- logger.blank();
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,15 @@ 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 { contextCommand } from "./commands/context.js";
18
+ import { isInteractive } from "./lib/prompts.js";
17
19
 
18
20
  const program = new Command();
19
21
 
20
22
  program
21
23
  .name("planmode")
22
24
  .description("The open source package manager for AI plans, rules, and prompts.")
23
- .version("0.2.2");
25
+ .version("0.4.0");
24
26
 
25
27
  program.addCommand(installCommand);
26
28
  program.addCommand(uninstallCommand);
@@ -37,5 +39,12 @@ program.addCommand(doctorCommand);
37
39
  program.addCommand(testCommand);
38
40
  program.addCommand(recordCommand);
39
41
  program.addCommand(snapshotCommand);
42
+ program.addCommand(contextCommand);
40
43
 
41
- program.parse();
44
+ // If no args and interactive TTY, show the interactive menu
45
+ if (process.argv.length <= 2 && isInteractive()) {
46
+ const { runInteractiveMenu } = await import("./commands/interactive.js");
47
+ runInteractiveMenu();
48
+ } else {
49
+ program.parse();
50
+ }