@pi-unipi/workflow 2.6.1 → 2.9.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.
Files changed (3) hide show
  1. package/commands.ts +38 -122
  2. package/index.ts +1 -5
  3. package/package.json +11 -7
package/commands.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import { readFileSync, readdirSync, existsSync, statSync } from "fs";
10
10
  import { join, basename } from "path";
11
+ import { execFileSync } from "node:child_process";
11
12
  import { UNIPI_PREFIX, WORKFLOW_COMMANDS, type UnipiWorkflowEvent } from "@pi-unipi/core";
12
13
 
13
14
  type CompletionItem = { value: string; label: string; description: string };
@@ -36,111 +37,56 @@ interface WorkflowCommand {
36
37
  /**
37
38
  * Suggest spec files from .unipi/docs/specs/ for plan command.
38
39
  */
39
- function suggestSpecFiles(prefix: string): CompletionItem[] {
40
- const specsDir = join(process.cwd(), ".unipi", "docs", "specs");
41
- if (!existsSync(specsDir)) return [];
40
+ /** Suggest .md files from a docs subdirectory, sorted by mtime. */
41
+ function suggestFilesFrom(
42
+ subdir: string,
43
+ valuePrefix: string,
44
+ descLabel: string,
45
+ prefix: string,
46
+ ): CompletionItem[] {
47
+ const dir = join(process.cwd(), ".unipi", "docs", subdir);
48
+ if (!existsSync(dir)) return [];
42
49
 
43
50
  try {
44
51
  const search = prefix?.trim().split(/\s+/).pop() ?? "";
45
- const files = readdirSync(specsDir)
52
+ const files = readdirSync(dir)
46
53
  .filter((f) => f.endsWith(".md"))
47
- .map((f) => ({ name: f, time: statSync(join(specsDir, f)).mtimeMs }))
54
+ .map((f) => ({ name: f, time: statSync(join(dir, f)).mtimeMs }))
48
55
  .sort((a, b) => b.time - a.time);
49
56
  return files
50
57
  .filter((f) => !search || f.name.includes(search))
51
58
  .map((f) => ({
52
- value: `specs:${f.name}`,
59
+ value: `${valuePrefix}:${f.name}`,
53
60
  label: basename(f.name, ".md"),
54
- description: `Spec: ${f.name}`,
61
+ description: `${descLabel}: ${f.name}`,
55
62
  }));
56
63
  } catch {
57
64
  return [];
58
65
  }
59
66
  }
60
67
 
61
- /**
62
- * Suggest plan files from .unipi/docs/plans/ for work and review-work commands.
63
- */
64
- function suggestPlanFiles(prefix: string): CompletionItem[] {
65
- const plansDir = join(process.cwd(), ".unipi", "docs", "plans");
66
- if (!existsSync(plansDir)) return [];
68
+ function suggestSpecFiles(prefix: string): CompletionItem[] {
69
+ return suggestFilesFrom("specs", "specs", "Spec", prefix);
70
+ }
67
71
 
68
- try {
69
- const search = prefix?.trim().split(/\s+/).pop() ?? "";
70
- const files = readdirSync(plansDir)
71
- .filter((f) => f.endsWith(".md"))
72
- .map((f) => ({ name: f, time: statSync(join(plansDir, f)).mtimeMs }))
73
- .sort((a, b) => b.time - a.time);
74
- return files
75
- .filter((f) => !search || f.name.includes(search))
76
- .map((f) => ({
77
- value: `plan:${f.name}`,
78
- label: basename(f.name, ".md"),
79
- description: `Plan: ${f.name}`,
80
- }));
81
- } catch {
82
- return [];
83
- }
72
+ function suggestPlanFiles(prefix: string): CompletionItem[] {
73
+ return suggestFilesFrom("plans", "plan", "Plan", prefix);
84
74
  }
85
75
 
86
- /**
87
- * Suggest debug files from .unipi/docs/debug/ for fix command.
88
- */
89
76
  function suggestDebugFiles(prefix: string): CompletionItem[] {
90
- const debugDir = join(process.cwd(), ".unipi", "docs", "debug");
91
- if (!existsSync(debugDir)) return [];
92
-
93
- try {
94
- const search = prefix?.trim().split(/\s+/).pop() ?? "";
95
- const files = readdirSync(debugDir)
96
- .filter((f) => f.endsWith(".md"))
97
- .map((f) => ({ name: f, time: statSync(join(debugDir, f)).mtimeMs }))
98
- .sort((a, b) => b.time - a.time);
99
- return files
100
- .filter((f) => !search || f.name.includes(search))
101
- .map((f) => ({
102
- value: `debug:${f.name}`,
103
- label: basename(f.name, ".md"),
104
- description: `Debug: ${f.name}`,
105
- }));
106
- } catch {
107
- return [];
108
- }
77
+ return suggestFilesFrom("debug", "debug", "Debug", prefix);
109
78
  }
110
79
 
111
- /**
112
- * Suggest chore files from .unipi/docs/chore/ for chore-execute command.
113
- */
114
80
  function suggestChoreFiles(prefix: string): CompletionItem[] {
115
- const choreDir = join(process.cwd(), ".unipi", "docs", "chore");
116
- if (!existsSync(choreDir)) return [];
117
-
118
- try {
119
- const search = prefix?.trim().split(/\s+/).pop() ?? "";
120
- const files = readdirSync(choreDir)
121
- .filter((f) => f.endsWith(".md"))
122
- .map((f) => ({ name: f, time: statSync(join(choreDir, f)).mtimeMs }))
123
- .sort((a, b) => b.time - a.time);
124
- return files
125
- .filter((f) => !search || f.name.includes(search))
126
- .map((f) => ({
127
- value: `chore:${f.name}`,
128
- label: basename(f.name, ".md"),
129
- description: `Chore: ${f.name}`,
130
- }));
131
- } catch {
132
- return [];
133
- }
81
+ return suggestFilesFrom("chore", "chore", "Chore", prefix);
134
82
  }
135
83
 
136
- /** Cached per-cwd worktree suggestions. Worktree autocomplete can be invoked on every
137
- * keystroke, so the recursive scan is intentionally paid only once per session/cwd.
138
- */
84
+ /** Cached per-cwd worktree suggestions. */
139
85
  let worktreeSuggestionsCache: { cwd: string; items: CompletionItem[] } | null = null;
140
86
 
141
87
  /**
142
88
  * Suggest existing worktree names for merge/list commands.
143
- * Recursively scans for actual git worktrees (directories containing .git files).
89
+ * Uses `git worktree list --porcelain` for a fast, native scan.
144
90
  */
145
91
  function suggestWorktrees(): CompletionItem[] {
146
92
  const cwd = process.cwd();
@@ -148,49 +94,26 @@ function suggestWorktrees(): CompletionItem[] {
148
94
  return worktreeSuggestionsCache.items;
149
95
  }
150
96
 
151
- const worktreesDir = join(cwd, ".unipi", "worktrees");
152
- if (!existsSync(worktreesDir)) {
153
- worktreeSuggestionsCache = { cwd, items: [] };
154
- return worktreeSuggestionsCache.items;
155
- }
156
-
157
97
  try {
98
+ const output = execFileSync("git", ["worktree", "list", "--porcelain"], {
99
+ encoding: "utf-8",
100
+ cwd,
101
+ stdio: ["pipe", "pipe", "pipe"],
102
+ });
158
103
  const results: CompletionItem[] = [];
159
-
160
- /**
161
- * Recursively find worktree directories (those containing a .git file).
162
- * A worktree's .git file contains: gitdir: /path/to/.git/worktrees/<branch>
163
- */
164
- function findWorktrees(dir: string, relativePath: string): void {
165
- const entries = readdirSync(dir, { withFileTypes: true });
166
- for (const entry of entries) {
167
- const fullPath = join(dir, entry.name);
168
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
169
-
170
- if (entry.name === ".git" && entry.isFile()) {
171
- // This is a worktree — extract branch name from gitdir path
172
- try {
173
- const gitContent = readFileSync(fullPath, "utf-8").trim();
174
- const match = gitContent.match(/gitdir:\s+.+?\/([^/]+)$/);
175
- const branchName = match?.[1] ?? entry.name;
176
- results.push({
177
- value: branchName,
178
- label: branchName,
179
- description: `Worktree: ${relPath}`,
180
- });
181
- } catch {
182
- // Can't read .git file — skip
183
- }
184
- } else if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
185
- // Recurse into subdirectories (but not into nested .git or node_modules)
186
- findWorktrees(fullPath, relPath);
187
- }
104
+ for (const line of output.split("\n")) {
105
+ if (line.startsWith("worktree ")) {
106
+ const path = line.slice("worktree ".length);
107
+ const branch = basename(path);
108
+ results.push({
109
+ value: branch,
110
+ label: branch,
111
+ description: `Worktree: ${path}`,
112
+ });
188
113
  }
189
114
  }
190
-
191
- findWorktrees(worktreesDir, "");
192
115
  worktreeSuggestionsCache = { cwd, items: results };
193
- return worktreeSuggestionsCache.items;
116
+ return results;
194
117
  } catch {
195
118
  worktreeSuggestionsCache = { cwd, items: [] };
196
119
  return worktreeSuggestionsCache.items;
@@ -482,10 +405,3 @@ export function registerWorkflowCommands(
482
405
  },
483
406
  });
484
407
  }
485
-
486
- /**
487
- * Get list of all registered workflow command names.
488
- */
489
- export function getWorkflowCommandNames(): string[] {
490
- return COMMANDS.map((c) => `${UNIPI_PREFIX}${c.name}`);
491
- }
package/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  getBlockedToolsForLevel,
24
24
  getSandboxLevel,
25
25
  isToolAllowed,
26
+ isActiveSnapshot,
26
27
  type SandboxLevel,
27
28
  } from "@pi-unipi/core";
28
29
  import { registerWorkflowCommands } from "./commands.js";
@@ -130,11 +131,6 @@ function latestHistoricalSandboxSnapshot(
130
131
  return undefined;
131
132
  }
132
133
 
133
- function isActiveSnapshot(snapshot: EffectiveSandboxSnapshot): boolean {
134
- if (typeof snapshot.details?.active === "boolean") return snapshot.details.active;
135
- return typeof snapshot.content === "string" && snapshot.content.includes("Status: active");
136
- }
137
-
138
134
  export default function (pi: ExtensionAPI) {
139
135
  const workflowLifecycle = new WorkflowLifecycle();
140
136
  let ralphDetected = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/workflow",
3
- "version": "2.6.1",
3
+ "version": "2.9.0",
4
4
  "description": "Structured development workflow commands for Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -30,17 +30,21 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@pi-unipi/core": "2.6.1"
33
+ "@pi-unipi/core": "2.9.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@earendil-works/pi-ai": "^0.80.0",
37
- "@earendil-works/pi-coding-agent": "^0.80.0",
38
- "@earendil-works/pi-tui": "^0.80.0",
36
+ "@earendil-works/pi-ai": "^0.84.0",
37
+ "@earendil-works/pi-coding-agent": "^0.84.0",
38
+ "@earendil-works/pi-tui": "^0.84.0",
39
39
  "typebox": "^1.1.38"
40
40
  },
41
41
  "pi": {
42
- "extensions": [],
43
- "skills": [],
42
+ "extensions": [
43
+ "./index.ts"
44
+ ],
45
+ "skills": [
46
+ "./skills"
47
+ ],
44
48
  "prompts": [],
45
49
  "themes": []
46
50
  }