@diegopetrucci/pi-extensions 0.1.74 → 0.1.76

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 (93) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +9 -4
  3. package/extensions/agent-workflow-audit/index.ts +1398 -1009
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/README.md +3 -1
  6. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  7. package/extensions/annotate-git-diff/git.ts +853 -767
  8. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  9. package/extensions/annotate-git-diff/index.ts +712 -671
  10. package/extensions/annotate-git-diff/package.json +1 -1
  11. package/extensions/annotate-git-diff/prompt.ts +45 -45
  12. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  13. package/extensions/annotate-git-diff/review-server.ts +337 -311
  14. package/extensions/annotate-git-diff/types.ts +132 -132
  15. package/extensions/annotate-git-diff/ui.ts +27 -27
  16. package/extensions/annotate-git-diff/watch.ts +74 -74
  17. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  18. package/extensions/annotate-git-diff/web/index.html +5 -6
  19. package/extensions/annotate-last-message/README.md +1 -1
  20. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  21. package/extensions/annotate-last-message/index.ts +210 -206
  22. package/extensions/annotate-last-message/package.json +1 -1
  23. package/extensions/annotate-last-message/prompt.ts +87 -66
  24. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  25. package/extensions/annotate-last-message/session.ts +94 -89
  26. package/extensions/annotate-last-message/theme.ts +199 -193
  27. package/extensions/annotate-last-message/types.ts +23 -23
  28. package/extensions/annotate-last-message/ui.ts +27 -25
  29. package/extensions/annotate-last-message/web/app.js +4 -7
  30. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  31. package/extensions/brrr/README.md +1 -1
  32. package/extensions/brrr/index.ts +213 -191
  33. package/extensions/brrr/package.json +1 -1
  34. package/extensions/claude-fast/README.md +5 -79
  35. package/extensions/claude-fast/index.ts +31 -289
  36. package/extensions/claude-fast/package.json +2 -3
  37. package/extensions/code-reviewer/README.md +1 -1
  38. package/extensions/code-reviewer/index.ts +1598 -1350
  39. package/extensions/code-reviewer/package.json +1 -1
  40. package/extensions/confirm-destructive/README.md +1 -1
  41. package/extensions/confirm-destructive/index.ts +55 -51
  42. package/extensions/confirm-destructive/package.json +1 -1
  43. package/extensions/context-cap/index.ts +312 -304
  44. package/extensions/context-cap/package.json +1 -1
  45. package/extensions/context-inspector/index.ts +1211 -1064
  46. package/extensions/context-inspector/package.json +1 -1
  47. package/extensions/contrarian/index.ts +1854 -1735
  48. package/extensions/contrarian/package.json +1 -1
  49. package/extensions/dirty-repo-guard/index.ts +41 -41
  50. package/extensions/dynamic-context-pruning/README.md +6 -38
  51. package/extensions/dynamic-context-pruning/index.ts +2124 -1980
  52. package/extensions/dynamic-context-pruning/package.json +1 -1
  53. package/extensions/fast/README.md +50 -12
  54. package/extensions/fast/index.ts +513 -317
  55. package/extensions/fast/package.json +1 -1
  56. package/extensions/git-footer/README.md +1 -1
  57. package/extensions/git-footer/index.ts +458 -411
  58. package/extensions/git-footer/package.json +2 -3
  59. package/extensions/gnosis/index.ts +180 -130
  60. package/extensions/gnosis/package.json +1 -1
  61. package/extensions/inline-bash/index.ts +88 -88
  62. package/extensions/inline-bash/package.json +1 -1
  63. package/extensions/librarian/index.ts +1669 -1419
  64. package/extensions/librarian/package.json +1 -1
  65. package/extensions/minimal-footer/index.ts +994 -1012
  66. package/extensions/minimal-footer/openai-usage.ts +195 -190
  67. package/extensions/notify/index.ts +222 -230
  68. package/extensions/notify/package.json +1 -1
  69. package/extensions/openai-fast/README.md +5 -73
  70. package/extensions/openai-fast/index.ts +32 -272
  71. package/extensions/openai-fast/package.json +2 -3
  72. package/extensions/oracle/index.ts +1731 -1625
  73. package/extensions/oracle/package.json +1 -1
  74. package/extensions/permission-gate/index.ts +658 -608
  75. package/extensions/permission-gate/package.json +1 -1
  76. package/extensions/permission-gate/powershell-safety.ts +840 -793
  77. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  78. package/extensions/project-mcp-json/README.md +199 -0
  79. package/extensions/project-mcp-json/config.ts +564 -0
  80. package/extensions/project-mcp-json/index.ts +119 -0
  81. package/extensions/project-mcp-json/package.json +38 -0
  82. package/extensions/quiet-tools/index.ts +282 -264
  83. package/extensions/review/README.md +2 -2
  84. package/extensions/review/index.ts +1745 -1872
  85. package/extensions/review/package.json +1 -1
  86. package/extensions/todo/index.ts +284 -265
  87. package/extensions/triage-comments/README.md +2 -2
  88. package/extensions/triage-comments/index.ts +2294 -1882
  89. package/extensions/triage-comments/package.json +1 -1
  90. package/package.json +6 -3
  91. package/assets/social-preview.png +0 -0
  92. package/extensions/claude-fast/claude-fast.example.json +0 -4
  93. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -7,202 +7,202 @@ export type ReviewFileKind = "text" | "binary" | "image";
7
7
  export type ReviewCommitKind = "commit" | "working-tree";
8
8
 
9
9
  export interface ReviewFileComparison {
10
- status: ChangeStatus;
11
- oldPath: string | null;
12
- newPath: string | null;
13
- displayPath: string;
14
- hasOriginal: boolean;
15
- hasModified: boolean;
10
+ status: ChangeStatus;
11
+ oldPath: string | null;
12
+ newPath: string | null;
13
+ displayPath: string;
14
+ hasOriginal: boolean;
15
+ hasModified: boolean;
16
16
  }
17
17
 
18
18
  export interface ReviewFile {
19
- id: string;
20
- path: string;
21
- worktreeStatus: ChangeStatus | null;
22
- hasWorkingTreeFile: boolean;
23
- /** True when the file is touched by the current branch diff (merge-base/base ref vs HEAD). */
24
- inGitDiff: boolean;
25
- /** Diff comparison to render for the file in the current scope.
26
- * - branch scope: merge-base/base ref vs HEAD
27
- * - commits scope: commit vs commit^ (populated when loaded via request-commit) */
28
- gitDiff: ReviewFileComparison | null;
29
- kind: ReviewFileKind;
30
- mimeType: string | null;
19
+ id: string;
20
+ path: string;
21
+ worktreeStatus: ChangeStatus | null;
22
+ hasWorkingTreeFile: boolean;
23
+ /** True when the file is touched by the current branch diff (merge-base/base ref vs HEAD). */
24
+ inGitDiff: boolean;
25
+ /** Diff comparison to render for the file in the current scope.
26
+ * - branch scope: merge-base/base ref vs HEAD
27
+ * - commits scope: commit vs commit^ (populated when loaded via request-commit) */
28
+ gitDiff: ReviewFileComparison | null;
29
+ kind: ReviewFileKind;
30
+ mimeType: string | null;
31
31
  }
32
32
 
33
33
  export interface ReviewCommitInfo {
34
- sha: string;
35
- shortSha: string;
36
- subject: string;
37
- authorName: string;
38
- authorDate: string;
39
- kind: ReviewCommitKind;
34
+ sha: string;
35
+ shortSha: string;
36
+ subject: string;
37
+ authorName: string;
38
+ authorDate: string;
39
+ kind: ReviewCommitKind;
40
40
  }
41
41
 
42
42
  export interface ReviewFileContents {
43
- originalContent: string;
44
- modifiedContent: string;
45
- kind: ReviewFileKind;
46
- mimeType: string | null;
47
- originalExists: boolean;
48
- modifiedExists: boolean;
49
- originalPreviewUrl: string | null;
50
- modifiedPreviewUrl: string | null;
43
+ originalContent: string;
44
+ modifiedContent: string;
45
+ kind: ReviewFileKind;
46
+ mimeType: string | null;
47
+ originalExists: boolean;
48
+ modifiedExists: boolean;
49
+ originalPreviewUrl: string | null;
50
+ modifiedPreviewUrl: string | null;
51
51
  }
52
52
 
53
53
  export type CommentSide = "original" | "modified" | "file";
54
54
 
55
55
  export interface DiffReviewComment {
56
- id: string;
57
- fileId: string;
58
- scope: ReviewScope;
59
- /** Commit SHA when scope === "commits". */
60
- commitSha?: string | null;
61
- /** Short commit identifier to render in the prompt (e.g. first 7 chars of SHA). */
62
- commitShort?: string | null;
63
- commitKind?: ReviewCommitKind | null;
64
- side: CommentSide;
65
- startLine: number | null;
66
- endLine: number | null;
67
- body: string;
56
+ id: string;
57
+ fileId: string;
58
+ scope: ReviewScope;
59
+ /** Commit SHA when scope === "commits". */
60
+ commitSha?: string | null;
61
+ /** Short commit identifier to render in the prompt (e.g. first 7 chars of SHA). */
62
+ commitShort?: string | null;
63
+ commitKind?: ReviewCommitKind | null;
64
+ side: CommentSide;
65
+ startLine: number | null;
66
+ endLine: number | null;
67
+ body: string;
68
68
  }
69
69
 
70
70
  export interface ReviewSubmitPayload {
71
- type: "submit";
72
- overallComment: string;
73
- comments: DiffReviewComment[];
74
- /**
75
- * True means this is a close-recovery draft and must stay editor-only.
76
- * Only literal false permits an explicit submission to trigger an agent turn;
77
- * malformed or missing values are normalized to true by the host.
78
- */
79
- draft: boolean;
71
+ type: "submit";
72
+ overallComment: string;
73
+ comments: DiffReviewComment[];
74
+ /**
75
+ * True means this is a close-recovery draft and must stay editor-only.
76
+ * Only literal false permits an explicit submission to trigger an agent turn;
77
+ * malformed or missing values are normalized to true by the host.
78
+ */
79
+ draft: boolean;
80
80
  }
81
81
 
82
82
  export interface ReviewCancelPayload {
83
- type: "cancel";
83
+ type: "cancel";
84
84
  }
85
85
 
86
86
  export interface ReviewRequestFilePayload {
87
- type: "request-file";
88
- requestId: string;
89
- fileId: string;
90
- scope: ReviewScope;
91
- commitSha?: string | null;
87
+ type: "request-file";
88
+ requestId: string;
89
+ fileId: string;
90
+ scope: ReviewScope;
91
+ commitSha?: string | null;
92
92
  }
93
93
 
94
94
  export interface ReviewRequestCommitPayload {
95
- type: "request-commit";
96
- requestId: string;
97
- sha: string;
95
+ type: "request-commit";
96
+ requestId: string;
97
+ sha: string;
98
98
  }
99
99
 
100
100
  export interface ReviewRequestReviewDataPayload {
101
- type: "request-review-data";
102
- requestId: string;
101
+ type: "request-review-data";
102
+ requestId: string;
103
103
  }
104
104
 
105
105
  export interface ReviewClipboardReadPayload {
106
- type: "clipboard-read";
107
- requestId: string;
106
+ type: "clipboard-read";
107
+ requestId: string;
108
108
  }
109
109
 
110
110
  export interface ReviewClipboardWritePayload {
111
- type: "clipboard-write";
112
- text: string;
111
+ type: "clipboard-write";
112
+ text: string;
113
113
  }
114
114
 
115
115
  export type ReviewWindowMessage =
116
- | ReviewSubmitPayload
117
- | ReviewCancelPayload
118
- | ReviewRequestFilePayload
119
- | ReviewRequestCommitPayload
120
- | ReviewRequestReviewDataPayload
121
- | ReviewClipboardReadPayload
122
- | ReviewClipboardWritePayload;
116
+ | ReviewSubmitPayload
117
+ | ReviewCancelPayload
118
+ | ReviewRequestFilePayload
119
+ | ReviewRequestCommitPayload
120
+ | ReviewRequestReviewDataPayload
121
+ | ReviewClipboardReadPayload
122
+ | ReviewClipboardWritePayload;
123
123
 
124
124
  export interface ReviewFileDataMessage {
125
- type: "file-data";
126
- requestId: string;
127
- fileId: string;
128
- scope: ReviewScope;
129
- commitSha?: string | null;
130
- originalContent: string;
131
- modifiedContent: string;
132
- kind: ReviewFileKind;
133
- mimeType: string | null;
134
- originalExists: boolean;
135
- modifiedExists: boolean;
136
- originalPreviewUrl: string | null;
137
- modifiedPreviewUrl: string | null;
125
+ type: "file-data";
126
+ requestId: string;
127
+ fileId: string;
128
+ scope: ReviewScope;
129
+ commitSha?: string | null;
130
+ originalContent: string;
131
+ modifiedContent: string;
132
+ kind: ReviewFileKind;
133
+ mimeType: string | null;
134
+ originalExists: boolean;
135
+ modifiedExists: boolean;
136
+ originalPreviewUrl: string | null;
137
+ modifiedPreviewUrl: string | null;
138
138
  }
139
139
 
140
140
  export interface ReviewFileErrorMessage {
141
- type: "file-error";
142
- requestId: string;
143
- fileId: string;
144
- scope: ReviewScope;
145
- commitSha?: string | null;
146
- message: string;
141
+ type: "file-error";
142
+ requestId: string;
143
+ fileId: string;
144
+ scope: ReviewScope;
145
+ commitSha?: string | null;
146
+ message: string;
147
147
  }
148
148
 
149
149
  export interface ReviewCommitDataMessage {
150
- type: "commit-data";
151
- requestId: string;
152
- sha: string;
153
- files: ReviewFile[];
150
+ type: "commit-data";
151
+ requestId: string;
152
+ sha: string;
153
+ files: ReviewFile[];
154
154
  }
155
155
 
156
156
  export interface ReviewCommitErrorMessage {
157
- type: "commit-error";
158
- requestId: string;
159
- sha: string;
160
- message: string;
157
+ type: "commit-error";
158
+ requestId: string;
159
+ sha: string;
160
+ message: string;
161
161
  }
162
162
 
163
163
  export interface ReviewReviewDataMessage {
164
- type: "review-data";
165
- requestId: string;
166
- files: ReviewFile[];
167
- commits: ReviewCommitInfo[];
168
- branchBaseRef: string | null;
169
- branchMergeBaseSha: string | null;
170
- repositoryHasHead: boolean;
164
+ type: "review-data";
165
+ requestId: string;
166
+ files: ReviewFile[];
167
+ commits: ReviewCommitInfo[];
168
+ branchBaseRef: string | null;
169
+ branchMergeBaseSha: string | null;
170
+ repositoryHasHead: boolean;
171
171
  }
172
172
 
173
173
  export interface ReviewReviewDataErrorMessage {
174
- type: "review-data-error";
175
- requestId: string;
176
- message: string;
174
+ type: "review-data-error";
175
+ requestId: string;
176
+ message: string;
177
177
  }
178
178
 
179
179
  export interface ReviewClipboardDataMessage {
180
- type: "clipboard-data";
181
- requestId: string;
182
- text: string;
183
- message?: string;
180
+ type: "clipboard-data";
181
+ requestId: string;
182
+ text: string;
183
+ message?: string;
184
184
  }
185
185
 
186
186
  export interface ReviewWorkingTreeChangedMessage {
187
- type: "working-tree-changed";
188
- changedAt: number;
187
+ type: "working-tree-changed";
188
+ changedAt: number;
189
189
  }
190
190
 
191
191
  export type ReviewHostMessage =
192
- | ReviewFileDataMessage
193
- | ReviewFileErrorMessage
194
- | ReviewCommitDataMessage
195
- | ReviewCommitErrorMessage
196
- | ReviewReviewDataMessage
197
- | ReviewReviewDataErrorMessage
198
- | ReviewClipboardDataMessage
199
- | ReviewWorkingTreeChangedMessage;
192
+ | ReviewFileDataMessage
193
+ | ReviewFileErrorMessage
194
+ | ReviewCommitDataMessage
195
+ | ReviewCommitErrorMessage
196
+ | ReviewReviewDataMessage
197
+ | ReviewReviewDataErrorMessage
198
+ | ReviewClipboardDataMessage
199
+ | ReviewWorkingTreeChangedMessage;
200
200
 
201
201
  export interface ReviewWindowData {
202
- repoRoot: string;
203
- files: ReviewFile[];
204
- commits: ReviewCommitInfo[];
205
- branchBaseRef: string | null;
206
- branchMergeBaseSha: string | null;
207
- repositoryHasHead: boolean;
202
+ repoRoot: string;
203
+ files: ReviewFile[];
204
+ commits: ReviewCommitInfo[];
205
+ branchBaseRef: string | null;
206
+ branchMergeBaseSha: string | null;
207
+ repositoryHasHead: boolean;
208
208
  }
@@ -7,41 +7,41 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  const webDir = join(__dirname, "web");
8
8
 
9
9
  function escapeForInlineScript(value: string): string {
10
- return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
10
+ return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
11
11
  }
12
12
 
13
13
  function escapeInlineScriptSource(value: string): string {
14
- return value.replace(/<\/(script)/gi, "<\\/$1");
14
+ return value.replace(/<\/(script)/gi, "<\\/$1");
15
15
  }
16
16
 
17
17
  export interface ReviewUiAssets {
18
- tailwindBrowserJs: string;
19
- monacoLoaderJs: string;
20
- monacoVsBaseUrl: string;
18
+ tailwindBrowserJs: string;
19
+ monacoLoaderJs: string;
20
+ monacoVsBaseUrl: string;
21
21
  }
22
22
 
23
23
  export function buildReviewHtml(data: ReviewWindowData, assets: ReviewUiAssets): string {
24
- const templateHtml = readFileSync(join(webDir, "index.html"), "utf8");
25
- const appJs = escapeInlineScriptSource(readFileSync(join(webDir, "app.js"), "utf8"));
26
- const payload = escapeForInlineScript(JSON.stringify(data));
27
- const assetConfig = escapeForInlineScript(
28
- JSON.stringify({
29
- monacoVsBaseUrl: assets.monacoVsBaseUrl,
30
- bootstrapError: null,
31
- }),
32
- );
33
- const replacements = new Map([
34
- ['"__INLINE_DATA__"', payload],
35
- ["__INLINE_ASSET_CONFIG__", assetConfig],
36
- ["__INLINE_TAILWIND_JS__", escapeInlineScriptSource(assets.tailwindBrowserJs)],
37
- ["__INLINE_MONACO_LOADER_JS__", escapeInlineScriptSource(assets.monacoLoaderJs)],
38
- ["__INLINE_JS__", appJs],
39
- ]);
24
+ const templateHtml = readFileSync(join(webDir, "index.html"), "utf8");
25
+ const appJs = escapeInlineScriptSource(readFileSync(join(webDir, "app.js"), "utf8"));
26
+ const payload = escapeForInlineScript(JSON.stringify(data));
27
+ const assetConfig = escapeForInlineScript(
28
+ JSON.stringify({
29
+ monacoVsBaseUrl: assets.monacoVsBaseUrl,
30
+ bootstrapError: null,
31
+ }),
32
+ );
33
+ const replacements = new Map([
34
+ ['"__INLINE_DATA__"', payload],
35
+ ["__INLINE_ASSET_CONFIG__", assetConfig],
36
+ ["__INLINE_TAILWIND_JS__", escapeInlineScriptSource(assets.tailwindBrowserJs)],
37
+ ["__INLINE_MONACO_LOADER_JS__", escapeInlineScriptSource(assets.monacoLoaderJs)],
38
+ ["__INLINE_JS__", appJs],
39
+ ]);
40
40
 
41
- // Replace the original template in one pass so marker-like repository data
42
- // or runtime source is never scanned as another template placeholder.
43
- return templateHtml.replace(
44
- /"__INLINE_DATA__"|__INLINE_ASSET_CONFIG__|__INLINE_TAILWIND_JS__|__INLINE_MONACO_LOADER_JS__|__INLINE_JS__/g,
45
- (marker) => replacements.get(marker) ?? marker,
46
- );
41
+ // Replace the original template in one pass so marker-like repository data
42
+ // or runtime source is never scanned as another template placeholder.
43
+ return templateHtml.replace(
44
+ /"__INLINE_DATA__"|__INLINE_ASSET_CONFIG__|__INLINE_TAILWIND_JS__|__INLINE_MONACO_LOADER_JS__|__INLINE_JS__/g,
45
+ (marker) => replacements.get(marker) ?? marker,
46
+ );
47
47
  }
@@ -1,104 +1,104 @@
1
1
  import { type FSWatcher, watch } from "node:fs";
2
2
 
3
3
  export interface RepoChangeWatcher {
4
- dispose(): void;
4
+ dispose(): void;
5
5
  }
6
6
 
7
7
  interface RepoChangeWatcherOptions {
8
- debounceMs?: number;
9
- onError?: (error: Error) => void;
8
+ debounceMs?: number;
9
+ onError?: (error: Error) => void;
10
10
  }
11
11
 
12
12
  const DEFAULT_DEBOUNCE_MS = 2000;
13
13
 
14
14
  const ignoredPathSegments = new Set([
15
- ".cache",
16
- ".git",
17
- ".hg",
18
- ".next",
19
- ".nuxt",
20
- ".svn",
21
- ".turbo",
22
- "build",
23
- "coverage",
24
- "dist",
25
- "node_modules",
26
- "out",
27
- "target",
28
- "tmp",
15
+ ".cache",
16
+ ".git",
17
+ ".hg",
18
+ ".next",
19
+ ".nuxt",
20
+ ".svn",
21
+ ".turbo",
22
+ "build",
23
+ "coverage",
24
+ "dist",
25
+ "node_modules",
26
+ "out",
27
+ "target",
28
+ "tmp",
29
29
  ]);
30
30
 
31
31
  const ignoredFileNames = new Set([".DS_Store"]);
32
32
 
33
33
  function normalizeWatchPath(path: string): string {
34
- return path.replace(/\\/g, "/").replace(/^\/+/, "");
34
+ return path.replace(/\\/g, "/").replace(/^\/+/, "");
35
35
  }
36
36
 
37
37
  export function isIgnoredWatchPath(path: string | Buffer | null | undefined): boolean {
38
- if (path == null) return false;
39
- const normalized = normalizeWatchPath(path.toString());
40
- if (normalized.length === 0) return false;
38
+ if (path == null) return false;
39
+ const normalized = normalizeWatchPath(path.toString());
40
+ if (normalized.length === 0) return false;
41
41
 
42
- const segments = normalized.split("/").filter((segment) => segment.length > 0);
43
- if (segments.some((segment) => ignoredPathSegments.has(segment))) return true;
42
+ const segments = normalized.split("/").filter((segment) => segment.length > 0);
43
+ if (segments.some((segment) => ignoredPathSegments.has(segment))) return true;
44
44
 
45
- const fileName = segments.at(-1) ?? normalized;
46
- if (ignoredFileNames.has(fileName)) return true;
47
- if (fileName.endsWith("~") || fileName.endsWith(".swp") || fileName.endsWith(".tmp")) return true;
45
+ const fileName = segments.at(-1) ?? normalized;
46
+ if (ignoredFileNames.has(fileName)) return true;
47
+ if (fileName.endsWith("~") || fileName.endsWith(".swp") || fileName.endsWith(".tmp")) return true;
48
48
 
49
- return false;
49
+ return false;
50
50
  }
51
51
 
52
52
  export function createRepoChangeWatcher(
53
- repoRoot: string,
54
- onChange: () => void,
55
- options: RepoChangeWatcherOptions = {},
53
+ repoRoot: string,
54
+ onChange: () => void,
55
+ options: RepoChangeWatcherOptions = {},
56
56
  ): RepoChangeWatcher {
57
- const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
58
- let disposed = false;
59
- let timer: ReturnType<typeof setTimeout> | null = null;
60
- let watcher: FSWatcher | null = null;
61
-
62
- const clearPending = (): void => {
63
- if (timer == null) return;
64
- clearTimeout(timer);
65
- timer = null;
66
- };
67
-
68
- const scheduleChange = (path: string | Buffer | null): void => {
69
- if (disposed || isIgnoredWatchPath(path)) return;
70
- clearPending();
71
- timer = setTimeout(() => {
72
- timer = null;
73
- if (!disposed) onChange();
74
- }, debounceMs);
75
- };
76
-
77
- try {
78
- watcher = watch(repoRoot, { recursive: true }, (_eventType, fileName) => {
79
- scheduleChange(fileName);
80
- });
81
- watcher.on("error", (error) => {
82
- if (!disposed) options.onError?.(error);
83
- });
84
- } catch (error) {
85
- options.onError?.(error instanceof Error ? error : new Error(String(error)));
86
- }
87
-
88
- return {
89
- dispose() {
90
- disposed = true;
91
- clearPending();
92
- try {
93
- watcher?.close();
94
- } catch {
95
- // Ignore watcher shutdown races.
96
- }
97
- watcher = null;
98
- },
99
- };
57
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
58
+ let disposed = false;
59
+ let timer: ReturnType<typeof setTimeout> | null = null;
60
+ let watcher: FSWatcher | null = null;
61
+
62
+ const clearPending = (): void => {
63
+ if (timer == null) return;
64
+ clearTimeout(timer);
65
+ timer = null;
66
+ };
67
+
68
+ const scheduleChange = (path: string | Buffer | null): void => {
69
+ if (disposed || isIgnoredWatchPath(path)) return;
70
+ clearPending();
71
+ timer = setTimeout(() => {
72
+ timer = null;
73
+ if (!disposed) onChange();
74
+ }, debounceMs);
75
+ };
76
+
77
+ try {
78
+ watcher = watch(repoRoot, { recursive: true }, (_eventType, fileName) => {
79
+ scheduleChange(fileName);
80
+ });
81
+ watcher.on("error", (error) => {
82
+ if (!disposed) options.onError?.(error);
83
+ });
84
+ } catch (error) {
85
+ options.onError?.(error instanceof Error ? error : new Error(String(error)));
86
+ }
87
+
88
+ return {
89
+ dispose() {
90
+ disposed = true;
91
+ clearPending();
92
+ try {
93
+ watcher?.close();
94
+ } catch {
95
+ // Ignore watcher shutdown races.
96
+ }
97
+ watcher = null;
98
+ },
99
+ };
100
100
  }
101
101
 
102
102
  export const __testing = {
103
- isIgnoredWatchPath,
103
+ isIgnoredWatchPath,
104
104
  };