supipowers 0.1.4 → 0.2.1

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/bin/install.mjs CHANGED
@@ -4,7 +4,6 @@ import {
4
4
  intro,
5
5
  outro,
6
6
  confirm,
7
- select,
8
7
  multiselect,
9
8
  spinner,
10
9
  isCancel,
@@ -12,7 +11,7 @@ import {
12
11
  note,
13
12
  } from "@clack/prompts";
14
13
  import { spawnSync } from "node:child_process";
15
- import { readFileSync, existsSync } from "node:fs";
14
+ import { readFileSync, existsSync, mkdirSync, cpSync, readdirSync } from "node:fs";
16
15
  import { resolve, dirname, join } from "node:path";
17
16
  import { fileURLToPath } from "node:url";
18
17
  import { homedir } from "node:os";
@@ -131,29 +130,40 @@ async function main() {
131
130
  ompSpinner.stop(`OMP ${ver} detected`);
132
131
  }
133
132
 
134
- // ── Step 2: Install supipowers ─────────────────────────────
135
-
136
- const scope = await select({
137
- message: "Where should supipowers be installed?",
138
- options: [
139
- { value: "global", label: "Global", hint: "available in all projects" },
140
- { value: "local", label: "Project-local", hint: "only this directory" },
141
- ],
142
- });
143
- if (isCancel(scope)) bail("Cancelled.");
144
-
145
- const packageSpec = `npm:supipowers@${VERSION}`;
146
- const installArgs = ["install", packageSpec];
147
- if (scope === "local") installArgs.push("-l");
133
+ // ── Step 2: Install supipowers into ~/.omp/agent/ ───────────
148
134
 
149
135
  const s = spinner();
150
- s.start(`Installing supipowers (${scope})...`);
151
- const result = run(omp, installArgs);
152
- if (result.status !== 0) {
136
+ s.start("Installing supipowers...");
137
+
138
+ const packageRoot = resolve(__dirname, "..");
139
+ const ompAgent = join(homedir(), ".omp", "agent");
140
+
141
+ try {
142
+ // Copy extension (src/ + package.json) → ~/.omp/agent/extensions/supipowers/
143
+ const extDir = join(ompAgent, "extensions", "supipowers");
144
+ mkdirSync(extDir, { recursive: true });
145
+ cpSync(join(packageRoot, "src"), join(extDir, "src"), { recursive: true });
146
+ cpSync(join(packageRoot, "package.json"), join(extDir, "package.json"));
147
+
148
+ // Copy skills → ~/.omp/agent/skills/<skillname>/SKILL.md
149
+ const skillsSource = join(packageRoot, "skills");
150
+ if (existsSync(skillsSource)) {
151
+ const skillDirs = readdirSync(skillsSource, { withFileTypes: true });
152
+ for (const entry of skillDirs) {
153
+ if (!entry.isDirectory()) continue;
154
+ const skillFile = join(skillsSource, entry.name, "SKILL.md");
155
+ if (!existsSync(skillFile)) continue;
156
+ const destDir = join(ompAgent, "skills", entry.name);
157
+ mkdirSync(destDir, { recursive: true });
158
+ cpSync(skillFile, join(destDir, "SKILL.md"));
159
+ }
160
+ }
161
+
162
+ s.stop("supipowers installed");
163
+ } catch (err) {
153
164
  s.stop("Installation failed");
154
- bail(result.stderr?.trim() || "omp install failed.");
165
+ bail(err.message || "Failed to copy files to ~/.omp/agent/");
155
166
  }
156
- s.stop("supipowers installed");
157
167
 
158
168
  // ── Step 3: LSP setup (optional) ──────────────────────────
159
169
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supipowers",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "OMP-native workflow extension inspired by Superpowers.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,70 +1,156 @@
1
1
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
2
2
  import { loadConfig, updateConfig } from "../config/loader.js";
3
- import { listProfiles, resolveProfile } from "../config/profiles.js";
4
- import { notifyInfo, notifySuccess } from "../notifications/renderer.js";
3
+ import { listProfiles } from "../config/profiles.js";
4
+ import type { SupipowersConfig } from "../types.js";
5
+
6
+ interface SettingDef {
7
+ label: string;
8
+ key: string;
9
+ type: "select" | "toggle" | "number" | "text";
10
+ options?: string[];
11
+ get: (config: SupipowersConfig) => string;
12
+ set: (cwd: string, value: unknown) => void;
13
+ }
14
+
15
+ function buildSettings(cwd: string): SettingDef[] {
16
+ return [
17
+ {
18
+ label: "Default profile",
19
+ key: "defaultProfile",
20
+ type: "select",
21
+ options: listProfiles(cwd),
22
+ get: (c) => c.defaultProfile,
23
+ set: (d, v) => updateConfig(d, { defaultProfile: v }),
24
+ },
25
+ {
26
+ label: "Max parallel agents",
27
+ key: "orchestration.maxParallelAgents",
28
+ type: "select",
29
+ options: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
30
+ get: (c) => String(c.orchestration.maxParallelAgents),
31
+ set: (d, v) => updateConfig(d, { orchestration: { maxParallelAgents: Number(v) } }),
32
+ },
33
+ {
34
+ label: "Max fix retries",
35
+ key: "orchestration.maxFixRetries",
36
+ type: "select",
37
+ options: ["0", "1", "2", "3", "4", "5"],
38
+ get: (c) => String(c.orchestration.maxFixRetries),
39
+ set: (d, v) => updateConfig(d, { orchestration: { maxFixRetries: Number(v) } }),
40
+ },
41
+ {
42
+ label: "Max nesting depth",
43
+ key: "orchestration.maxNestingDepth",
44
+ type: "select",
45
+ options: ["0", "1", "2", "3", "4", "5"],
46
+ get: (c) => String(c.orchestration.maxNestingDepth),
47
+ set: (d, v) => updateConfig(d, { orchestration: { maxNestingDepth: Number(v) } }),
48
+ },
49
+ {
50
+ label: "Model preference",
51
+ key: "orchestration.modelPreference",
52
+ type: "select",
53
+ options: ["auto", "fast", "balanced", "quality"],
54
+ get: (c) => c.orchestration.modelPreference,
55
+ set: (d, v) => updateConfig(d, { orchestration: { modelPreference: v } }),
56
+ },
57
+ {
58
+ label: "LSP setup guide",
59
+ key: "lsp.setupGuide",
60
+ type: "toggle",
61
+ get: (c) => c.lsp.setupGuide ? "on" : "off",
62
+ set: (d, v) => updateConfig(d, { lsp: { setupGuide: v === "on" } }),
63
+ },
64
+ {
65
+ label: "Notification verbosity",
66
+ key: "notifications.verbosity",
67
+ type: "select",
68
+ options: ["quiet", "normal", "verbose"],
69
+ get: (c) => c.notifications.verbosity,
70
+ set: (d, v) => updateConfig(d, { notifications: { verbosity: v } }),
71
+ },
72
+ {
73
+ label: "QA framework",
74
+ key: "qa.framework",
75
+ type: "text",
76
+ get: (c) => c.qa.framework ?? "not set",
77
+ set: (d, v) => updateConfig(d, { qa: { framework: v || null } }),
78
+ },
79
+ {
80
+ label: "QA command",
81
+ key: "qa.command",
82
+ type: "text",
83
+ get: (c) => c.qa.command ?? "not set",
84
+ set: (d, v) => updateConfig(d, { qa: { command: v || null } }),
85
+ },
86
+ {
87
+ label: "Release pipeline",
88
+ key: "release.pipeline",
89
+ type: "text",
90
+ get: (c) => c.release.pipeline ?? "not set",
91
+ set: (d, v) => updateConfig(d, { release: { pipeline: v || null } }),
92
+ },
93
+ ];
94
+ }
5
95
 
6
96
  export function registerConfigCommand(pi: ExtensionAPI): void {
7
97
  pi.registerCommand("supi:config", {
8
- description: "View and manage Supipowers configuration and profiles",
9
- async handler(args, ctx) {
10
- const config = loadConfig(ctx.cwd);
98
+ description: "View and manage Supipowers configuration",
99
+ async handler(_args, ctx) {
100
+ if (!ctx.hasUI) {
101
+ ctx.ui.notify("Config UI requires interactive mode", "warning");
102
+ return;
103
+ }
11
104
 
12
- if (!args || args.trim() === "") {
13
- const profiles = listProfiles(ctx.cwd);
14
- const activeProfile = resolveProfile(ctx.cwd, config);
105
+ const settings = buildSettings(ctx.cwd);
15
106
 
16
- const lines = [
17
- "# Supipowers Configuration",
18
- "",
19
- `Profile: ${config.defaultProfile}`,
20
- `Max parallel agents: ${config.orchestration.maxParallelAgents}`,
21
- `Max fix retries: ${config.orchestration.maxFixRetries}`,
22
- `Max nesting depth: ${config.orchestration.maxNestingDepth}`,
23
- `Model preference: ${config.orchestration.modelPreference}`,
24
- `LSP setup guide: ${config.lsp.setupGuide}`,
25
- `Notification verbosity: ${config.notifications.verbosity}`,
26
- `QA framework: ${config.qa.framework ?? "not detected"}`,
27
- `Release pipeline: ${config.release.pipeline ?? "not configured"}`,
28
- "",
29
- `Available profiles: ${profiles.join(", ")}`,
30
- "",
31
- "To update: /supi:config set <key> <value>",
32
- "Example: /supi:config set orchestration.maxParallelAgents 5",
33
- ];
107
+ // Main settings loop
108
+ while (true) {
109
+ const config = loadConfig(ctx.cwd);
34
110
 
35
- pi.sendMessage({
36
- customType: "supi-config",
37
- content: [{ type: "text", text: lines.join("\n") }],
38
- display: "inline",
39
- });
40
- return;
41
- }
111
+ const options = settings.map(
112
+ (s) => `${s.label}: ${s.get(config)}`
113
+ );
114
+ options.push("Done");
42
115
 
43
- const setMatch = args.match(/^set\s+(\S+)\s+(.+)$/);
44
- if (setMatch) {
45
- const [, keyPath, rawValue] = setMatch;
46
- const keys = keyPath.split(".");
47
- let value: unknown = rawValue;
116
+ const choice = await ctx.ui.select(
117
+ "Supipowers Settings",
118
+ options,
119
+ { helpText: "Select a setting to change · Esc to close" },
120
+ );
48
121
 
49
- if (rawValue === "true") value = true;
50
- else if (rawValue === "false") value = false;
51
- else if (rawValue === "null") value = null;
52
- else if (!isNaN(Number(rawValue))) value = Number(rawValue);
122
+ if (choice === undefined || choice === "Done") break;
53
123
 
54
- const update: Record<string, unknown> = {};
55
- let current = update;
56
- for (let i = 0; i < keys.length - 1; i++) {
57
- current[keys[i]] = {};
58
- current = current[keys[i]] as Record<string, unknown>;
59
- }
60
- current[keys[keys.length - 1]] = value;
124
+ const index = options.indexOf(choice);
125
+ const setting = settings[index];
126
+ if (!setting) break;
61
127
 
62
- updateConfig(ctx.cwd, update);
63
- notifySuccess(ctx, "Config updated", `${keyPath} = ${rawValue}`);
64
- return;
128
+ if (setting.type === "select" && setting.options) {
129
+ const value = await ctx.ui.select(
130
+ setting.label,
131
+ setting.options,
132
+ { initialIndex: setting.options.indexOf(setting.get(config)) },
133
+ );
134
+ if (value !== undefined) {
135
+ setting.set(ctx.cwd, value);
136
+ ctx.ui.notify(`${setting.label} → ${value}`, "info");
137
+ }
138
+ } else if (setting.type === "toggle") {
139
+ const current = setting.get(config);
140
+ const newValue = current === "on" ? "off" : "on";
141
+ setting.set(ctx.cwd, newValue);
142
+ ctx.ui.notify(`${setting.label} → ${newValue}`, "info");
143
+ } else if (setting.type === "text") {
144
+ const value = await ctx.ui.input(
145
+ setting.label,
146
+ setting.get(config) === "not set" ? undefined : setting.get(config),
147
+ );
148
+ if (value !== undefined) {
149
+ setting.set(ctx.cwd, value);
150
+ ctx.ui.notify(`${setting.label} → ${value || "cleared"}`, "info");
151
+ }
152
+ }
65
153
  }
66
-
67
- notifyInfo(ctx, "Usage", "/supi:config or /supi:config set <key> <value>");
68
154
  },
69
155
  });
70
156
  }
@@ -23,6 +23,20 @@ export function registerQaCommand(pi: ExtensionAPI): void {
23
23
 
24
24
  if (args?.includes("--changed")) {
25
25
  scope = "changed";
26
+ } else if (args?.includes("--e2e")) {
27
+ scope = "e2e";
28
+ } else if (ctx.hasUI && !args?.trim()) {
29
+ // No flag provided — let the user pick
30
+ const choice = await ctx.ui.select(
31
+ "QA scope",
32
+ ["all — Run all tests", "changed — Only changed files", "e2e — E2E / Playwright only"],
33
+ { helpText: "Select test scope · Esc to cancel" },
34
+ );
35
+ if (!choice) return;
36
+ scope = choice.split(" — ")[0] as "all" | "changed" | "e2e";
37
+ }
38
+
39
+ if (scope === "changed") {
26
40
  try {
27
41
  const result = await pi.exec("git", ["diff", "--name-only", "HEAD"], { cwd: ctx.cwd });
28
42
  if (result.exitCode === 0) {
@@ -31,8 +45,6 @@ export function registerQaCommand(pi: ExtensionAPI): void {
31
45
  } catch {
32
46
  scope = "all";
33
47
  }
34
- } else if (args?.includes("--e2e")) {
35
- scope = "e2e";
36
48
  }
37
49
 
38
50
  notifyInfo(ctx, "QA started", `${framework.name} | scope: ${scope}`);
@@ -18,32 +18,19 @@ export function registerReleaseCommand(pi: ExtensionAPI): void {
18
18
  }
19
19
 
20
20
  if (!config.release.pipeline) {
21
- const prompt = [
22
- "# Release Setup",
23
- "",
24
- "This is your first release with supipowers. How do you publish?",
25
- "",
26
- "1. **npm** — npm publish to registry",
27
- "2. **github** — GitHub Release with gh CLI",
28
- "3. **manual** — I'll handle publishing myself",
29
- "",
30
- "Tell me which option, and I'll save it for future releases.",
31
- "",
32
- "After you answer, I'll analyze commits and prepare the release.",
33
- ].join("\n");
34
-
35
- pi.sendMessage(
36
- {
37
- customType: "supi-release-setup",
38
- content: [{ type: "text", text: prompt }],
39
- display: "none",
40
- },
41
- { deliverAs: "steer" }
21
+ const choice = await ctx.ui.select(
22
+ "Release Setup — How do you publish?",
23
+ ["npm — npm publish to registry", "github — GitHub Release with gh CLI", "manual — I'll handle publishing myself"],
24
+ { helpText: "Select your release pipeline" },
42
25
  );
43
- return;
26
+
27
+ if (!choice) return;
28
+ const pipeline = choice.split(" — ")[0];
29
+ updateConfig(ctx.cwd, { release: { pipeline } });
30
+ ctx.ui.notify(`Release pipeline set to: ${pipeline}`, "info");
44
31
  }
45
32
 
46
- notifyInfo(ctx, "Release started", `Pipeline: ${config.release.pipeline}`);
33
+ notifyInfo(ctx, "Release started", `Pipeline: ${config.release.pipeline || "just configured"}`);
47
34
 
48
35
  const prompt = buildAnalyzerPrompt(lastTag);
49
36
 
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
2
2
  import { loadConfig } from "../config/loader.js";
3
- import { resolveProfile } from "../config/profiles.js";
3
+ import { listProfiles, resolveProfile } from "../config/profiles.js";
4
4
  import { buildReviewPrompt } from "../quality/gate-runner.js";
5
5
  import { isLspAvailable } from "../lsp/detector.js";
6
6
  import { notifyInfo, notifyWarning } from "../notifications/renderer.js";
@@ -20,6 +20,21 @@ export function registerReviewCommand(pi: ExtensionAPI): void {
20
20
  if (match) profileOverride = match[1];
21
21
  }
22
22
 
23
+ // If no flag provided and UI is available, let the user pick
24
+ if (!profileOverride && ctx.hasUI) {
25
+ const profiles = listProfiles(ctx.cwd);
26
+ const choice = await ctx.ui.select(
27
+ "Review profile",
28
+ profiles,
29
+ {
30
+ initialIndex: profiles.indexOf(config.defaultProfile),
31
+ helpText: "Select review depth · Esc to cancel",
32
+ },
33
+ );
34
+ if (!choice) return;
35
+ profileOverride = choice;
36
+ }
37
+
23
38
  const profile = resolveProfile(ctx.cwd, config, profileOverride);
24
39
  const lsp = isLspAvailable(pi.getActiveTools());
25
40
 
@@ -1,6 +1,5 @@
1
1
  import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
2
2
  import { findActiveRun, loadAllAgentResults } from "../storage/runs.js";
3
- import { notifyInfo } from "../notifications/renderer.js";
4
3
 
5
4
  export function registerStatusCommand(pi: ExtensionAPI): void {
6
5
  pi.registerCommand("supi:status", {
@@ -9,42 +8,35 @@ export function registerStatusCommand(pi: ExtensionAPI): void {
9
8
  const activeRun = findActiveRun(ctx.cwd);
10
9
 
11
10
  if (!activeRun) {
12
- notifyInfo(ctx, "No active runs", "Use /supi:run to execute a plan");
11
+ ctx.ui.notify("No active runs use /supi:run to execute a plan", "info");
13
12
  return;
14
13
  }
15
14
 
16
15
  const results = loadAllAgentResults(ctx.cwd, activeRun.id);
17
- const completedIds = new Set(results.map((r) => r.taskId));
18
16
  const totalTasks = activeRun.batches.reduce(
19
17
  (sum, b) => sum + b.taskIds.length,
20
18
  0
21
19
  );
22
- const completedCount = results.length;
23
20
  const doneCount = results.filter((r) => r.status === "done").length;
24
21
  const concernCount = results.filter((r) => r.status === "done_with_concerns").length;
25
22
  const blockedCount = results.filter((r) => r.status === "blocked").length;
26
-
27
23
  const currentBatch = activeRun.batches.find((b) => b.status !== "completed");
28
24
 
29
- const lines = [
30
- `# Run: ${activeRun.id}`,
31
- "",
25
+ const options = [
26
+ `Run: ${activeRun.id}`,
32
27
  `Status: ${activeRun.status}`,
33
28
  `Plan: ${activeRun.planRef}`,
34
29
  `Profile: ${activeRun.profile}`,
35
- `Progress: ${completedCount}/${totalTasks} tasks`,
36
- "",
30
+ `Progress: ${results.length}/${totalTasks} tasks`,
37
31
  ` Done: ${doneCount}`,
38
32
  ` With concerns: ${concernCount}`,
39
33
  ` Blocked: ${blockedCount}`,
40
- "",
41
- `Current batch: ${currentBatch ? `#${currentBatch.index} (${currentBatch.status})` : "none"}`,
34
+ `Batch: ${currentBatch ? `#${currentBatch.index} (${currentBatch.status})` : "all complete"}`,
35
+ "Close",
42
36
  ];
43
37
 
44
- pi.sendMessage({
45
- customType: "supi-status",
46
- content: [{ type: "text", text: lines.join("\n") }],
47
- display: "inline",
38
+ await ctx.ui.select("Supipowers Status", options, {
39
+ helpText: "Esc to close",
48
40
  });
49
41
  },
50
42
  });
@@ -13,30 +13,36 @@ export function registerSupiCommand(pi: ExtensionAPI): void {
13
13
  const latestReport = loadLatestReport(ctx.cwd);
14
14
  const plans = listPlans(ctx.cwd);
15
15
 
16
- const lines: string[] = [
17
- "# Supipowers",
18
- "",
19
- "## Commands",
20
- " /supi:plan Start collaborative planning",
21
- " /supi:run Execute a plan with sub-agents",
22
- " /supi:reviewRun quality gates",
23
- " /supi:qa Run QA pipeline",
24
- " /supi:release — Release automation",
25
- " /supi:config — Manage configuration",
26
- " /supi:status — Check running tasks",
27
- "",
28
- "## Project Status",
29
- ` Profile: ${config.defaultProfile}`,
30
- ` Plans: ${plans.length}`,
31
- ` Active run: ${activeRun ? activeRun.id : "none"}`,
32
- ` Last review: ${latestReport ? `${latestReport.timestamp.slice(0, 10)} (${latestReport.passed ? "passed" : "failed"})` : "none"}`,
16
+ const commands = [
17
+ "/supi:plan — Start collaborative planning",
18
+ "/supi:run — Execute a plan with sub-agents",
19
+ "/supi:review — Run quality gates",
20
+ "/supi:qa Run QA pipeline",
21
+ "/supi:release Release automation",
22
+ "/supi:configManage configuration",
23
+ "/supi:status Check running tasks",
33
24
  ];
34
25
 
35
- pi.sendMessage({
36
- customType: "supi-overview",
37
- content: [{ type: "text", text: lines.join("\n") }],
38
- display: "inline",
39
- });
26
+ const status = [
27
+ `Profile: ${config.defaultProfile}`,
28
+ `Plans: ${plans.length}`,
29
+ `Active run: ${activeRun ? activeRun.id : "none"}`,
30
+ `Last review: ${latestReport ? `${latestReport.timestamp.slice(0, 10)} (${latestReport.passed ? "passed" : "failed"})` : "none"}`,
31
+ ];
32
+
33
+ const choice = await ctx.ui.select(
34
+ "Supipowers",
35
+ [...commands, "", ...status, "", "Close"],
36
+ { helpText: "Select a command to run · Esc to close" },
37
+ );
38
+
39
+ if (choice && choice.startsWith("/supi:")) {
40
+ const cmdName = choice.split(" ")[0].slice(1); // remove leading /
41
+ const handler = pi.getCommands().find((c) => c.name === cmdName);
42
+ if (handler) {
43
+ await handler.handler("", ctx);
44
+ }
45
+ }
40
46
  },
41
47
  });
42
48
  }