@vaultcompass/vault-guard 1.1.1 → 1.2.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/dist/cli-entry.js CHANGED
@@ -9,11 +9,8 @@ if (nodeMajor < 22) {
9
9
  }
10
10
  const program = (0, cli_1.buildCli)();
11
11
  // Parse arguments and execute command
12
- program.parseAsync().then(() => {
13
- // Successful completion
14
- process.exit(0);
15
- }).catch((error) => {
12
+ program.parseAsync().catch((error) => {
16
13
  // Handle errors
17
14
  console.error(error);
18
- process.exit(1);
15
+ process.exitCode = 1;
19
16
  });
package/dist/cli.js CHANGED
@@ -47,11 +47,17 @@ const suggest_model_1 = require("./commands/suggest-model");
47
47
  const proxy_1 = require("./commands/proxy");
48
48
  const data_1 = require("./commands/data");
49
49
  const config_1 = require("./commands/config");
50
+ const init_1 = require("./commands/init");
50
51
  function readCliVersion() {
51
52
  const pkgPath = path.join(__dirname, '..', 'package.json');
52
53
  const raw = fs.readFileSync(pkgPath, 'utf-8');
53
54
  return JSON.parse(raw).version;
54
55
  }
56
+ function setExitCode(exitCode) {
57
+ if (exitCode !== 0) {
58
+ process.exitCode = exitCode;
59
+ }
60
+ }
55
61
  function buildCli() {
56
62
  const program = new commander_1.Command();
57
63
  program
@@ -64,8 +70,7 @@ function buildCli() {
64
70
  .description('Validate the nearest .vault-guard.json (structure + scanner load)')
65
71
  .action(async () => {
66
72
  const exitCode = await (0, config_1.configValidateCommand)(process.cwd());
67
- if (exitCode !== 0)
68
- process.exit(exitCode);
73
+ setExitCode(exitCode);
69
74
  });
70
75
  // Scan command
71
76
  program
@@ -77,9 +82,37 @@ function buildCli() {
77
82
  .action(async (path, options) => {
78
83
  const format = options.format ?? 'text';
79
84
  const exitCode = await (0, scan_1.scanCommand)(path, format, Boolean(options.staged));
80
- if (exitCode !== 0) {
81
- process.exit(exitCode);
85
+ setExitCode(exitCode);
86
+ });
87
+ program
88
+ .command('init')
89
+ .description('Initialize Vault Guard in this repository (config, CI workflow, agent rules, hook)')
90
+ .option('--dry-run', 'Print the file manifest and planned actions without writing', false)
91
+ .option('--revert', 'Remove files recorded in .vault-guard/manifest.json', false)
92
+ .option('--json', 'Machine-readable output', false)
93
+ .option('-m, --manager <manager>', 'Hook integration when installing pre-commit: native | husky | lefthook | precommit', 'native')
94
+ .option('--skip-hook', 'Do not install a pre-commit hook', false)
95
+ .option('--skip-workflow', 'Do not create .github/workflows/vault-guard.yml', false)
96
+ .option('--skip-config', 'Do not create .vault-guard.json', false)
97
+ .option('--skip-agent-rules', 'Do not create .vault-guard/mcp-snippet.json or agent-rules.md', false)
98
+ .action(async (options) => {
99
+ const m = (options.manager ?? 'native').toLowerCase();
100
+ if (!['native', 'husky', 'lefthook', 'precommit'].includes(m)) {
101
+ console.error(`Unknown manager: ${options.manager}`);
102
+ process.exitCode = 1;
103
+ return;
82
104
  }
105
+ const exitCode = await (0, init_1.initCommand)({
106
+ dryRun: Boolean(options.dryRun),
107
+ revert: Boolean(options.revert),
108
+ json: Boolean(options.json),
109
+ manager: m,
110
+ skipHook: Boolean(options.skipHook),
111
+ skipWorkflow: Boolean(options.skipWorkflow),
112
+ skipConfig: Boolean(options.skipConfig),
113
+ skipAgentRules: Boolean(options.skipAgentRules),
114
+ });
115
+ setExitCode(exitCode);
83
116
  });
84
117
  // Install-hook command
85
118
  program
@@ -90,7 +123,8 @@ function buildCli() {
90
123
  const m = (options.manager ?? 'native').toLowerCase();
91
124
  if (!['native', 'husky', 'lefthook', 'precommit'].includes(m)) {
92
125
  console.error(`Unknown manager: ${options.manager}`);
93
- process.exit(1);
126
+ process.exitCode = 1;
127
+ return;
94
128
  }
95
129
  await (0, install_hook_1.installHookCommand)(m);
96
130
  });
@@ -108,9 +142,7 @@ function buildCli() {
108
142
  .argument('[files...]', 'Files to check')
109
143
  .action(async (files) => {
110
144
  const exitCode = await (0, fix_1.fixCommand)(files);
111
- if (exitCode !== 0) {
112
- process.exit(exitCode);
113
- }
145
+ setExitCode(exitCode);
114
146
  });
115
147
  // Check command
116
148
  program
@@ -119,9 +151,7 @@ function buildCli() {
119
151
  .argument('[files...]', 'Files to check')
120
152
  .action(async (files) => {
121
153
  const exitCode = await (0, check_1.checkCommand)(files);
122
- if (exitCode !== 0) {
123
- process.exit(exitCode);
124
- }
154
+ setExitCode(exitCode);
125
155
  });
126
156
  program
127
157
  .command('statusline')
@@ -162,7 +192,8 @@ function buildCli() {
162
192
  const n = Number(options.maxRpm);
163
193
  if (!Number.isFinite(n) || n < 1) {
164
194
  console.error('--max-rpm must be a positive number');
165
- process.exit(1);
195
+ process.exitCode = 1;
196
+ return;
166
197
  }
167
198
  maxRpm = Math.floor(n);
168
199
  }
@@ -192,7 +223,7 @@ function buildCli() {
192
223
  }
193
224
  catch (e) {
194
225
  console.error(String(e));
195
- process.exit(1);
226
+ process.exitCode = 1;
196
227
  }
197
228
  });
198
229
  // `data` parent command — inspects, exports, and resets the local
@@ -207,8 +238,7 @@ function buildCli() {
207
238
  .option('--json', 'Print JSON', false)
208
239
  .action(async (options) => {
209
240
  const exitCode = await (0, data_1.dataStatusCommand)({ json: Boolean(options.json) });
210
- if (exitCode !== 0)
211
- process.exit(exitCode);
241
+ setExitCode(exitCode);
212
242
  });
213
243
  dataCmd
214
244
  .command('reset')
@@ -222,8 +252,7 @@ function buildCli() {
222
252
  dryRun: Boolean(options.dryRun),
223
253
  json: Boolean(options.json),
224
254
  });
225
- if (exitCode !== 0)
226
- process.exit(exitCode);
255
+ setExitCode(exitCode);
227
256
  });
228
257
  dataCmd
229
258
  .command('export')
@@ -233,8 +262,7 @@ function buildCli() {
233
262
  .action(async (options) => {
234
263
  const fmt = options.format === 'jsonl' ? 'jsonl' : 'json';
235
264
  const exitCode = await (0, data_1.dataExportCommand)({ output: options.output, format: fmt });
236
- if (exitCode !== 0)
237
- process.exit(exitCode);
265
+ setExitCode(exitCode);
238
266
  });
239
267
  return program;
240
268
  }
@@ -1,6 +1,7 @@
1
1
  export { scanCommand } from './scan';
2
2
  export { configValidateCommand } from './config';
3
3
  export { installHookCommand } from './install-hook';
4
+ export { initCommand, type InitOptions } from './init';
4
5
  export { tokensCommand } from './tokens';
5
6
  export { fixCommand } from './fix';
6
7
  export { checkCommand } from './check';
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.dataExportCommand = exports.dataResetCommand = exports.dataStatusCommand = exports.proxyCommand = exports.suggestModelCommand = exports.statuslineCommand = exports.checkCommand = exports.fixCommand = exports.tokensCommand = exports.installHookCommand = exports.configValidateCommand = exports.scanCommand = void 0;
3
+ exports.dataExportCommand = exports.dataResetCommand = exports.dataStatusCommand = exports.proxyCommand = exports.suggestModelCommand = exports.statuslineCommand = exports.checkCommand = exports.fixCommand = exports.tokensCommand = exports.initCommand = exports.installHookCommand = exports.configValidateCommand = exports.scanCommand = void 0;
4
4
  var scan_1 = require("./scan");
5
5
  Object.defineProperty(exports, "scanCommand", { enumerable: true, get: function () { return scan_1.scanCommand; } });
6
6
  var config_1 = require("./config");
7
7
  Object.defineProperty(exports, "configValidateCommand", { enumerable: true, get: function () { return config_1.configValidateCommand; } });
8
8
  var install_hook_1 = require("./install-hook");
9
9
  Object.defineProperty(exports, "installHookCommand", { enumerable: true, get: function () { return install_hook_1.installHookCommand; } });
10
+ var init_1 = require("./init");
11
+ Object.defineProperty(exports, "initCommand", { enumerable: true, get: function () { return init_1.initCommand; } });
10
12
  var tokens_1 = require("./tokens");
11
13
  Object.defineProperty(exports, "tokensCommand", { enumerable: true, get: function () { return tokens_1.tokensCommand; } });
12
14
  var fix_1 = require("./fix");
@@ -0,0 +1,40 @@
1
+ import { type HookManager } from '@vaultcompass/vault-guard-core';
2
+ export interface InitOptions {
3
+ cwd?: string;
4
+ dryRun?: boolean;
5
+ revert?: boolean;
6
+ json?: boolean;
7
+ manager?: HookManager;
8
+ skipHook?: boolean;
9
+ skipWorkflow?: boolean;
10
+ skipConfig?: boolean;
11
+ skipAgentRules?: boolean;
12
+ }
13
+ export interface InitConflict {
14
+ path: string;
15
+ reason: 'exists' | 'foreign_manifest' | 'manifest_mismatch' | 'not_a_git_repository' | 'foreign_hook';
16
+ }
17
+ export interface InitPlannedAction {
18
+ kind: 'create' | 'hook-install' | 'skip';
19
+ path: string;
20
+ detail?: string;
21
+ }
22
+ export interface InitResult {
23
+ ok: boolean;
24
+ dryRun: boolean;
25
+ reverted: boolean;
26
+ alreadyInitialized: boolean;
27
+ actions: InitPlannedAction[];
28
+ conflicts: InitConflict[];
29
+ hook?: {
30
+ manager: string;
31
+ path?: string;
32
+ installed: boolean;
33
+ };
34
+ manifestPath: string;
35
+ mcpMergeHint: string;
36
+ }
37
+ export declare function planInit(options?: InitOptions): InitResult;
38
+ export declare function applyInit(plan: InitResult, options?: InitOptions): InitResult;
39
+ export declare function revertInit(options?: InitOptions): InitResult;
40
+ export declare function initCommand(options?: InitOptions): Promise<number>;
@@ -0,0 +1,488 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.planInit = planInit;
40
+ exports.applyInit = applyInit;
41
+ exports.revertInit = revertInit;
42
+ exports.initCommand = initCommand;
43
+ const fs = __importStar(require("fs"));
44
+ const path = __importStar(require("path"));
45
+ const vault_guard_core_1 = require("@vaultcompass/vault-guard-core");
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ const templates_1 = require("../init/templates");
48
+ const MCP_MERGE_HINT = 'Merge .vault-guard/mcp-snippet.json into your editor MCP config (~/.cursor/mcp.json or Claude Desktop).';
49
+ function isGitRepo(cwd) {
50
+ return fs.existsSync(path.join(cwd, '.git'));
51
+ }
52
+ function readFileIfExists(filePath) {
53
+ try {
54
+ return fs.readFileSync(filePath, 'utf8');
55
+ }
56
+ catch {
57
+ return undefined;
58
+ }
59
+ }
60
+ function parseManifest(content) {
61
+ try {
62
+ const parsed = JSON.parse(content);
63
+ if (parsed.initVersion !== '1' || !Array.isArray(parsed.files)) {
64
+ return undefined;
65
+ }
66
+ return parsed;
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ function managedContentPaths(options) {
73
+ const skip = new Set();
74
+ if (options.skipConfig)
75
+ skip.add('.vault-guard.json');
76
+ if (options.skipWorkflow)
77
+ skip.add('.github/workflows/vault-guard.yml');
78
+ if (options.skipAgentRules) {
79
+ skip.add('.vault-guard/mcp-snippet.json');
80
+ skip.add('.vault-guard/agent-rules.md');
81
+ }
82
+ return templates_1.MANAGED_FILE_PATHS.filter((p) => p !== templates_1.MANIFEST_RELATIVE_PATH && !skip.has(p));
83
+ }
84
+ function hookRelativePath(cwd, hookPath) {
85
+ return path.relative(cwd, hookPath).split(path.sep).join('/');
86
+ }
87
+ function foreignHookConflict(cwd, manager) {
88
+ const hook = new vault_guard_core_1.PreCommitHook();
89
+ const hookPath = hook.getPreCommitHookPath(cwd, manager);
90
+ if (!fs.existsSync(hookPath))
91
+ return undefined;
92
+ if (hook.isInstalled({ cwd, manager }))
93
+ return undefined;
94
+ const rel = hookRelativePath(cwd, hookPath);
95
+ if (manager === 'husky') {
96
+ const content = readFileIfExists(hookPath) ?? '';
97
+ if (content.includes('vault-guard'))
98
+ return undefined;
99
+ return { path: rel, reason: 'foreign_hook' };
100
+ }
101
+ if (manager === 'lefthook') {
102
+ const localPath = path.join(cwd, 'lefthook-local.yml');
103
+ if (!fs.existsSync(localPath))
104
+ return undefined;
105
+ const content = readFileIfExists(localPath) ?? '';
106
+ if (content.includes('vault-guard scan --staged'))
107
+ return undefined;
108
+ return { path: 'lefthook-local.yml', reason: 'foreign_hook' };
109
+ }
110
+ if (manager === 'precommit') {
111
+ const configPath = path.join(cwd, '.pre-commit-config.yaml');
112
+ if (!fs.existsSync(configPath))
113
+ return undefined;
114
+ const content = readFileIfExists(configPath) ?? '';
115
+ if (content.includes('vault-guard scan --staged'))
116
+ return undefined;
117
+ return { path: '.pre-commit-config.yaml', reason: 'foreign_hook' };
118
+ }
119
+ const content = readFileIfExists(hookPath) ?? '';
120
+ if (content.trim().length === 0)
121
+ return undefined;
122
+ return { path: rel, reason: 'foreign_hook' };
123
+ }
124
+ function ensureParentDir(filePath) {
125
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
126
+ }
127
+ function planInit(options = {}) {
128
+ const cwd = options.cwd ?? process.cwd();
129
+ const dryRun = Boolean(options.dryRun);
130
+ const manager = options.manager ?? 'native';
131
+ const actions = [];
132
+ const conflicts = [];
133
+ const trackedFiles = [];
134
+ const manifestAbs = path.join(cwd, templates_1.MANIFEST_RELATIVE_PATH);
135
+ const manifestRaw = readFileIfExists(manifestAbs);
136
+ const manifestParsed = manifestRaw ? parseManifest(manifestRaw) : undefined;
137
+ if (manifestRaw && !manifestParsed) {
138
+ conflicts.push({ path: templates_1.MANIFEST_RELATIVE_PATH, reason: 'foreign_manifest' });
139
+ }
140
+ for (const rel of managedContentPaths(options)) {
141
+ const abs = path.join(cwd, rel);
142
+ const expected = (0, templates_1.templateContentForPath)(rel);
143
+ const current = readFileIfExists(abs);
144
+ if (current === undefined) {
145
+ actions.push({ kind: 'create', path: rel });
146
+ trackedFiles.push({ path: rel, action: 'created' });
147
+ continue;
148
+ }
149
+ if (current === expected) {
150
+ actions.push({ kind: 'skip', path: rel, detail: 'content matches template' });
151
+ trackedFiles.push({ path: rel, action: 'created' });
152
+ continue;
153
+ }
154
+ conflicts.push({ path: rel, reason: 'exists' });
155
+ }
156
+ let hookState;
157
+ if (!options.skipHook) {
158
+ if (!isGitRepo(cwd)) {
159
+ conflicts.push({ path: '.git', reason: 'not_a_git_repository' });
160
+ }
161
+ else {
162
+ const foreignHook = foreignHookConflict(cwd, manager);
163
+ if (foreignHook) {
164
+ conflicts.push(foreignHook);
165
+ }
166
+ else {
167
+ const hook = new vault_guard_core_1.PreCommitHook();
168
+ const hookPath = hook.getPreCommitHookPath(cwd, manager);
169
+ if (hook.isInstalled({ cwd, manager })) {
170
+ hookState = { manager, path: hookPath, installed: true };
171
+ actions.push({ kind: 'skip', path: hookPath, detail: 'hook already installed' });
172
+ }
173
+ else {
174
+ hookState = { manager, path: hookPath, installed: false };
175
+ actions.push({ kind: 'hook-install', path: hookPath, detail: manager });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ const hookMeta = hookState?.path && !options.skipHook
181
+ ? { manager, path: hookRelativePath(cwd, hookState.path) }
182
+ : undefined;
183
+ function manifestMatchesState(manifest) {
184
+ if (manifest.templateVersion !== templates_1.INIT_TEMPLATE_VERSION)
185
+ return false;
186
+ const expectedPaths = trackedFiles
187
+ .map(f => f.path)
188
+ .sort()
189
+ .join('\0');
190
+ const actualPaths = manifest.files
191
+ .map(f => f.path)
192
+ .sort()
193
+ .join('\0');
194
+ if (expectedPaths !== actualPaths)
195
+ return false;
196
+ if (hookMeta) {
197
+ return manifest.hookManager === hookMeta.manager && manifest.hookPath === hookMeta.path;
198
+ }
199
+ return manifest.hookManager === undefined && manifest.hookPath === undefined;
200
+ }
201
+ if (manifestRaw === undefined && conflicts.length === 0) {
202
+ actions.push({ kind: 'create', path: templates_1.MANIFEST_RELATIVE_PATH });
203
+ }
204
+ else if (manifestParsed) {
205
+ if (manifestMatchesState(manifestParsed)) {
206
+ actions.push({ kind: 'skip', path: templates_1.MANIFEST_RELATIVE_PATH, detail: 'manifest current' });
207
+ }
208
+ else if (conflicts.length === 0) {
209
+ conflicts.push({ path: templates_1.MANIFEST_RELATIVE_PATH, reason: 'manifest_mismatch' });
210
+ }
211
+ }
212
+ const alreadyInitialized = conflicts.length === 0 &&
213
+ actions.length > 0 &&
214
+ actions.every(a => a.kind === 'skip');
215
+ return {
216
+ ok: conflicts.length === 0,
217
+ dryRun,
218
+ reverted: false,
219
+ alreadyInitialized,
220
+ actions,
221
+ conflicts,
222
+ hook: hookState,
223
+ manifestPath: templates_1.MANIFEST_RELATIVE_PATH,
224
+ mcpMergeHint: MCP_MERGE_HINT,
225
+ };
226
+ }
227
+ function applyInit(plan, options = {}) {
228
+ if (!plan.ok || plan.dryRun || plan.alreadyInitialized) {
229
+ return plan;
230
+ }
231
+ const cwd = options.cwd ?? process.cwd();
232
+ const manager = options.manager ?? 'native';
233
+ const trackedFiles = [];
234
+ const createdPaths = [];
235
+ const rollbackCreatedFiles = () => {
236
+ for (const rel of createdPaths.reverse()) {
237
+ const abs = path.join(cwd, rel);
238
+ try {
239
+ if (fs.existsSync(abs))
240
+ fs.unlinkSync(abs);
241
+ }
242
+ catch {
243
+ /* best effort */
244
+ }
245
+ }
246
+ };
247
+ if (plan.hook && !plan.hook.installed && !options.skipHook) {
248
+ const hook = new vault_guard_core_1.PreCommitHook();
249
+ const result = hook.install({ cwd, manager });
250
+ if (!result.success) {
251
+ return {
252
+ ...plan,
253
+ ok: false,
254
+ actions: [
255
+ ...plan.actions,
256
+ {
257
+ kind: 'skip',
258
+ path: plan.hook.path ?? 'pre-commit',
259
+ detail: `hook install failed: ${result.message}`,
260
+ },
261
+ ],
262
+ };
263
+ }
264
+ plan.hook = {
265
+ manager,
266
+ path: result.hookPath ?? plan.hook.path,
267
+ installed: true,
268
+ };
269
+ }
270
+ try {
271
+ for (const action of plan.actions) {
272
+ if (action.kind !== 'create' || action.path === templates_1.MANIFEST_RELATIVE_PATH)
273
+ continue;
274
+ const abs = path.join(cwd, action.path);
275
+ ensureParentDir(abs);
276
+ fs.writeFileSync(abs, (0, templates_1.templateContentForPath)(action.path), {
277
+ encoding: 'utf8',
278
+ flag: 'wx',
279
+ });
280
+ trackedFiles.push({ path: action.path, action: 'created' });
281
+ createdPaths.push(action.path);
282
+ }
283
+ }
284
+ catch (error) {
285
+ rollbackCreatedFiles();
286
+ if (plan.hook?.installed && !options.skipHook) {
287
+ new vault_guard_core_1.PreCommitHook().uninstall({ cwd, manager });
288
+ }
289
+ return {
290
+ ...plan,
291
+ ok: false,
292
+ actions: [
293
+ ...plan.actions,
294
+ {
295
+ kind: 'skip',
296
+ path: 'init',
297
+ detail: `file write failed: ${String(error)}`,
298
+ },
299
+ ],
300
+ };
301
+ }
302
+ const hookMeta = plan.hook?.path && !options.skipHook
303
+ ? { manager, path: hookRelativePath(cwd, plan.hook.path) }
304
+ : undefined;
305
+ const allTracked = trackedFiles.length > 0
306
+ ? trackedFiles
307
+ : managedContentPaths(options).map(p => ({ path: p, action: 'created' }));
308
+ const manifestContent = (0, templates_1.buildManifestContent)(allTracked, hookMeta);
309
+ const manifestAbs = path.join(cwd, templates_1.MANIFEST_RELATIVE_PATH);
310
+ try {
311
+ if (!fs.existsSync(manifestAbs)) {
312
+ ensureParentDir(manifestAbs);
313
+ fs.writeFileSync(manifestAbs, manifestContent, { encoding: 'utf8', flag: 'wx' });
314
+ }
315
+ }
316
+ catch (error) {
317
+ rollbackCreatedFiles();
318
+ if (plan.hook?.installed && !options.skipHook) {
319
+ new vault_guard_core_1.PreCommitHook().uninstall({ cwd, manager });
320
+ }
321
+ return {
322
+ ...plan,
323
+ ok: false,
324
+ actions: [
325
+ ...plan.actions,
326
+ {
327
+ kind: 'skip',
328
+ path: templates_1.MANIFEST_RELATIVE_PATH,
329
+ detail: `manifest write failed: ${String(error)}`,
330
+ },
331
+ ],
332
+ };
333
+ }
334
+ return { ...plan, ok: true };
335
+ }
336
+ function revertInit(options = {}) {
337
+ const cwd = options.cwd ?? process.cwd();
338
+ const dryRun = Boolean(options.dryRun);
339
+ const manifestAbs = path.join(cwd, templates_1.MANIFEST_RELATIVE_PATH);
340
+ const raw = readFileIfExists(manifestAbs);
341
+ if (!raw) {
342
+ return {
343
+ ok: false,
344
+ dryRun,
345
+ reverted: false,
346
+ alreadyInitialized: false,
347
+ actions: [],
348
+ conflicts: [{ path: templates_1.MANIFEST_RELATIVE_PATH, reason: 'foreign_manifest' }],
349
+ manifestPath: templates_1.MANIFEST_RELATIVE_PATH,
350
+ mcpMergeHint: MCP_MERGE_HINT,
351
+ };
352
+ }
353
+ const manifest = parseManifest(raw);
354
+ if (!manifest) {
355
+ return {
356
+ ok: false,
357
+ dryRun,
358
+ reverted: false,
359
+ alreadyInitialized: false,
360
+ actions: [],
361
+ conflicts: [{ path: templates_1.MANIFEST_RELATIVE_PATH, reason: 'foreign_manifest' }],
362
+ manifestPath: templates_1.MANIFEST_RELATIVE_PATH,
363
+ mcpMergeHint: MCP_MERGE_HINT,
364
+ };
365
+ }
366
+ const actions = [];
367
+ const filePaths = [...manifest.files.map(f => f.path)].reverse();
368
+ for (const rel of filePaths) {
369
+ actions.push({
370
+ kind: 'skip',
371
+ path: rel,
372
+ detail: dryRun ? 'would remove file' : 'removed file',
373
+ });
374
+ if (!dryRun) {
375
+ const abs = path.join(cwd, rel);
376
+ if (fs.existsSync(abs))
377
+ fs.unlinkSync(abs);
378
+ }
379
+ }
380
+ if (manifest.hookManager) {
381
+ actions.push({
382
+ kind: 'hook-install',
383
+ path: manifest.hookPath ?? 'pre-commit',
384
+ detail: dryRun ? 'would uninstall hook' : 'uninstalled hook',
385
+ });
386
+ if (!dryRun) {
387
+ new vault_guard_core_1.PreCommitHook().uninstall({
388
+ cwd,
389
+ manager: manifest.hookManager,
390
+ });
391
+ }
392
+ }
393
+ actions.push({
394
+ kind: 'skip',
395
+ path: templates_1.MANIFEST_RELATIVE_PATH,
396
+ detail: dryRun ? 'would remove manifest' : 'removed manifest',
397
+ });
398
+ if (!dryRun && fs.existsSync(manifestAbs)) {
399
+ fs.unlinkSync(manifestAbs);
400
+ const vgDir = path.join(cwd, '.vault-guard');
401
+ try {
402
+ if (fs.existsSync(vgDir) && fs.readdirSync(vgDir).length === 0) {
403
+ fs.rmdirSync(vgDir);
404
+ }
405
+ }
406
+ catch {
407
+ /* best effort */
408
+ }
409
+ }
410
+ return {
411
+ ok: true,
412
+ dryRun,
413
+ reverted: !dryRun,
414
+ alreadyInitialized: false,
415
+ actions,
416
+ conflicts: [],
417
+ manifestPath: templates_1.MANIFEST_RELATIVE_PATH,
418
+ mcpMergeHint: MCP_MERGE_HINT,
419
+ };
420
+ }
421
+ function printHuman(result, options) {
422
+ if (options.revert) {
423
+ if (!result.ok) {
424
+ console.error(chalk_1.default.red('❌ Revert failed:'), 'no valid init manifest found');
425
+ return;
426
+ }
427
+ console.log(result.dryRun
428
+ ? chalk_1.default.blue('🔍 Dry-run revert plan')
429
+ : chalk_1.default.green.bold('✅ Reverted Vault Guard init artifacts'));
430
+ for (const a of result.actions) {
431
+ console.log(chalk_1.default.gray(` ${a.detail ?? a.path}`));
432
+ }
433
+ return;
434
+ }
435
+ if (result.conflicts.length > 0) {
436
+ console.error(chalk_1.default.red.bold('❌ Init blocked — conflicts (no automatic overwrites):'));
437
+ for (const c of result.conflicts) {
438
+ console.error(chalk_1.default.white(` ${c.path}`), chalk_1.default.gray(`(${c.reason})`));
439
+ }
440
+ console.error(chalk_1.default.gray('\nResolve manually, then re-run vault-guard init.'));
441
+ return;
442
+ }
443
+ if (result.alreadyInitialized) {
444
+ console.log(chalk_1.default.green('✅ Already initialized — no changes needed'));
445
+ return;
446
+ }
447
+ if (result.dryRun) {
448
+ console.log(chalk_1.default.blue('🔍 Dry-run — would apply:'));
449
+ }
450
+ else {
451
+ console.log(chalk_1.default.green.bold('✅ Vault Guard initialized'));
452
+ }
453
+ for (const a of result.actions) {
454
+ if (a.kind === 'create') {
455
+ console.log(chalk_1.default.white(` ${result.dryRun ? 'create' : 'created'} ${a.path}`));
456
+ }
457
+ else if (a.kind === 'hook-install') {
458
+ console.log(chalk_1.default.white(` ${result.dryRun ? 'install' : 'installed'} hook (${a.detail})`));
459
+ }
460
+ }
461
+ console.log(chalk_1.default.gray(`\n${result.mcpMergeHint}`));
462
+ console.log(chalk_1.default.gray(`Manifest: ${result.manifestPath}`));
463
+ }
464
+ async function initCommand(options = {}) {
465
+ if (options.revert) {
466
+ const result = revertInit(options);
467
+ if (options.json) {
468
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
469
+ }
470
+ else {
471
+ printHuman(result, options);
472
+ }
473
+ return result.ok ? 0 : 1;
474
+ }
475
+ const plan = planInit(options);
476
+ const result = plan.dryRun || plan.alreadyInitialized ? plan : applyInit(plan, options);
477
+ if (options.json) {
478
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
479
+ }
480
+ else {
481
+ printHuman(result, options);
482
+ }
483
+ if (result.conflicts.length > 0)
484
+ return 2;
485
+ if (!result.ok)
486
+ return 1;
487
+ return 0;
488
+ }
@@ -0,0 +1,28 @@
1
+ /** Stable init template version — bump when file contents change materially. */
2
+ export declare const INIT_TEMPLATE_VERSION = "1";
3
+ export declare const MANIFEST_RELATIVE_PATH = ".vault-guard/manifest.json";
4
+ export declare const MANAGED_FILE_PATHS: readonly [".vault-guard.json", ".github/workflows/vault-guard.yml", ".vault-guard/mcp-snippet.json", ".vault-guard/agent-rules.md", ".vault-guard/manifest.json"];
5
+ export type ManagedFilePath = (typeof MANAGED_FILE_PATHS)[number];
6
+ export declare function defaultVaultGuardConfigJson(): string;
7
+ export declare function githubWorkflowYaml(): string;
8
+ export declare function mcpSnippetJson(): string;
9
+ export declare function agentRulesMarkdown(): string;
10
+ export declare function templateContentForPath(relativePath: ManagedFilePath): string;
11
+ export interface InitManifest {
12
+ initVersion: string;
13
+ templateVersion: string;
14
+ createdAt: string;
15
+ hookManager?: string;
16
+ hookPath?: string;
17
+ files: Array<{
18
+ path: string;
19
+ action: 'created';
20
+ }>;
21
+ }
22
+ export declare function buildManifestContent(files: Array<{
23
+ path: string;
24
+ action: 'created';
25
+ }>, hook?: {
26
+ manager: string;
27
+ path: string;
28
+ }): string;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MANAGED_FILE_PATHS = exports.MANIFEST_RELATIVE_PATH = exports.INIT_TEMPLATE_VERSION = void 0;
4
+ exports.defaultVaultGuardConfigJson = defaultVaultGuardConfigJson;
5
+ exports.githubWorkflowYaml = githubWorkflowYaml;
6
+ exports.mcpSnippetJson = mcpSnippetJson;
7
+ exports.agentRulesMarkdown = agentRulesMarkdown;
8
+ exports.templateContentForPath = templateContentForPath;
9
+ exports.buildManifestContent = buildManifestContent;
10
+ /** Stable init template version — bump when file contents change materially. */
11
+ exports.INIT_TEMPLATE_VERSION = '1';
12
+ exports.MANIFEST_RELATIVE_PATH = '.vault-guard/manifest.json';
13
+ exports.MANAGED_FILE_PATHS = [
14
+ '.vault-guard.json',
15
+ '.github/workflows/vault-guard.yml',
16
+ '.vault-guard/mcp-snippet.json',
17
+ '.vault-guard/agent-rules.md',
18
+ exports.MANIFEST_RELATIVE_PATH,
19
+ ];
20
+ function defaultVaultGuardConfigJson() {
21
+ return `${JSON.stringify({
22
+ ignore: {
23
+ patterns: ['**/__tests__/**', 'fixtures/**', 'bench/fixtures/**'],
24
+ },
25
+ }, null, 2)}\n`;
26
+ }
27
+ function githubWorkflowYaml() {
28
+ return `name: Vault Guard
29
+
30
+ on:
31
+ push:
32
+ branches: [main]
33
+ pull_request:
34
+ branches: [main]
35
+
36
+ jobs:
37
+ secrets:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
41
+ - uses: vaultcompasshq/vault-guard@v1.1.2
42
+ with:
43
+ version: latest
44
+ path: .
45
+ format: sarif
46
+ sarif-output: vault-guard-results.sarif
47
+ - uses: github/codeql-action/upload-sarif@v3
48
+ if: always()
49
+ with:
50
+ sarif_file: vault-guard-results.sarif
51
+ `;
52
+ }
53
+ function mcpSnippetJson() {
54
+ return `${JSON.stringify({
55
+ mcpServers: {
56
+ 'vault-guard': {
57
+ command: 'npx',
58
+ args: ['-y', '@vaultcompass/vault-guard-mcp'],
59
+ },
60
+ },
61
+ }, null, 2)}\n`;
62
+ }
63
+ function agentRulesMarkdown() {
64
+ return `# Vault Guard — agent guardrails
65
+
66
+ Vault Guard is the local secret scanner for this repository. Follow these rules
67
+ before writing, editing, or committing code.
68
+
69
+ ## Before applying edits
70
+
71
+ 1. Call the Vault Guard MCP tool \`scan_text\` on any proposed file content that
72
+ may contain credentials (API keys, tokens, connection strings, private keys).
73
+ 2. If findings are returned, do **not** write the secret material. Redact or
74
+ replace with environment variables / placeholders and scan again.
75
+ 3. For whole files on disk, use \`scan_file\`. For directories, use
76
+ \`scan_workspace\`.
77
+
78
+ ## Before committing
79
+
80
+ - Ensure \`vault-guard scan --staged\` passes (pre-commit hook enforces this).
81
+ - Never use \`git commit --no-verify\` to bypass secret checks unless the user
82
+ explicitly requests an emergency bypass.
83
+
84
+ ## Merge MCP config (manual)
85
+
86
+ Copy the \`mcpServers\` block from \`.vault-guard/mcp-snippet.json\` into your
87
+ editor MCP config (e.g. \`~/.cursor/mcp.json\` or Claude Desktop config). Vault
88
+ Guard does not modify files outside this repository.
89
+
90
+ ## History scanning
91
+
92
+ Vault Guard does not scan Git history. Use Gitleaks or TruffleHog for retroactive
93
+ history mining alongside Vault Guard's working-tree protection.
94
+ `;
95
+ }
96
+ function templateContentForPath(relativePath) {
97
+ switch (relativePath) {
98
+ case '.vault-guard.json':
99
+ return defaultVaultGuardConfigJson();
100
+ case '.github/workflows/vault-guard.yml':
101
+ return githubWorkflowYaml();
102
+ case '.vault-guard/mcp-snippet.json':
103
+ return mcpSnippetJson();
104
+ case '.vault-guard/agent-rules.md':
105
+ return agentRulesMarkdown();
106
+ default:
107
+ throw new Error(`No template for ${relativePath}`);
108
+ }
109
+ }
110
+ function buildManifestContent(files, hook) {
111
+ const manifest = {
112
+ initVersion: '1',
113
+ templateVersion: exports.INIT_TEMPLATE_VERSION,
114
+ createdAt: new Date().toISOString(),
115
+ files,
116
+ ...(hook ? { hookManager: hook.manager, hookPath: hook.path } : {}),
117
+ };
118
+ return `${JSON.stringify(manifest, null, 2)}\n`;
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaultcompass/vault-guard",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Block secrets at commit and in CI. Pre-commit hooks, SARIF output, and fast staged-file scans for AI-native workflows.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "chalk": "^4.1.2",
38
38
  "commander": "^12.0.0",
39
- "@vaultcompass/vault-guard-core": "1.1.1",
40
- "@vaultcompass/vault-guard-telemetry": "1.1.1"
39
+ "@vaultcompass/vault-guard-core": "1.2.0",
40
+ "@vaultcompass/vault-guard-telemetry": "1.2.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/jest": "^30.0.0",