@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.
- package/commands.ts +38 -122
- package/index.ts +1 -5
- 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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
52
|
+
const files = readdirSync(dir)
|
|
46
53
|
.filter((f) => f.endsWith(".md"))
|
|
47
|
-
.map((f) => ({ name: f, time: statSync(join(
|
|
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:
|
|
59
|
+
value: `${valuePrefix}:${f.name}`,
|
|
53
60
|
label: basename(f.name, ".md"),
|
|
54
|
-
description:
|
|
61
|
+
description: `${descLabel}: ${f.name}`,
|
|
55
62
|
}));
|
|
56
63
|
} catch {
|
|
57
64
|
return [];
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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.
|
|
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.
|
|
33
|
+
"@pi-unipi/core": "2.9.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-ai": "^0.
|
|
37
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
38
|
-
"@earendil-works/pi-tui": "^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
|
-
|
|
42
|
+
"extensions": [
|
|
43
|
+
"./index.ts"
|
|
44
|
+
],
|
|
45
|
+
"skills": [
|
|
46
|
+
"./skills"
|
|
47
|
+
],
|
|
44
48
|
"prompts": [],
|
|
45
49
|
"themes": []
|
|
46
50
|
}
|