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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planmode",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "The open source package manager for AI plans, rules, and prompts.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "typecheck": "tsc --noEmit"
17
17
  },
18
18
  "dependencies": {
19
+ "@clack/prompts": "^0.9.1",
19
20
  "@modelcontextprotocol/sdk": "^1.26.0",
20
21
  "commander": "^13.1.0",
21
22
  "handlebars": "^4.7.8",
@@ -0,0 +1,111 @@
1
+ import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
3
+ import { addContextRepo, removeContextRepo, reindexContext, getContextSummary, formatSize } from "../lib/context.js";
4
+ import { logger } from "../lib/logger.js";
5
+ import { isInteractive, withSpinner } from "../lib/prompts.js";
6
+
7
+ export const contextCommand = new Command("context")
8
+ .description("Manage project document context for AI");
9
+
10
+ contextCommand
11
+ .command("add <path>")
12
+ .description("Add a document directory to the project context")
13
+ .option("--name <name>", "Human-readable label for this directory")
14
+ .action(async (dirPath: string, options: { name?: string }) => {
15
+ try {
16
+ const interactive = isInteractive();
17
+
18
+ if (interactive) {
19
+ await withSpinner(
20
+ "Indexing documents...",
21
+ async () => addContextRepo(dirPath, { name: options.name }),
22
+ "Indexing complete",
23
+ );
24
+ } else {
25
+ logger.blank();
26
+ addContextRepo(dirPath, { name: options.name });
27
+ logger.blank();
28
+ }
29
+ } catch (err) {
30
+ logger.error((err as Error).message);
31
+ process.exit(1);
32
+ }
33
+ });
34
+
35
+ contextCommand
36
+ .command("remove <path-or-name>")
37
+ .description("Remove a directory from the project context")
38
+ .action((pathOrName: string) => {
39
+ try {
40
+ logger.blank();
41
+ removeContextRepo(pathOrName);
42
+ logger.blank();
43
+ } catch (err) {
44
+ logger.error((err as Error).message);
45
+ process.exit(1);
46
+ }
47
+ });
48
+
49
+ contextCommand
50
+ .command("list")
51
+ .description("Show all directories in the project context")
52
+ .option("--json", "Output as JSON")
53
+ .action((options: { json?: boolean }) => {
54
+ try {
55
+ const summary = getContextSummary();
56
+
57
+ if (options.json) {
58
+ console.log(JSON.stringify(summary, null, 2));
59
+ return;
60
+ }
61
+
62
+ logger.blank();
63
+
64
+ if (summary.totalRepos === 0) {
65
+ logger.info("No context repos configured. Run `planmode context add <path>` to add one.");
66
+ logger.blank();
67
+ return;
68
+ }
69
+
70
+ logger.bold(`${summary.totalRepos} context repo(s) — ${summary.totalFiles} file(s), ${formatSize(summary.totalSize)}`);
71
+ logger.blank();
72
+
73
+ for (const repo of summary.repos) {
74
+ logger.info(`${repo.name}`);
75
+ logger.dim(` Path: ${repo.path}`);
76
+ logger.dim(` Files: ${repo.fileCount} (${formatSize(repo.totalSize)})`);
77
+ if (repo.typeBreakdown.length > 0) {
78
+ logger.dim(` Types: ${repo.typeBreakdown.join(", ")}`);
79
+ }
80
+ logger.dim(` Indexed: ${repo.indexedAt}`);
81
+ logger.blank();
82
+ }
83
+ } catch (err) {
84
+ logger.error((err as Error).message);
85
+ process.exit(1);
86
+ }
87
+ });
88
+
89
+ contextCommand
90
+ .command("reindex [path-or-name]")
91
+ .description("Re-scan files in one or all context directories")
92
+ .action(async (pathOrName?: string) => {
93
+ try {
94
+ const interactive = isInteractive();
95
+
96
+ if (interactive) {
97
+ await withSpinner(
98
+ "Re-scanning documents...",
99
+ async () => reindexContext(pathOrName),
100
+ "Reindex complete",
101
+ );
102
+ } else {
103
+ logger.blank();
104
+ reindexContext(pathOrName);
105
+ logger.blank();
106
+ }
107
+ } catch (err) {
108
+ logger.error((err as Error).message);
109
+ process.exit(1);
110
+ }
111
+ });
@@ -1,19 +1,35 @@
1
1
  import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
2
3
  import { runDoctor } from "../lib/doctor.js";
3
4
  import { logger } from "../lib/logger.js";
5
+ import { isInteractive } from "../lib/prompts.js";
4
6
 
5
7
  export const doctorCommand = new Command("doctor")
6
8
  .description("Check project health: verify installed packages, imports, and file integrity")
7
9
  .action(() => {
10
+ const interactive = isInteractive();
8
11
  const result = runDoctor();
9
12
 
10
- logger.blank();
11
- logger.bold(`Checked ${result.packagesChecked} package(s)`);
12
- logger.blank();
13
+ if (interactive) {
14
+ p.intro("Health check");
15
+ } else {
16
+ logger.blank();
17
+ }
13
18
 
14
- if (result.issues.length === 0) {
15
- logger.success("Everything looks good. No issues found.");
19
+ if (interactive) {
20
+ p.log.info(`Checked ${result.packagesChecked} package(s)`);
21
+ } else {
22
+ logger.bold(`Checked ${result.packagesChecked} package(s)`);
16
23
  logger.blank();
24
+ }
25
+
26
+ if (result.issues.length === 0) {
27
+ if (interactive) {
28
+ p.outro("Everything looks good. No issues found.");
29
+ } else {
30
+ logger.success("Everything looks good. No issues found.");
31
+ logger.blank();
32
+ }
17
33
  return;
18
34
  }
19
35
 
@@ -21,21 +37,37 @@ export const doctorCommand = new Command("doctor")
21
37
  const warnings = result.issues.filter((i) => i.severity === "warning");
22
38
 
23
39
  for (const issue of errors) {
24
- logger.error(issue.message);
25
- if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
40
+ if (interactive) {
41
+ p.log.error(issue.message);
42
+ } else {
43
+ logger.error(issue.message);
44
+ if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
45
+ }
26
46
  }
27
47
  for (const issue of warnings) {
28
- logger.warn(issue.message);
29
- if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
48
+ if (interactive) {
49
+ p.log.warn(issue.message);
50
+ } else {
51
+ logger.warn(issue.message);
52
+ if (issue.fix) logger.dim(` Fix: ${issue.fix}`);
53
+ }
30
54
  }
31
55
 
32
- logger.blank();
33
- if (errors.length > 0) {
34
- logger.error(`${errors.length} error(s), ${warnings.length} warning(s)`);
56
+ if (interactive) {
57
+ if (errors.length > 0) {
58
+ p.outro(`${errors.length} error(s), ${warnings.length} warning(s)`);
59
+ } else {
60
+ p.outro(`${warnings.length} warning(s)`);
61
+ }
35
62
  } else {
36
- logger.warn(`${warnings.length} warning(s)`);
63
+ logger.blank();
64
+ if (errors.length > 0) {
65
+ logger.error(`${errors.length} error(s), ${warnings.length} warning(s)`);
66
+ } else {
67
+ logger.warn(`${warnings.length} warning(s)`);
68
+ }
69
+ logger.blank();
37
70
  }
38
- logger.blank();
39
71
 
40
72
  if (errors.length > 0) {
41
73
  process.exit(1);
@@ -1,64 +1,112 @@
1
1
  import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
2
3
  import { logger } from "../lib/logger.js";
3
4
  import { createPackage } from "../lib/init.js";
5
+ import { isInteractive, handleCancel } from "../lib/prompts.js";
4
6
  import type { PackageType, Category } from "../types/index.js";
5
7
 
6
- async function prompt(question: string): Promise<string> {
7
- const { createInterface } = await import("node:readline");
8
- const rl = createInterface({ input: process.stdin, output: process.stdout });
9
- return new Promise((resolve) => {
10
- rl.question(question, (answer) => {
11
- rl.close();
12
- resolve(answer.trim());
13
- });
8
+ const CATEGORIES: Category[] = [
9
+ "frontend",
10
+ "backend",
11
+ "devops",
12
+ "database",
13
+ "testing",
14
+ "mobile",
15
+ "ai-ml",
16
+ "design",
17
+ "security",
18
+ "other",
19
+ ];
20
+
21
+ export async function initInteractive(): Promise<void> {
22
+ p.intro("Create a new planmode package");
23
+
24
+ const result = await p.group(
25
+ {
26
+ name: () =>
27
+ p.text({
28
+ message: "Package name",
29
+ placeholder: "my-awesome-plan",
30
+ validate(input) {
31
+ if (!input) return "Package name is required";
32
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(input))
33
+ return "Lowercase letters, numbers, and hyphens only";
34
+ },
35
+ }),
36
+ type: () =>
37
+ p.select<PackageType>({
38
+ message: "Package type",
39
+ options: [
40
+ { value: "plan" as PackageType, label: "Plan", hint: "multi-step implementation guide" },
41
+ { value: "rule" as PackageType, label: "Rule", hint: "always-on coding constraint" },
42
+ { value: "prompt" as PackageType, label: "Prompt", hint: "single-use templated prompt" },
43
+ ],
44
+ }),
45
+ description: () =>
46
+ p.text({
47
+ message: "Description",
48
+ placeholder: "A short description of what this package does",
49
+ }),
50
+ author: () =>
51
+ p.text({
52
+ message: "Author (GitHub username)",
53
+ placeholder: "username",
54
+ }),
55
+ license: () =>
56
+ p.text({
57
+ message: "License",
58
+ defaultValue: "MIT",
59
+ placeholder: "MIT",
60
+ }),
61
+ category: () =>
62
+ p.select<Category>({
63
+ message: "Category",
64
+ options: CATEGORIES.map((cat) => ({ value: cat as Category, label: cat })),
65
+ initialValue: "other" as Category,
66
+ }),
67
+ tags: () =>
68
+ p.text({
69
+ message: "Tags (comma-separated)",
70
+ placeholder: "nextjs, tailwind, starter",
71
+ }),
72
+ },
73
+ {
74
+ onCancel() {
75
+ p.cancel("Cancelled.");
76
+ process.exit(0);
77
+ },
78
+ },
79
+ );
80
+
81
+ const tags = result.tags
82
+ ? result.tags.split(",").map((t) => t.trim().toLowerCase()).filter(Boolean)
83
+ : [];
84
+
85
+ const output = createPackage({
86
+ name: result.name,
87
+ type: result.type,
88
+ description: result.description ?? "",
89
+ author: result.author ?? "",
90
+ license: result.license || "MIT",
91
+ tags,
92
+ category: result.category,
14
93
  });
94
+
95
+ p.log.success(`Created ${output.files.join(", ")}`);
96
+ p.outro(`Edit ${output.files[1]}, then run \`planmode publish\` when ready.`);
15
97
  }
16
98
 
17
99
  export const initCommand = new Command("init")
18
100
  .description("Initialize a new package in the current directory")
19
101
  .action(async () => {
20
102
  try {
21
- logger.blank();
22
- logger.bold("Initialize a new Planmode package");
23
- logger.blank();
24
-
25
- const name = await prompt("Package name: ");
26
- if (!name) {
27
- logger.error("Package name is required.");
103
+ if (isInteractive()) {
104
+ await initInteractive();
105
+ } else {
106
+ // Non-interactive fallback: require all fields via env or fail
107
+ logger.error("Interactive terminal required for `planmode init`. Use a TTY.");
28
108
  process.exit(1);
29
109
  }
30
-
31
- const typeInput = await prompt("Type (plan/rule/prompt) [plan]: ");
32
- const type = (typeInput || "plan") as PackageType;
33
-
34
- const description = await prompt("Description: ");
35
- const author = await prompt("Author (GitHub username): ");
36
- const license = (await prompt("License [MIT]: ")) || "MIT";
37
- const tagsInput = await prompt("Tags (comma-separated): ");
38
- const tags = tagsInput
39
- ? tagsInput.split(",").map((t) => t.trim().toLowerCase())
40
- : [];
41
- const category =
42
- ((await prompt(
43
- "Category (frontend/backend/devops/database/testing/mobile/ai-ml/security/other) [other]: ",
44
- )) || "other") as Category;
45
-
46
- const result = createPackage({
47
- name,
48
- type,
49
- description,
50
- author,
51
- license,
52
- tags,
53
- category,
54
- });
55
-
56
- logger.success(`Created ${result.files.join(", ")}`);
57
- logger.blank();
58
- logger.info(
59
- `Edit ${result.files[1]}, then run \`planmode publish\` when ready.`,
60
- );
61
- logger.blank();
62
110
  } catch (err) {
63
111
  logger.error((err as Error).message);
64
112
  process.exit(1);
@@ -1,6 +1,8 @@
1
1
  import { Command } from "commander";
2
+ import * as p from "@clack/prompts";
2
3
  import { installPackage } from "../lib/installer.js";
3
4
  import { logger } from "../lib/logger.js";
5
+ import { isInteractive } from "../lib/prompts.js";
4
6
 
5
7
  function parseVariables(pairs: string[]): Record<string, string> {
6
8
  const vars: Record<string, string> = {};
@@ -27,15 +29,28 @@ export const installCommand = new Command("install")
27
29
  options: { version?: string; rule?: boolean; input?: boolean; set?: string[] },
28
30
  ) => {
29
31
  try {
30
- logger.blank();
32
+ const interactive = isInteractive() && options.input !== false;
31
33
  const variables = options.set ? parseVariables(options.set) : undefined;
34
+
35
+ if (interactive) {
36
+ p.intro(`Installing ${packageName}`);
37
+ } else {
38
+ logger.blank();
39
+ }
40
+
32
41
  await installPackage(packageName, {
33
42
  version: options.version,
34
43
  forceRule: options.rule,
35
44
  noInput: options.input === false,
36
45
  variables,
46
+ interactive,
37
47
  });
38
- logger.blank();
48
+
49
+ if (interactive) {
50
+ p.outro("Done!");
51
+ } else {
52
+ logger.blank();
53
+ }
39
54
  } catch (err) {
40
55
  logger.error((err as Error).message);
41
56
  process.exit(1);