cursor-guard 4.9.6 → 4.9.9

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 (32) hide show
  1. package/README.md +124 -61
  2. package/README.zh-CN.md +121 -58
  3. package/ROADMAP.md +48 -20
  4. package/SKILL.md +1 -1
  5. package/docs/RELEASE.md +196 -0
  6. package/package.json +4 -2
  7. package/references/dashboard/public/app.js +79 -79
  8. package/references/dashboard/public/style.css +264 -159
  9. package/references/lib/core/core.test.js +139 -101
  10. package/references/lib/core/snapshot.js +8 -4
  11. package/references/mcp/server.js +73 -72
  12. package/references/vscode-extension/build-vsix.js +7 -5
  13. package/references/vscode-extension/dist/{cursor-guard-ide-4.9.6.vsix → cursor-guard-ide-4.9.9.vsix} +0 -0
  14. package/references/vscode-extension/dist/dashboard/public/app.js +79 -79
  15. package/references/vscode-extension/dist/dashboard/public/style.css +264 -159
  16. package/references/vscode-extension/dist/extension.js +704 -498
  17. package/references/vscode-extension/dist/guard-version.json +1 -1
  18. package/references/vscode-extension/dist/lib/core/snapshot.js +8 -4
  19. package/references/vscode-extension/dist/lib/dashboard-manager.js +70 -13
  20. package/references/vscode-extension/dist/lib/locale.js +36 -0
  21. package/references/vscode-extension/dist/lib/sidebar-webview.js +1484 -502
  22. package/references/vscode-extension/dist/mcp/server.js +11 -7
  23. package/references/vscode-extension/dist/media/brand-placeholder.png +0 -0
  24. package/references/vscode-extension/dist/package.json +1 -1
  25. package/references/vscode-extension/dist/skill/ROADMAP.md +48 -20
  26. package/references/vscode-extension/dist/skill/SKILL.md +1 -1
  27. package/references/vscode-extension/extension.js +704 -498
  28. package/references/vscode-extension/lib/dashboard-manager.js +70 -13
  29. package/references/vscode-extension/lib/locale.js +36 -0
  30. package/references/vscode-extension/lib/sidebar-webview.js +1484 -502
  31. package/references/vscode-extension/media/brand-placeholder.png +0 -0
  32. package/references/vscode-extension/package.json +1 -1
@@ -1 +1 @@
1
- {"version":"4.9.6"}
1
+ {"version":"4.9.9"}
@@ -91,6 +91,8 @@ function buildCommitMessage(ts, opts) {
91
91
  * @param {string} [opts.context.intent] - Why this snapshot was created (e.g. "refactoring auth middleware")
92
92
  * @param {string} [opts.context.agent] - AI model identifier (e.g. "claude-4-opus")
93
93
  * @param {string} [opts.context.session] - Conversation/session ID
94
+ * @param {boolean} [opts.allowEmptyTree] - If true, still create a commit when the snapshot tree equals the previous ref (empty / bookmark commit). Auto-backup should omit this; explicit manual snapshots should set it.
95
+ * @param {boolean} [opts.fullWorkspaceSnapshot] - If true, ignore `cfg.protect` when building the snapshot tree (still apply `ignore` / secrets). Use for IDE/MCP "snapshot everything" so edits outside protect patterns are not invisible to the snapshot.
94
96
  * @returns {{ status: 'created'|'skipped'|'error', commitHash?: string, shortHash?: string, fileCount?: number, reason?: string, error?: string, secretsExcluded?: string[] }}
95
97
  */
96
98
  function createGitSnapshot(projectDir, cfg, opts = {}) {
@@ -99,6 +101,8 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
99
101
  const gDir = getGitDir(projectDir);
100
102
  if (!gDir) return { status: 'error', error: 'not a git repository' };
101
103
 
104
+ const narrowProtect = cfg.protect.length > 0 && !opts.fullWorkspaceSnapshot;
105
+
102
106
  const guardIndex = path.join(gDir, 'cursor-guard-index');
103
107
  const guardIndexLock = guardIndex + '.lock';
104
108
  const env = { ...process.env, GIT_INDEX_FILE: guardIndex };
@@ -109,7 +113,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
109
113
  try {
110
114
  const parentHash = git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
111
115
 
112
- if (cfg.protect.length > 0) {
116
+ if (narrowProtect) {
113
117
  // protect uses strict matching (full path only, no basename fallback)
114
118
  // so *.js only matches root-level js files, not nested ones
115
119
  execFileSync('git', ['add', '-A'], { cwd, env, stdio: 'pipe' });
@@ -132,7 +136,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
132
136
  ? git(['rev-parse', `${branchRef}^{tree}`], { cwd, allowFail: true })
133
137
  : null;
134
138
 
135
- if (newTree === parentTree) {
139
+ if (newTree === parentTree && !opts.allowEmptyTree) {
136
140
  return { status: 'skipped', reason: 'tree unchanged' };
137
141
  }
138
142
 
@@ -156,7 +160,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
156
160
  : 'M';
157
161
  const fileName = filePart.split('\t').pop();
158
162
  if (matchesAny(cfg.ignore, fileName) || matchesAny(cfg.ignore, path.basename(fileName))) continue;
159
- if (cfg.protect.length > 0 && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
163
+ if (narrowProtect && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
160
164
  groups[key].push(fileName);
161
165
  }
162
166
  changedCount = Object.values(groups).reduce((sum, arr) => sum + arr.length, 0);
@@ -202,7 +206,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
202
206
  if (lsInitial) {
203
207
  const files = lsInitial.split('\n').filter(Boolean)
204
208
  .filter(f => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path.basename(f)))
205
- .filter(f => cfg.protect.length === 0 || matchesAny(cfg.protect, f, { strict: true }));
209
+ .filter(f => !narrowProtect || matchesAny(cfg.protect, f, { strict: true }));
206
210
  changedCount = files.length;
207
211
  const sample = files.slice(0, 5).join(', ');
208
212
 
@@ -7,11 +7,13 @@ const { spawn } = require('child_process');
7
7
  const { guardPath } = require('./paths');
8
8
 
9
9
  const CONFIG_FILE = '.cursor-guard.json';
10
+ const WATCHER_START_GRACE_MS = 8000;
10
11
 
11
12
  class DashboardManager {
12
13
  constructor() {
13
14
  this._instance = null;
14
15
  this._serverModule = null;
16
+ this._startingWatchers = new Map();
15
17
  }
16
18
 
17
19
  get running() { return !!this._instance; }
@@ -89,12 +91,51 @@ class DashboardManager {
89
91
  const { createGitSnapshot } = require(guardPath('lib', 'core', 'snapshot'));
90
92
  const { loadConfig } = require(guardPath('lib', 'utils'));
91
93
  const { cfg } = loadConfig(projectPath);
92
- return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
94
+ return createGitSnapshot(projectPath, cfg, {
95
+ branchRef: 'refs/guard/snapshot',
96
+ message: `guard: manual snapshot via IDE (${new Date().toISOString()})`,
97
+ context: { trigger: 'manual' },
98
+ allowEmptyTree: true,
99
+ // Match user expectation: "Snapshot now" captures the whole repo (except ignore/secrets), not only protect globs
100
+ fullWorkspaceSnapshot: true,
101
+ });
93
102
  } catch (e) {
94
103
  return { status: 'error', error: e.message };
95
104
  }
96
105
  }
97
106
 
107
+ _getWatcherLockPath(projectPath) {
108
+ try {
109
+ const { gitAvailable, isGitRepo, gitDir: getGitDir } = require(guardPath('lib', 'utils'));
110
+ const repo = gitAvailable() && isGitRepo(projectPath);
111
+ if (repo) {
112
+ const gDir = getGitDir(projectPath);
113
+ if (gDir) return path.join(gDir, 'cursor-guard.lock');
114
+ }
115
+ } catch { /* ignore */ }
116
+ return path.join(projectPath, '.cursor-guard-backup', 'cursor-guard.lock');
117
+ }
118
+
119
+ _getPendingWatcherPid(projectPath) {
120
+ const pending = this._startingWatchers.get(projectPath);
121
+ if (!pending) return null;
122
+ try {
123
+ process.kill(pending.pid, 0);
124
+ return pending.pid;
125
+ } catch {
126
+ this._startingWatchers.delete(projectPath);
127
+ return null;
128
+ }
129
+ }
130
+
131
+ _clearPendingWatcher(projectPath, pid) {
132
+ const pending = this._startingWatchers.get(projectPath);
133
+ if (!pending) return;
134
+ if (pid == null || pending.pid === pid) {
135
+ this._startingWatchers.delete(projectPath);
136
+ }
137
+ }
138
+
98
139
  startWatcher(projectPath) {
99
140
  if (!projectPath) return null;
100
141
  const existingPid = this.getWatcherPid(projectPath);
@@ -106,33 +147,49 @@ class DashboardManager {
106
147
  detached: true,
107
148
  env: { ...process.env, GUARD_SPAWNED_BY_EXT: '1' },
108
149
  });
150
+ this._startingWatchers.set(projectPath, { pid: child.pid, startedAt: Date.now() });
151
+ const clearPending = () => this._clearPendingWatcher(projectPath, child.pid);
152
+ child.once('exit', clearPending);
153
+ child.once('error', clearPending);
154
+ setTimeout(clearPending, WATCHER_START_GRACE_MS);
109
155
  child.unref();
110
156
  return child.pid;
111
157
  }
112
158
 
113
159
  stopWatcher(projectPath) {
114
160
  if (!projectPath) return false;
161
+ const pendingPid = this._getPendingWatcherPid(projectPath);
162
+ if (pendingPid) {
163
+ try { process.kill(pendingPid, 'SIGTERM'); } catch { /* ignore */ }
164
+ this._clearPendingWatcher(projectPath, pendingPid);
165
+ }
115
166
  try {
116
- const lockPath = path.join(projectPath, '.cursor-guard-backup.lock');
167
+ const lockPath = this._getWatcherLockPath(projectPath);
117
168
  if (!fs.existsSync(lockPath)) return false;
118
- const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
119
- if (lockData.pid) {
120
- process.kill(lockData.pid, 'SIGTERM');
121
- try { fs.unlinkSync(lockPath); } catch { /* ok */ }
122
- return true;
169
+ const content = fs.readFileSync(lockPath, 'utf-8');
170
+ const pidMatch = content.match(/pid=(\d+)/);
171
+ if (pidMatch) {
172
+ process.kill(parseInt(pidMatch[1], 10), 'SIGTERM');
123
173
  }
174
+ try { fs.unlinkSync(lockPath); } catch { /* ok */ }
175
+ return true;
124
176
  } catch { /* ok */ }
125
- return false;
177
+ return !!pendingPid;
126
178
  }
127
179
 
128
180
  getWatcherPid(projectPath) {
181
+ const pendingPid = this._getPendingWatcherPid(projectPath);
182
+ if (pendingPid) return pendingPid;
129
183
  try {
130
- const lockPath = path.join(projectPath, '.cursor-guard-backup.lock');
184
+ const lockPath = this._getWatcherLockPath(projectPath);
131
185
  if (!fs.existsSync(lockPath)) return null;
132
- const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
133
- if (lockData.pid) {
134
- process.kill(lockData.pid, 0);
135
- return lockData.pid;
186
+ const content = fs.readFileSync(lockPath, 'utf-8');
187
+ const pidMatch = content.match(/pid=(\d+)/);
188
+ if (pidMatch) {
189
+ const pid = parseInt(pidMatch[1], 10);
190
+ process.kill(pid, 0);
191
+ this._clearPendingWatcher(projectPath, pid);
192
+ return pid;
136
193
  }
137
194
  } catch { /* not running */ }
138
195
  return null;
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ const EXTENSION_LOCALE_KEY = 'cursorGuard.locale';
6
+
7
+ function normalizeLocale(locale) {
8
+ return locale === 'zh-CN' ? 'zh-CN' : 'en-US';
9
+ }
10
+
11
+ function detectLocale() {
12
+ return normalizeLocale(
13
+ (vscode.env.language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US'
14
+ );
15
+ }
16
+
17
+ function getLocale(storage) {
18
+ if (!storage || typeof storage.get !== 'function') return detectLocale();
19
+ return normalizeLocale(storage.get(EXTENSION_LOCALE_KEY) || detectLocale());
20
+ }
21
+
22
+ async function setLocale(storage, locale) {
23
+ const normalized = normalizeLocale(locale);
24
+ if (storage && typeof storage.update === 'function') {
25
+ await storage.update(EXTENSION_LOCALE_KEY, normalized);
26
+ }
27
+ return normalized;
28
+ }
29
+
30
+ module.exports = {
31
+ EXTENSION_LOCALE_KEY,
32
+ normalizeLocale,
33
+ detectLocale,
34
+ getLocale,
35
+ setLocale,
36
+ };