@vaultcompass/vault-guard 1.1.2 → 1.2.1
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.js +31 -0
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +3 -1
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +488 -0
- package/dist/init/templates.d.ts +28 -0
- package/dist/init/templates.js +119 -0
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -47,6 +47,7 @@ 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');
|
|
@@ -83,6 +84,36 @@ function buildCli() {
|
|
|
83
84
|
const exitCode = await (0, scan_1.scanCommand)(path, format, Boolean(options.staged));
|
|
84
85
|
setExitCode(exitCode);
|
|
85
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;
|
|
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);
|
|
116
|
+
});
|
|
86
117
|
// Install-hook command
|
|
87
118
|
program
|
|
88
119
|
.command('install-hook')
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/commands/index.js
CHANGED
|
@@ -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.2.0
|
|
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
|
|
3
|
+
"version": "1.2.1",
|
|
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
|
|
40
|
-
"@vaultcompass/vault-guard-telemetry": "1.1
|
|
39
|
+
"@vaultcompass/vault-guard-core": "1.2.1",
|
|
40
|
+
"@vaultcompass/vault-guard-telemetry": "1.2.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/jest": "^30.0.0",
|