guardian-framework 0.1.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/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +12958 -0
- package/dist/exports.js +12765 -0
- package/package.json +61 -0
- package/templates/languages/go-patterns.md +411 -0
- package/templates/languages/java-patterns.md +545 -0
- package/templates/languages/python-patterns.md +312 -0
- package/templates/languages/rust-patterns.md +232 -0
- package/templates/languages/typescript-patterns.md +313 -0
- package/templates/pi/INDEX.md +437 -0
- package/templates/pi/agent/AGENTS.md +166 -0
- package/templates/pi/agents/architecture-coordinator.md +40 -0
- package/templates/pi/agents/architecture-validator.md +36 -0
- package/templates/pi/agents/bootstrap-implementer.md +37 -0
- package/templates/pi/agents/issue-factory.md +31 -0
- package/templates/pi/agents/operations-validator.md +32 -0
- package/templates/pi/agents/security-validator.md +33 -0
- package/templates/pi/architecture/CHANGELOG.md +78 -0
- package/templates/pi/architecture/decisions/ADR-template.md +76 -0
- package/templates/pi/architecture/diagrams/system-overview.md +159 -0
- package/templates/pi/architecture/modules/module-template.md +152 -0
- package/templates/pi/context/checklists.md +91 -0
- package/templates/pi/context/domain-workflow.md +180 -0
- package/templates/pi/context/output-formats.md +135 -0
- package/templates/pi/context/patterns-base.md +47 -0
- package/templates/pi/context/patterns.md +38 -0
- package/templates/pi/context/project.md +88 -0
- package/templates/pi/domain/exploration.md +93 -0
- package/templates/pi/domain/ubiquitous-language.md +24 -0
- package/templates/pi/extensions/architect-lib/generators.ts +528 -0
- package/templates/pi/extensions/architect-lib/helpers.ts +344 -0
- package/templates/pi/extensions/architect-lib/types.ts +117 -0
- package/templates/pi/extensions/architect.ts +1425 -0
- package/templates/pi/extensions/ask-user-question.ts +579 -0
- package/templates/pi/extensions/bash-guard.ts +556 -0
- package/templates/pi/extensions/config-reload.ts +115 -0
- package/templates/pi/extensions/coordinator.ts +304 -0
- package/templates/pi/extensions/curator.ts +543 -0
- package/templates/pi/extensions/domain-explorer.ts +990 -0
- package/templates/pi/extensions/filechanges.ts +656 -0
- package/templates/pi/extensions/goal-loop.ts +717 -0
- package/templates/pi/extensions/hooks.ts +452 -0
- package/templates/pi/extensions/kanban.ts +547 -0
- package/templates/pi/extensions/pipeline.ts +1246 -0
- package/templates/pi/extensions/plan-mode.ts +209 -0
- package/templates/pi/extensions/project-scaffolder.ts +284 -0
- package/templates/pi/extensions/read-only-mode.ts +192 -0
- package/templates/pi/extensions/redaction.ts +128 -0
- package/templates/pi/extensions/session-persistence.ts +174 -0
- package/templates/pi/extensions/slash-commands.ts +229 -0
- package/templates/pi/extensions/snippets.ts +201 -0
- package/templates/pi/extensions/validation-runner.ts +100 -0
- package/templates/pi/github/agents/architecture-coordinator.agent.md +27 -0
- package/templates/pi/github/agents/architecture-validator.agent.md +20 -0
- package/templates/pi/github/agents/bootstrap-implementer.agent.md +20 -0
- package/templates/pi/github/agents/epic-planner.agent.md +70 -0
- package/templates/pi/github/agents/issue-factory.agent.md +20 -0
- package/templates/pi/github/agents/operations-validator.agent.md +20 -0
- package/templates/pi/github/agents/security-validator.agent.md +20 -0
- package/templates/pi/github/copilot/settings.json +16 -0
- package/templates/pi/github/copilot-instructions.md +155 -0
- package/templates/pi/github/instructions/architecture.instructions.md +71 -0
- package/templates/pi/github/instructions/validation.instructions.md +69 -0
- package/templates/pi/github/workflows/01-planning-workflow.md +75 -0
- package/templates/pi/github/workflows/02-issue-generation-workflow.md +69 -0
- package/templates/pi/github/workflows/03-implementation-workflow.md +80 -0
- package/templates/pi/github/workflows/04-validation-workflow.md +68 -0
- package/templates/pi/preflight_report.json +99 -0
- package/templates/pi/prompts/blueprint-update.md +262 -0
- package/templates/pi/prompts/blueprint-validate.md +218 -0
- package/templates/pi/prompts/bug-fix.md +50 -0
- package/templates/pi/prompts/ci-blueprint.md +131 -0
- package/templates/pi/prompts/context-refresh.md +258 -0
- package/templates/pi/prompts/epic-plan.md +356 -0
- package/templates/pi/prompts/epic-template.md +186 -0
- package/templates/pi/prompts/feature-development.md +131 -0
- package/templates/pi/prompts/git-issues.md +243 -0
- package/templates/pi/prompts/hotfix.md +71 -0
- package/templates/pi/prompts/issue-closeout.md +342 -0
- package/templates/pi/prompts/issue-draft.md +218 -0
- package/templates/pi/prompts/issue-implementation-series.md +173 -0
- package/templates/pi/prompts/issue-merge.md +372 -0
- package/templates/pi/prompts/issue-template-set.md +392 -0
- package/templates/pi/prompts/issue-template.md +207 -0
- package/templates/pi/prompts/pattern-extract.md +269 -0
- package/templates/pi/prompts/plan-to-issues.md +430 -0
- package/templates/pi/prompts/refactoring.md +82 -0
- package/templates/pi/prompts/scope-analyzer.md +254 -0
- package/templates/pi/prompts/sync-check.md +230 -0
- package/templates/pi/scripts/categorize-issues.sh +171 -0
- package/templates/pi/scripts/ci/check_architecture_conformance.sh +748 -0
- package/templates/pi/scripts/ci/check_planning_packet.py +228 -0
- package/templates/pi/scripts/ci/run_hardening_stages.sh +264 -0
- package/templates/pi/scripts/ci/run_preflight.sh +649 -0
- package/templates/pi/scripts/ci/run_stage.sh +193 -0
- package/templates/pi/scripts/ci/stage_docs_policy.sh +82 -0
- package/templates/pi/scripts/ci/stage_integration.sh +3 -0
- package/templates/pi/scripts/ci/stage_lint.sh +130 -0
- package/templates/pi/scripts/ci/stage_migration_verify.sh +3 -0
- package/templates/pi/scripts/ci/stage_package_build.sh +3 -0
- package/templates/pi/scripts/ci/stage_release_readiness.sh +3 -0
- package/templates/pi/scripts/ci/stage_remaining.sh +242 -0
- package/templates/pi/scripts/ci/stage_security.sh +3 -0
- package/templates/pi/scripts/ci/stage_static_analysis.sh +114 -0
- package/templates/pi/scripts/ci/stage_unit.sh +119 -0
- package/templates/pi/scripts/ci/validate_agent_output.py +181 -0
- package/templates/pi/scripts/ci/validate_agent_output.sh +243 -0
- package/templates/pi/scripts/create-feature-branch.sh +136 -0
- package/templates/pi/scripts/create-mr.sh +117 -0
- package/templates/pi/scripts/fetch-issues.sh +131 -0
- package/templates/pi/scripts/generate-architecture.sh +161 -0
- package/templates/pi/scripts/git/close-epic.sh +69 -0
- package/templates/pi/scripts/git/close-issue.sh +35 -0
- package/templates/pi/scripts/git/create-tracking-issue.sh +174 -0
- package/templates/pi/scripts/git/link-issue-to-epic.sh +51 -0
- package/templates/pi/scripts/git/update-tracking-issue.sh +66 -0
- package/templates/pi/scripts/languages/go/validate-architecture.sh +139 -0
- package/templates/pi/scripts/languages/go/validate-canonical.sh +129 -0
- package/templates/pi/scripts/languages/go/validate-ci.sh +132 -0
- package/templates/pi/scripts/languages/go/validate-integration.sh +156 -0
- package/templates/pi/scripts/languages/go/validate-operations.sh +146 -0
- package/templates/pi/scripts/languages/go/validate-security.sh +135 -0
- package/templates/pi/scripts/languages/go/validate-tests.sh +125 -0
- package/templates/pi/scripts/languages/java/validate-annotations.sh +185 -0
- package/templates/pi/scripts/languages/java/validate-architecture.sh +159 -0
- package/templates/pi/scripts/languages/java/validate-canonical.sh +114 -0
- package/templates/pi/scripts/languages/java/validate-ci.sh +269 -0
- package/templates/pi/scripts/languages/java/validate-integration.sh +149 -0
- package/templates/pi/scripts/languages/java/validate-operations.sh +142 -0
- package/templates/pi/scripts/languages/java/validate-security.sh +174 -0
- package/templates/pi/scripts/languages/java/validate-spring-architecture.sh +227 -0
- package/templates/pi/scripts/languages/java/validate-tests.sh +159 -0
- package/templates/pi/scripts/languages/python/validate-architecture.sh +99 -0
- package/templates/pi/scripts/languages/python/validate-canonical.sh +124 -0
- package/templates/pi/scripts/languages/python/validate-ci.sh +130 -0
- package/templates/pi/scripts/languages/python/validate-integration.sh +119 -0
- package/templates/pi/scripts/languages/python/validate-operations.sh +107 -0
- package/templates/pi/scripts/languages/python/validate-security.sh +68 -0
- package/templates/pi/scripts/languages/python/validate-tests.sh +102 -0
- package/templates/pi/scripts/languages/rust/validate-architecture.sh +188 -0
- package/templates/pi/scripts/languages/rust/validate-canonical.sh +146 -0
- package/templates/pi/scripts/languages/rust/validate-ci.sh +147 -0
- package/templates/pi/scripts/languages/rust/validate-integration.sh +149 -0
- package/templates/pi/scripts/languages/rust/validate-operations.sh +154 -0
- package/templates/pi/scripts/languages/rust/validate-security.sh +153 -0
- package/templates/pi/scripts/languages/rust/validate-tests.sh +150 -0
- package/templates/pi/scripts/languages/typescript/validate-architecture.sh +157 -0
- package/templates/pi/scripts/languages/typescript/validate-canonical.sh +135 -0
- package/templates/pi/scripts/languages/typescript/validate-ci.sh +244 -0
- package/templates/pi/scripts/languages/typescript/validate-integration.sh +166 -0
- package/templates/pi/scripts/languages/typescript/validate-operations.sh +162 -0
- package/templates/pi/scripts/languages/typescript/validate-security.sh +169 -0
- package/templates/pi/scripts/languages/typescript/validate-tests.sh +158 -0
- package/templates/pi/scripts/merge-mr.sh +95 -0
- package/templates/pi/scripts/mr-validation.sh +228 -0
- package/templates/pi/scripts/validate-architecture-readiness.sh +153 -0
- package/templates/pi/scripts/validate-architecture.sh +22 -0
- package/templates/pi/scripts/validate-canonical.sh +22 -0
- package/templates/pi/scripts/validate-ci.sh +37 -0
- package/templates/pi/scripts/validate-integration.sh +22 -0
- package/templates/pi/scripts/validate-operations.sh +22 -0
- package/templates/pi/scripts/validate-security.sh +22 -0
- package/templates/pi/scripts/validate-tests.sh +22 -0
- package/templates/pi/scripts/validate-ubiquitous-language.sh +291 -0
- package/templates/pi/scripts/validation-cache.sh +139 -0
- package/templates/pi/skills/agents/architecture-coordinator.md +178 -0
- package/templates/pi/skills/agents/architecture-generator.md +79 -0
- package/templates/pi/skills/agents/architecture-validator.md +74 -0
- package/templates/pi/skills/agents/ci-mr-validator.md +46 -0
- package/templates/pi/skills/agents/code-developer.md +76 -0
- package/templates/pi/skills/agents/commit.md +46 -0
- package/templates/pi/skills/agents/curator.md +71 -0
- package/templates/pi/skills/agents/debug.md +59 -0
- package/templates/pi/skills/agents/documentation-maintainer.md +36 -0
- package/templates/pi/skills/agents/goal-loop.md +125 -0
- package/templates/pi/skills/agents/hooks.md +122 -0
- package/templates/pi/skills/agents/integration-validator.md +35 -0
- package/templates/pi/skills/agents/issue-creator.md +75 -0
- package/templates/pi/skills/agents/issue-factory.md +143 -0
- package/templates/pi/skills/agents/kanban.md +85 -0
- package/templates/pi/skills/agents/land.md +59 -0
- package/templates/pi/skills/agents/operations-validator.md +43 -0
- package/templates/pi/skills/agents/pipeline.md +113 -0
- package/templates/pi/skills/agents/plan-mode.md +77 -0
- package/templates/pi/skills/agents/pull.md +40 -0
- package/templates/pi/skills/agents/push.md +41 -0
- package/templates/pi/skills/agents/security-validator.md +85 -0
- package/templates/pi/skills/agents/session-persistence.md +119 -0
- package/templates/pi/skills/agents/slash-commands.md +117 -0
- package/templates/pi/skills/agents/snippets.md +99 -0
- package/templates/pi/skills/agents/subagent-registry.md +78 -0
- package/templates/pi/skills/agents/test-validator.md +42 -0
- package/templates/pi/skills/validators/architecture-validator.md +43 -0
- package/templates/pi/skills/validators/ci-validator.md +27 -0
- package/templates/pi/skills/validators/context-compaction.md +73 -0
- package/templates/pi/skills/validators/integration-validator.md +38 -0
- package/templates/pi/skills/validators/model-registry.md +96 -0
- package/templates/pi/skills/validators/operations-validator.md +34 -0
- package/templates/pi/skills/validators/security-guards.md +72 -0
- package/templates/pi/skills/validators/security-validator.md +41 -0
- package/templates/pi/skills/validators/system-prompt-tiers.md +69 -0
- package/templates/pi/skills/validators/test-validator.md +33 -0
- package/templates/pi/types.ts +62 -0
- package/templates/pi/validators/README.md +93 -0
- package/templates/pi/validators/default.toml +209 -0
- package/templates/pi/validators/spring.toml +142 -0
- package/templates/pi/workpad.md +53 -0
- package/templates/project/java/Dockerfile +14 -0
- package/templates/project/java/README.md +40 -0
- package/templates/project/java/pom.xml +132 -0
- package/templates/project/typescript/Dockerfile +15 -0
- package/templates/project/typescript/README.md +29 -0
- package/templates/project/typescript/package.json +23 -0
- package/templates/project/typescript/tsconfig.json +21 -0
- package/templates/workflow.md +123 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
// biome-ignore lint/suspicious/noExplicitAny: pi ExtensionContext has no published types
|
|
2
|
+
// biome-ignore lint/style/noNonNullAssertion: pi UI callbacks use non-null assertions
|
|
3
|
+
/**
|
|
4
|
+
* File-Changes Extension for pi
|
|
5
|
+
*
|
|
6
|
+
* Tracks all files modified by pi during a session.
|
|
7
|
+
* Provides /filechanges to review diffs, /filechanges-accept to keep them,
|
|
8
|
+
* or /filechanges-decline to revert all pi-made changes.
|
|
9
|
+
*
|
|
10
|
+
* State persists across session branches via custom entries.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, relative, resolve } from "node:path";
|
|
15
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
import {
|
|
17
|
+
DynamicBorder,
|
|
18
|
+
getMarkdownTheme,
|
|
19
|
+
isEditToolResult,
|
|
20
|
+
isToolCallEventType,
|
|
21
|
+
isWriteToolResult,
|
|
22
|
+
} from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import type { SelectItem } from "@mariozechner/pi-tui";
|
|
24
|
+
import { Container, Key, Markdown, SelectList, Text, matchesKey } from "@mariozechner/pi-tui";
|
|
25
|
+
// Custom session entry types
|
|
26
|
+
const ENTRY_BASELINE = "filechanges:baseline";
|
|
27
|
+
const ENTRY_CLEAR = "filechanges:clear";
|
|
28
|
+
const ENTRY_UNTRACK = "filechanges:untrack";
|
|
29
|
+
|
|
30
|
+
type Baseline = {
|
|
31
|
+
path: string;
|
|
32
|
+
absPath: string;
|
|
33
|
+
originalContent: string | null; // null => file did not exist (created)
|
|
34
|
+
createdAt: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type TrackedFile = {
|
|
38
|
+
path: string;
|
|
39
|
+
absPath: string;
|
|
40
|
+
displayPath: string;
|
|
41
|
+
originalContent: string | null;
|
|
42
|
+
currentContent: string;
|
|
43
|
+
diff: string;
|
|
44
|
+
added: number;
|
|
45
|
+
removed: number;
|
|
46
|
+
kind: "new" | "edited";
|
|
47
|
+
updatedAt: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function stripAtPrefix(p: string): string {
|
|
51
|
+
return p.startsWith("@") ? p.slice(1) : p;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeToolPath(cwd: string, raw: string): { absPath: string; relPath: string } {
|
|
55
|
+
const cleaned = stripAtPrefix(raw);
|
|
56
|
+
const absPath = resolve(cwd, cleaned);
|
|
57
|
+
const rel = relative(cwd, absPath);
|
|
58
|
+
const relPath = rel && !rel.startsWith("..") && rel !== "" ? rel : cleaned;
|
|
59
|
+
return { absPath, relPath };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function readTextOrNull(absPath: string): Promise<string | null> {
|
|
63
|
+
try {
|
|
64
|
+
return await readFile(absPath, "utf-8");
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function countDiffLines(unifiedDiff: string): { added: number; removed: number } {
|
|
71
|
+
let added = 0;
|
|
72
|
+
let removed = 0;
|
|
73
|
+
for (const line of unifiedDiff.split("\n")) {
|
|
74
|
+
if (line.startsWith("+++ ") || line.startsWith("--- ") || line.startsWith("@@")) continue;
|
|
75
|
+
if (line.startsWith("+")) added++;
|
|
76
|
+
else if (line.startsWith("-")) removed++;
|
|
77
|
+
}
|
|
78
|
+
return { added, removed };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Simple unified diff generator — LCS-based. Zero external dependencies.
|
|
82
|
+
function createUnifiedPatch(
|
|
83
|
+
oldName: string,
|
|
84
|
+
newName: string,
|
|
85
|
+
oldContent: string,
|
|
86
|
+
newContent: string,
|
|
87
|
+
contextSize = 3,
|
|
88
|
+
): string {
|
|
89
|
+
const oldLines = oldContent.split("\n");
|
|
90
|
+
const newLines = newContent.split("\n");
|
|
91
|
+
const m = oldLines.length;
|
|
92
|
+
const n = newLines.length;
|
|
93
|
+
|
|
94
|
+
// Full LCS DP
|
|
95
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
96
|
+
for (let i = 1; i <= m; i++) {
|
|
97
|
+
for (let j = 1; j <= n; j++) {
|
|
98
|
+
if (oldLines[i - 1] === newLines[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
99
|
+
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Backtrack
|
|
104
|
+
const ops: Array<"equal" | "delete" | "insert"> = [];
|
|
105
|
+
let i = m;
|
|
106
|
+
let j = n;
|
|
107
|
+
while (i > 0 || j > 0) {
|
|
108
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
109
|
+
ops.push("equal");
|
|
110
|
+
i--;
|
|
111
|
+
j--;
|
|
112
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
113
|
+
ops.push("insert");
|
|
114
|
+
j--;
|
|
115
|
+
} else {
|
|
116
|
+
ops.push("delete");
|
|
117
|
+
i--;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
ops.reverse();
|
|
121
|
+
|
|
122
|
+
// Build detailed changes with line content
|
|
123
|
+
interface Change {
|
|
124
|
+
type: "equal" | "delete" | "insert";
|
|
125
|
+
oldLine?: string;
|
|
126
|
+
newLine?: string;
|
|
127
|
+
}
|
|
128
|
+
const detailed: Change[] = [];
|
|
129
|
+
let oi = 0;
|
|
130
|
+
let ni = 0;
|
|
131
|
+
for (const op of ops) {
|
|
132
|
+
if (op === "equal") {
|
|
133
|
+
detailed.push({ type: "equal", oldLine: oldLines[oi], newLine: newLines[ni] });
|
|
134
|
+
oi++;
|
|
135
|
+
ni++;
|
|
136
|
+
} else if (op === "delete") {
|
|
137
|
+
detailed.push({ type: "delete", oldLine: oldLines[oi] });
|
|
138
|
+
oi++;
|
|
139
|
+
} else {
|
|
140
|
+
detailed.push({ type: "insert", newLine: newLines[ni] });
|
|
141
|
+
ni++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Group into hunks with context
|
|
146
|
+
const hunks: Change[][] = [];
|
|
147
|
+
let currentHunk: Change[] = [];
|
|
148
|
+
let pendingContext: Change[] = [];
|
|
149
|
+
|
|
150
|
+
for (const c of detailed) {
|
|
151
|
+
if (c.type === "equal") {
|
|
152
|
+
pendingContext.push(c);
|
|
153
|
+
if (pendingContext.length > contextSize) {
|
|
154
|
+
if (currentHunk.length > 0) {
|
|
155
|
+
currentHunk.push(...pendingContext.slice(0, contextSize));
|
|
156
|
+
hunks.push(currentHunk);
|
|
157
|
+
currentHunk = [];
|
|
158
|
+
}
|
|
159
|
+
pendingContext = pendingContext.slice(contextSize);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
if (currentHunk.length === 0) {
|
|
163
|
+
currentHunk = [...pendingContext.slice(-contextSize)];
|
|
164
|
+
}
|
|
165
|
+
currentHunk.push(c);
|
|
166
|
+
pendingContext = [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (currentHunk.length > 0) {
|
|
170
|
+
currentHunk.push(...pendingContext.slice(0, contextSize));
|
|
171
|
+
hunks.push(currentHunk);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (hunks.length === 0) return "";
|
|
175
|
+
|
|
176
|
+
// Build output
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
lines.push(`--- ${oldName}`);
|
|
179
|
+
lines.push(`+++ ${newName}`);
|
|
180
|
+
|
|
181
|
+
let oldLineNum = 1;
|
|
182
|
+
let newLineNum = 1;
|
|
183
|
+
|
|
184
|
+
for (const hunk of hunks) {
|
|
185
|
+
let oldCount = 0;
|
|
186
|
+
let newCount = 0;
|
|
187
|
+
if (hunk.every((c) => c.type === "equal")) continue;
|
|
188
|
+
|
|
189
|
+
for (const c of hunk) {
|
|
190
|
+
if (c.type === "delete" || c.type === "equal") oldCount++;
|
|
191
|
+
if (c.type === "insert" || c.type === "equal") newCount++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const hunkOldStart = oldLineNum;
|
|
195
|
+
const hunkNewStart = newLineNum;
|
|
196
|
+
lines.push(`@@ -${hunkOldStart},${oldCount} +${hunkNewStart},${newCount} @@`);
|
|
197
|
+
|
|
198
|
+
for (const c of hunk) {
|
|
199
|
+
if (c.type === "equal") {
|
|
200
|
+
lines.push(` ${c.oldLine ?? ""}`);
|
|
201
|
+
oldLineNum++;
|
|
202
|
+
newLineNum++;
|
|
203
|
+
} else if (c.type === "delete") {
|
|
204
|
+
lines.push(`-${c.oldLine ?? ""}`);
|
|
205
|
+
oldLineNum++;
|
|
206
|
+
} else {
|
|
207
|
+
lines.push(`+${c.newLine ?? ""}`);
|
|
208
|
+
newLineNum++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return `${lines.join("\n")}\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function patchFromBaseline(displayPath: string, original: string | null, current: string): string {
|
|
217
|
+
return createUnifiedPatch(displayPath, displayPath, original ?? "", current);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function ensureParentDir(absPath: string): Promise<void> {
|
|
221
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default function (pi: ExtensionAPI) {
|
|
225
|
+
// In-memory state (reconstructed on session_start from custom entries)
|
|
226
|
+
const baselines = new Map<string, Baseline>();
|
|
227
|
+
const tracked = new Map<string, TrackedFile>();
|
|
228
|
+
|
|
229
|
+
// Per-tool-call snapshot, committed on successful tool_result
|
|
230
|
+
const pendingByToolCallId = new Map<
|
|
231
|
+
string,
|
|
232
|
+
{ path: string; absPath: string; before: string | null }
|
|
233
|
+
>();
|
|
234
|
+
|
|
235
|
+
function formatStatus(theme?: any): string | undefined {
|
|
236
|
+
if (tracked.size === 0) return undefined;
|
|
237
|
+
let edited = 0;
|
|
238
|
+
let created = 0;
|
|
239
|
+
for (const t of tracked.values()) {
|
|
240
|
+
if (t.kind === "new") created++;
|
|
241
|
+
else edited++;
|
|
242
|
+
}
|
|
243
|
+
if (!theme) return `Δ ${edited} + ${created}`;
|
|
244
|
+
return theme.fg("muted", `Δ ${edited} + ${created}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function updateUi(ctx: any) {
|
|
248
|
+
if (!ctx?.hasUI) return;
|
|
249
|
+
ctx.ui.setStatus("filechanges", formatStatus(ctx.ui.theme));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function recomputeTrackedFile(ctx: any, relPath: string) {
|
|
253
|
+
const baseline = baselines.get(relPath);
|
|
254
|
+
if (!baseline) return;
|
|
255
|
+
|
|
256
|
+
const current = await readTextOrNull(baseline.absPath);
|
|
257
|
+
const displayPath = baseline.path;
|
|
258
|
+
|
|
259
|
+
if (baseline.originalContent === null) {
|
|
260
|
+
// file was created
|
|
261
|
+
if (current === null) {
|
|
262
|
+
tracked.delete(relPath);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const diff = patchFromBaseline(displayPath, null, current);
|
|
266
|
+
const { added, removed } = countDiffLines(diff);
|
|
267
|
+
tracked.set(relPath, {
|
|
268
|
+
path: baseline.path,
|
|
269
|
+
absPath: baseline.absPath,
|
|
270
|
+
displayPath,
|
|
271
|
+
originalContent: null,
|
|
272
|
+
currentContent: current,
|
|
273
|
+
diff,
|
|
274
|
+
added,
|
|
275
|
+
removed,
|
|
276
|
+
kind: "new",
|
|
277
|
+
updatedAt: Date.now(),
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (current === null) {
|
|
283
|
+
const diff = patchFromBaseline(displayPath, baseline.originalContent, "");
|
|
284
|
+
const { added, removed } = countDiffLines(diff);
|
|
285
|
+
tracked.set(relPath, {
|
|
286
|
+
path: baseline.path,
|
|
287
|
+
absPath: baseline.absPath,
|
|
288
|
+
displayPath,
|
|
289
|
+
originalContent: baseline.originalContent,
|
|
290
|
+
currentContent: "",
|
|
291
|
+
diff,
|
|
292
|
+
added,
|
|
293
|
+
removed,
|
|
294
|
+
kind: "edited",
|
|
295
|
+
updatedAt: Date.now(),
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (current === baseline.originalContent) {
|
|
301
|
+
tracked.delete(relPath);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const diff = patchFromBaseline(displayPath, baseline.originalContent, current);
|
|
306
|
+
const { added, removed } = countDiffLines(diff);
|
|
307
|
+
tracked.set(relPath, {
|
|
308
|
+
path: baseline.path,
|
|
309
|
+
absPath: baseline.absPath,
|
|
310
|
+
displayPath,
|
|
311
|
+
originalContent: baseline.originalContent,
|
|
312
|
+
currentContent: current,
|
|
313
|
+
diff,
|
|
314
|
+
added,
|
|
315
|
+
removed,
|
|
316
|
+
kind: "edited",
|
|
317
|
+
updatedAt: Date.now(),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function clearLog(ctx: any, reason: "accept" | "decline") {
|
|
322
|
+
baselines.clear();
|
|
323
|
+
tracked.clear();
|
|
324
|
+
pendingByToolCallId.clear();
|
|
325
|
+
pi.appendEntry(ENTRY_CLEAR, { timestamp: Date.now(), reason });
|
|
326
|
+
updateUi(ctx);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function declineAll(ctx: any) {
|
|
330
|
+
await ctx.waitForIdle();
|
|
331
|
+
|
|
332
|
+
if (tracked.size === 0) {
|
|
333
|
+
if (ctx.hasUI) ctx.ui.notify("filechanges: nothing to decline.", "info");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const args: string[] = ctx.args ?? [];
|
|
338
|
+
const force = args.includes("force");
|
|
339
|
+
|
|
340
|
+
if (ctx.hasUI && !force) {
|
|
341
|
+
const ok = await ctx.ui.confirm(
|
|
342
|
+
"Decline pi changes?",
|
|
343
|
+
"This will revert ALL currently logged pi changes (overwrite files / delete created files).",
|
|
344
|
+
);
|
|
345
|
+
if (!ok) return;
|
|
346
|
+
} else if (!ctx.hasUI && !force) {
|
|
347
|
+
throw new Error("Decline requires confirmation. Run: /filechanges-decline force");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
351
|
+
let reverted = 0;
|
|
352
|
+
const errors: string[] = [];
|
|
353
|
+
|
|
354
|
+
for (const item of items) {
|
|
355
|
+
try {
|
|
356
|
+
if (item.originalContent === null) {
|
|
357
|
+
await rm(item.absPath, { force: true });
|
|
358
|
+
} else {
|
|
359
|
+
await ensureParentDir(item.absPath);
|
|
360
|
+
await writeFile(item.absPath, item.originalContent, "utf-8");
|
|
361
|
+
}
|
|
362
|
+
reverted++;
|
|
363
|
+
} catch (e: any) {
|
|
364
|
+
errors.push(`${item.displayPath}: ${e?.message ?? String(e)}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await clearLog(ctx, "decline");
|
|
369
|
+
|
|
370
|
+
if (ctx.hasUI) {
|
|
371
|
+
if (errors.length === 0) {
|
|
372
|
+
ctx.ui.notify(`filechanges: declined changes for ${reverted} file(s).`, "success");
|
|
373
|
+
} else {
|
|
374
|
+
ctx.ui.notify(
|
|
375
|
+
`filechanges: declined with ${errors.length} error(s). Run /filechanges to inspect.`,
|
|
376
|
+
"warning",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function acceptAll(ctx: any) {
|
|
383
|
+
await ctx.waitForIdle();
|
|
384
|
+
|
|
385
|
+
if (tracked.size === 0) {
|
|
386
|
+
if (ctx.hasUI) ctx.ui.notify("filechanges: nothing to accept.", "info");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const args: string[] = ctx.args ?? [];
|
|
391
|
+
const force = args.includes("force");
|
|
392
|
+
|
|
393
|
+
if (ctx.hasUI && !force) {
|
|
394
|
+
const ok = await ctx.ui.confirm(
|
|
395
|
+
"Accept pi changes?",
|
|
396
|
+
"This will keep current files as-is and clear the modification log.",
|
|
397
|
+
);
|
|
398
|
+
if (!ok) return;
|
|
399
|
+
} else if (!ctx.hasUI && !force) {
|
|
400
|
+
throw new Error("Accept requires confirmation. Run: /filechanges-accept force");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const count = tracked.size;
|
|
404
|
+
await clearLog(ctx, "accept");
|
|
405
|
+
if (ctx.hasUI) ctx.ui.notify(`filechanges: accepted changes for ${count} file(s).`, "success");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function rebuildFromSession(ctx: any): Promise<void> {
|
|
409
|
+
baselines.clear();
|
|
410
|
+
tracked.clear();
|
|
411
|
+
pendingByToolCallId.clear();
|
|
412
|
+
|
|
413
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
414
|
+
if (entry.type !== "custom") continue;
|
|
415
|
+
|
|
416
|
+
if (entry.customType === ENTRY_CLEAR) {
|
|
417
|
+
baselines.clear();
|
|
418
|
+
tracked.clear();
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (entry.customType === ENTRY_BASELINE) {
|
|
423
|
+
const data = entry.data as { path?: string; originalContent?: string | null };
|
|
424
|
+
if (!data?.path) continue;
|
|
425
|
+
const { absPath, relPath } = normalizeToolPath(ctx.cwd, data.path);
|
|
426
|
+
baselines.set(relPath, {
|
|
427
|
+
path: relPath,
|
|
428
|
+
absPath,
|
|
429
|
+
originalContent: typeof data.originalContent === "string" ? data.originalContent : null,
|
|
430
|
+
createdAt: Date.now(),
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (entry.customType === ENTRY_UNTRACK) {
|
|
436
|
+
const data = entry.data as { path?: string };
|
|
437
|
+
if (!data?.path) continue;
|
|
438
|
+
const { relPath } = normalizeToolPath(ctx.cwd, data.path);
|
|
439
|
+
baselines.delete(relPath);
|
|
440
|
+
tracked.delete(relPath);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
for (const relPath of baselines.keys()) {
|
|
445
|
+
await recomputeTrackedFile(ctx, relPath);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
updateUi(ctx);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── Commands ──
|
|
452
|
+
|
|
453
|
+
pi.registerCommand("filechanges", {
|
|
454
|
+
description: "Show files changed by pi and inspect diffs",
|
|
455
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
456
|
+
(ctx as any).args = _args ? _args.split(/\s+/g).filter(Boolean) : [];
|
|
457
|
+
await ctx.waitForIdle();
|
|
458
|
+
updateUi(ctx);
|
|
459
|
+
|
|
460
|
+
if (!ctx.hasUI) {
|
|
461
|
+
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
462
|
+
if (items.length === 0) {
|
|
463
|
+
console.log("filechanges: no pi-made modifications recorded.");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
for (const t of items.slice(0, 10)) {
|
|
467
|
+
console.log(
|
|
468
|
+
`${t.kind === "new" ? "+" : "Δ"} ${t.displayPath} (+${t.added}/-${t.removed})`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
while (true) {
|
|
475
|
+
await ctx.waitForIdle();
|
|
476
|
+
const items = [...tracked.values()].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
477
|
+
if (items.length === 0) {
|
|
478
|
+
ctx.ui.notify("filechanges: no pi-made modifications recorded.", "info");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const selectItems: SelectItem[] = [
|
|
483
|
+
{ value: "__accept__", label: "Accept changes", description: "Keep current files" },
|
|
484
|
+
{ value: "__decline__", label: "Undo changes", description: "Restore original contents" },
|
|
485
|
+
{ value: "__sep__", label: "────────", description: "" },
|
|
486
|
+
...items.map((t) => ({
|
|
487
|
+
value: t.path,
|
|
488
|
+
label: `${t.kind === "new" ? "+" : "Δ"} ${t.displayPath}`,
|
|
489
|
+
description: `+${t.added}/-${t.removed}`,
|
|
490
|
+
})),
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
const picked = await ctx.ui.custom<string | null>(
|
|
494
|
+
(tui, theme, _kb, done) => {
|
|
495
|
+
const container = new Container();
|
|
496
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
497
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("File changes")), 1, 0));
|
|
498
|
+
|
|
499
|
+
const list = new SelectList(selectItems, Math.min(14, selectItems.length), {
|
|
500
|
+
selectedPrefix: (t) => theme.fg("accent", t),
|
|
501
|
+
selectedText: (t) => theme.fg("accent", t),
|
|
502
|
+
description: (t) => theme.fg("muted", t),
|
|
503
|
+
scrollInfo: (t) => theme.fg("dim", t),
|
|
504
|
+
noMatch: (t) => theme.fg("warning", t),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
list.onSelect = (item) => {
|
|
508
|
+
if (item.value !== "__sep__") done(item.value);
|
|
509
|
+
};
|
|
510
|
+
list.onCancel = () => done(null);
|
|
511
|
+
container.addChild(list);
|
|
512
|
+
container.addChild(
|
|
513
|
+
new Text(theme.fg("dim", "↑↓ navigate • enter select • esc close"), 1, 0),
|
|
514
|
+
);
|
|
515
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
render: (w) => container.render(w),
|
|
519
|
+
invalidate: () => container.invalidate(),
|
|
520
|
+
handleInput: (data) => {
|
|
521
|
+
list.handleInput(data);
|
|
522
|
+
tui.requestRender();
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
{ overlay: true },
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (!picked) return;
|
|
530
|
+
if (picked === "__accept__") {
|
|
531
|
+
await acceptAll(ctx);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (picked === "__decline__") {
|
|
535
|
+
await declineAll(ctx);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const t = tracked.get(picked);
|
|
540
|
+
if (!t) {
|
|
541
|
+
ctx.ui.notify("filechanges: entry not found (maybe log was cleared).", "warning");
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const md = `\`\`\`diff\n${t.diff.trimEnd() || "(no diff)"}\n\`\`\``;
|
|
546
|
+
await ctx.ui.custom<void>(
|
|
547
|
+
(tui, theme, _kb, done) => {
|
|
548
|
+
const container = new Container();
|
|
549
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
550
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(t.displayPath)), 1, 0));
|
|
551
|
+
container.addChild(new Markdown(md, 1, 0, getMarkdownTheme()));
|
|
552
|
+
container.addChild(new Text(theme.fg("dim", "esc to go back"), 1, 0));
|
|
553
|
+
container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
render: (w) => container.render(w),
|
|
557
|
+
invalidate: () => container.invalidate(),
|
|
558
|
+
handleInput: (data) => {
|
|
559
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) done();
|
|
560
|
+
else tui.requestRender();
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
{ overlay: true },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
pi.registerCommand("filechanges-accept", {
|
|
571
|
+
description: "Accept pi-made changes (keeps files, clears log)",
|
|
572
|
+
handler: async (args, ctx: ExtensionCommandContext) => {
|
|
573
|
+
(ctx as any).args = args ? args.split(/\s+/g).filter(Boolean) : [];
|
|
574
|
+
await acceptAll(ctx);
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
pi.registerCommand("filechanges-decline", {
|
|
579
|
+
description: "Decline pi-made changes (reverts files, clears log)",
|
|
580
|
+
handler: async (args, ctx: ExtensionCommandContext) => {
|
|
581
|
+
(ctx as any).args = args ? args.split(/\s+/g).filter(Boolean) : [];
|
|
582
|
+
await declineAll(ctx);
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ── Session lifecycle ──
|
|
587
|
+
|
|
588
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
589
|
+
await rebuildFromSession(ctx);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
593
|
+
await rebuildFromSession(ctx);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
597
|
+
await rebuildFromSession(ctx);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// ── Capture before snapshots ──
|
|
601
|
+
|
|
602
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
603
|
+
if (isToolCallEventType("edit", event) || isToolCallEventType("write", event)) {
|
|
604
|
+
const { absPath, relPath } = normalizeToolPath(ctx.cwd, event.input.path);
|
|
605
|
+
const before = await readTextOrNull(absPath);
|
|
606
|
+
pendingByToolCallId.set(event.toolCallId, { path: relPath, absPath, before });
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// ── Commit on success ──
|
|
611
|
+
|
|
612
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
613
|
+
if (event.isError) {
|
|
614
|
+
pendingByToolCallId.delete(event.toolCallId);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (!isEditToolResult(event) && !isWriteToolResult(event)) return;
|
|
619
|
+
|
|
620
|
+
const pending = pendingByToolCallId.get(event.toolCallId);
|
|
621
|
+
pendingByToolCallId.delete(event.toolCallId);
|
|
622
|
+
if (!pending) return;
|
|
623
|
+
|
|
624
|
+
if (!baselines.has(pending.path)) {
|
|
625
|
+
baselines.set(pending.path, {
|
|
626
|
+
path: pending.path,
|
|
627
|
+
absPath: pending.absPath,
|
|
628
|
+
originalContent: pending.before,
|
|
629
|
+
createdAt: Date.now(),
|
|
630
|
+
});
|
|
631
|
+
pi.appendEntry(ENTRY_BASELINE, {
|
|
632
|
+
path: pending.path,
|
|
633
|
+
originalContent: pending.before,
|
|
634
|
+
timestamp: Date.now(),
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
await recomputeTrackedFile(ctx, pending.path);
|
|
639
|
+
|
|
640
|
+
// Untrack if back to baseline
|
|
641
|
+
const baseline = baselines.get(pending.path);
|
|
642
|
+
const current = await readTextOrNull(pending.absPath);
|
|
643
|
+
if (baseline) {
|
|
644
|
+
const back =
|
|
645
|
+
(baseline.originalContent !== null && current === baseline.originalContent) ||
|
|
646
|
+
(baseline.originalContent === null && current === null);
|
|
647
|
+
if (back) {
|
|
648
|
+
baselines.delete(pending.path);
|
|
649
|
+
tracked.delete(pending.path);
|
|
650
|
+
pi.appendEntry(ENTRY_UNTRACK, { path: pending.path, timestamp: Date.now() });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
updateUi(ctx);
|
|
655
|
+
});
|
|
656
|
+
}
|