sinapse-ai 7.7.3 → 7.7.4
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/.codex/catalog.json +157 -0
- package/.codex/command-registry.json +441 -0
- package/.codex/scripts/generate-codex-greeting.js +101 -0
- package/.codex/scripts/resolve-codex-command.js +147 -0
- package/.codex/skills/sinapse-analyst/SKILL.md +5 -4
- package/.codex/skills/sinapse-architect/SKILL.md +5 -4
- package/.codex/skills/sinapse-data-engineer/SKILL.md +5 -4
- package/.codex/skills/sinapse-dev/SKILL.md +5 -4
- package/.codex/skills/sinapse-devops/SKILL.md +5 -4
- package/.codex/skills/sinapse-orqx/SKILL.md +10 -15
- package/.codex/skills/sinapse-pm/SKILL.md +5 -4
- package/.codex/skills/sinapse-po/SKILL.md +4 -3
- package/.codex/skills/sinapse-qa/SKILL.md +12 -11
- package/.codex/skills/sinapse-sm/SKILL.md +5 -4
- package/.codex/skills/sinapse-squad-creator/SKILL.md +5 -4
- package/.codex/skills/sinapse-ux-design-expert/SKILL.md +5 -4
- package/.codex/tasks/convene-sinapse-council.md +28 -0
- package/.codex/tasks/create-sinapse-strategic-brief.md +29 -0
- package/.codex/tasks/onboard-sinapse-codex.md +34 -0
- package/.codex/tasks/plan-sinapse-initiative.md +33 -0
- package/.codex/tasks/resolve-sinapse-conflict.md +28 -0
- package/.codex/tasks/route-sinapse-request.md +33 -0
- package/.codex/tasks/status-sinapse-capabilities.md +28 -0
- package/.sinapse-ai/core-config.yaml +1 -1
- package/.sinapse-ai/data/entity-registry.yaml +848 -751
- package/.sinapse-ai/data/registry-update-log.jsonl +10 -0
- package/.sinapse-ai/infrastructure/scripts/codex-parity/catalog.js +123 -0
- package/.sinapse-ai/infrastructure/scripts/codex-skills-sync/index.js +60 -11
- package/.sinapse-ai/infrastructure/scripts/codex-skills-sync/validate.js +44 -16
- package/.sinapse-ai/infrastructure/scripts/sync-codex-local-first.js +156 -0
- package/.sinapse-ai/infrastructure/scripts/validate-codex-command-registry.js +264 -0
- package/.sinapse-ai/infrastructure/scripts/validate-codex-integration.js +15 -6
- package/.sinapse-ai/infrastructure/scripts/validate-codex-sync.js +156 -0
- package/.sinapse-ai/infrastructure/scripts/validate-parity.js +3 -1
- package/.sinapse-ai/infrastructure/scripts/validate-paths.js +8 -10
- package/.sinapse-ai/infrastructure/templates/safe-collab/README.md +8 -0
- package/.sinapse-ai/install-manifest.yaml +35 -19
- package/.sinapse-ai/project-config.yaml +1 -1
- package/bin/utils/collab-start.js +267 -0
- package/bin/utils/git-branch-guard.js +76 -0
- package/bin/utils/pre-push-safety.js +110 -0
- package/bin/utils/staged-secret-scan.js +108 -0
- package/docs/codex-parity-program.md +670 -0
- package/docs/codex-total-parity-orchestration-plan.md +301 -0
- package/docs/codex-workflow-task-parity.md +87 -0
- package/docs/collaboration-autonomy-plan.md +243 -0
- package/docs/guides/framework-contributor-mode.md +310 -0
- package/docs/guides/parallel-collaboration-source-of-truth.md +481 -0
- package/package.json +11 -3
- package/packages/installer/tests/unit/entity-registry-bootstrap.test.js +2 -2
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync, execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function execGit(command) {
|
|
7
|
+
return execSync(command, {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
+
}).trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeExec(command) {
|
|
14
|
+
try {
|
|
15
|
+
return execGit(command);
|
|
16
|
+
} catch {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const parsed = {
|
|
23
|
+
adoptCurrent: false,
|
|
24
|
+
check: false,
|
|
25
|
+
type: 'feat',
|
|
26
|
+
owner: null,
|
|
27
|
+
storyId: null,
|
|
28
|
+
slug: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const arg of argv) {
|
|
32
|
+
if (arg === '--adopt-current') {
|
|
33
|
+
parsed.adoptCurrent = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === '--check') {
|
|
37
|
+
parsed.check = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg.startsWith('--type=')) {
|
|
41
|
+
parsed.type = arg.split('=')[1] || parsed.type;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg.startsWith('--owner=')) {
|
|
45
|
+
parsed.owner = arg.split('=')[1] || null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!parsed.storyId) {
|
|
49
|
+
parsed.storyId = arg;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!parsed.slug) {
|
|
53
|
+
parsed.slug = arg;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveDefaultBranch() {
|
|
61
|
+
try {
|
|
62
|
+
const remoteHead = execGit('git symbolic-ref --short refs/remotes/origin/HEAD');
|
|
63
|
+
return remoteHead.replace(/^origin\//, '');
|
|
64
|
+
} catch {
|
|
65
|
+
return 'main';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getCurrentBranch() {
|
|
70
|
+
return execGit('git rev-parse --abbrev-ref HEAD');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getStatusLines() {
|
|
74
|
+
const output = safeExec('git status --porcelain');
|
|
75
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeSegment(value) {
|
|
79
|
+
return String(value || '')
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
83
|
+
.replace(/^-+|-+$/g, '')
|
|
84
|
+
.replace(/-{2,}/g, '-');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function detectOwnerPrefix(explicitOwner) {
|
|
88
|
+
if (explicitOwner) {
|
|
89
|
+
return sanitizeSegment(explicitOwner) || 'dev';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rawSignals = [
|
|
93
|
+
safeExec('git config user.name'),
|
|
94
|
+
safeExec('git config user.email'),
|
|
95
|
+
process.env.USERNAME || '',
|
|
96
|
+
process.env.USER || '',
|
|
97
|
+
]
|
|
98
|
+
.join(' ')
|
|
99
|
+
.toLowerCase();
|
|
100
|
+
|
|
101
|
+
if (/(caio|imori)/.test(rawSignals)) {
|
|
102
|
+
return 'caio';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (/(matheus|soier|sawyer)/.test(rawSignals)) {
|
|
106
|
+
return 'soier';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return 'dev';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildWorkItemSlug(storyId, slug) {
|
|
113
|
+
const sanitizedStory = sanitizeSegment(storyId);
|
|
114
|
+
const sanitizedSlug = sanitizeSegment(slug || storyId);
|
|
115
|
+
if (!sanitizedSlug) {
|
|
116
|
+
return sanitizedStory;
|
|
117
|
+
}
|
|
118
|
+
if (!sanitizedStory) {
|
|
119
|
+
return sanitizedSlug;
|
|
120
|
+
}
|
|
121
|
+
return sanitizedSlug.startsWith(`${sanitizedStory}-`) || sanitizedSlug === sanitizedStory
|
|
122
|
+
? sanitizedSlug
|
|
123
|
+
: `${sanitizedStory}-${sanitizedSlug}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildBranchName({ owner, type, storyId, slug }) {
|
|
127
|
+
return `${owner}/${sanitizeSegment(type) || 'feat'}/${buildWorkItemSlug(storyId, slug)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildWorktreePath(worktreeKey) {
|
|
131
|
+
return path.join(process.cwd(), '.sinapse', 'worktrees', worktreeKey);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureReadyDefaultBranch(defaultBranch) {
|
|
135
|
+
const branchName = getCurrentBranch();
|
|
136
|
+
const statusLines = getStatusLines();
|
|
137
|
+
|
|
138
|
+
if (branchName !== defaultBranch) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Run collab:start from a clean '${defaultBranch}' checkout. Current branch: '${branchName}'.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (statusLines.length > 0) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Working tree is dirty on '${defaultBranch}'. Commit or stash your changes before opening a new worktree.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ensureOnDefaultBranch(defaultBranch) {
|
|
152
|
+
const branchName = getCurrentBranch();
|
|
153
|
+
if (branchName !== defaultBranch) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Run this command from '${defaultBranch}'. Current branch: '${branchName}'.`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function ensureUpToDate(defaultBranch) {
|
|
161
|
+
execFileSync('git', ['fetch', 'origin'], {
|
|
162
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const localSha = safeExec(`git rev-parse ${defaultBranch}`);
|
|
166
|
+
const remoteSha = safeExec(`git rev-parse origin/${defaultBranch}`);
|
|
167
|
+
|
|
168
|
+
if (localSha && remoteSha && localSha !== remoteSha) {
|
|
169
|
+
execFileSync('git', ['pull', '--ff-only', 'origin', defaultBranch], {
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createWorktree({ branchName, defaultBranch, worktreePath }) {
|
|
176
|
+
execFileSync('git', ['worktree', 'add', worktreePath, '-b', branchName, defaultBranch], {
|
|
177
|
+
stdio: 'inherit',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function adoptCurrentWorkspace(branchName) {
|
|
182
|
+
execFileSync('git', ['checkout', '-b', branchName], {
|
|
183
|
+
stdio: 'inherit',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function runCheck() {
|
|
188
|
+
const defaultBranch = resolveDefaultBranch();
|
|
189
|
+
const currentBranch = getCurrentBranch();
|
|
190
|
+
const statusLines = getStatusLines();
|
|
191
|
+
const owner = detectOwnerPrefix(null);
|
|
192
|
+
|
|
193
|
+
console.log(`Default branch: ${defaultBranch}`);
|
|
194
|
+
console.log(`Current branch: ${currentBranch}`);
|
|
195
|
+
console.log(`Detected maintainer prefix: ${owner}`);
|
|
196
|
+
console.log(`Working tree clean: ${statusLines.length === 0 ? 'yes' : 'no'}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function main() {
|
|
200
|
+
const args = parseArgs(process.argv.slice(2));
|
|
201
|
+
if (args.check) {
|
|
202
|
+
runCheck();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!args.storyId) {
|
|
207
|
+
console.error(
|
|
208
|
+
'Usage: npm run collab:start -- <story-id> <slug> [--type=feat] [--owner=caio] [--adopt-current]',
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const defaultBranch = resolveDefaultBranch();
|
|
214
|
+
const owner = detectOwnerPrefix(args.owner);
|
|
215
|
+
const branchName = buildBranchName({
|
|
216
|
+
owner,
|
|
217
|
+
type: args.type,
|
|
218
|
+
storyId: args.storyId,
|
|
219
|
+
slug: args.slug,
|
|
220
|
+
});
|
|
221
|
+
const worktreeKey = sanitizeSegment(branchName.replace(/\//g, '-'));
|
|
222
|
+
const worktreePath = buildWorktreePath(worktreeKey);
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
if (args.adoptCurrent) {
|
|
226
|
+
ensureOnDefaultBranch(defaultBranch);
|
|
227
|
+
adoptCurrentWorkspace(branchName);
|
|
228
|
+
} else {
|
|
229
|
+
ensureReadyDefaultBranch(defaultBranch);
|
|
230
|
+
ensureUpToDate(defaultBranch);
|
|
231
|
+
createWorktree({ branchName, defaultBranch, worktreePath });
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error('');
|
|
235
|
+
console.error(`collab:start failed: ${error.message}`);
|
|
236
|
+
console.error('');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log('');
|
|
241
|
+
if (args.adoptCurrent) {
|
|
242
|
+
console.log('Current workspace adopted into a safe maintainer branch.');
|
|
243
|
+
console.log(`Branch: ${branchName}`);
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log('Continue working in the current directory, now outside main.');
|
|
246
|
+
} else {
|
|
247
|
+
console.log('Safe maintainer workspace created.');
|
|
248
|
+
console.log(`Branch: ${branchName}`);
|
|
249
|
+
console.log(`Worktree: ${worktreePath}`);
|
|
250
|
+
console.log('');
|
|
251
|
+
console.log(`Next step: cd "${worktreePath}"`);
|
|
252
|
+
}
|
|
253
|
+
console.log('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
buildBranchName,
|
|
258
|
+
buildWorkItemSlug,
|
|
259
|
+
detectOwnerPrefix,
|
|
260
|
+
parseArgs,
|
|
261
|
+
resolveDefaultBranch,
|
|
262
|
+
sanitizeSegment,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (require.main === module) {
|
|
266
|
+
main();
|
|
267
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function execGit(command, options = {}) {
|
|
6
|
+
return execSync(command, {
|
|
7
|
+
encoding: 'utf8',
|
|
8
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9
|
+
...options,
|
|
10
|
+
}).trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveDefaultBranch() {
|
|
14
|
+
try {
|
|
15
|
+
const remoteHead = execGit('git symbolic-ref --short refs/remotes/origin/HEAD');
|
|
16
|
+
return remoteHead.replace(/^origin\//, '');
|
|
17
|
+
} catch {
|
|
18
|
+
return 'main';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCurrentBranch() {
|
|
23
|
+
return execGit('git rev-parse --abbrev-ref HEAD');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getStagedFiles() {
|
|
27
|
+
try {
|
|
28
|
+
const output = execGit('git diff --cached --name-only');
|
|
29
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shouldBlockCommit({ branchName, defaultBranch, stagedFiles }) {
|
|
36
|
+
const protectedBranches = new Set([defaultBranch, 'master']);
|
|
37
|
+
return Boolean(stagedFiles.length) && (protectedBranches.has(branchName) || branchName === 'HEAD');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function main() {
|
|
41
|
+
const defaultBranch = resolveDefaultBranch();
|
|
42
|
+
const branchName = getCurrentBranch();
|
|
43
|
+
const stagedFiles = getStagedFiles();
|
|
44
|
+
|
|
45
|
+
if (!shouldBlockCommit({ branchName, defaultBranch, stagedFiles })) {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.error('');
|
|
50
|
+
console.error('Git Branch Guard: commit blocked.');
|
|
51
|
+
console.error('');
|
|
52
|
+
if (branchName === 'HEAD') {
|
|
53
|
+
console.error('You are in detached HEAD state. Create an isolated branch or worktree first.');
|
|
54
|
+
} else {
|
|
55
|
+
console.error(`Commits are blocked on the protected branch '${branchName}'.`);
|
|
56
|
+
}
|
|
57
|
+
console.error('');
|
|
58
|
+
console.error('Start a safe maintainer workspace first:');
|
|
59
|
+
console.error(' npm run collab:start -- <story-id> <slug>');
|
|
60
|
+
console.error('');
|
|
61
|
+
console.error('Example:');
|
|
62
|
+
console.error(' npm run collab:start -- 7.7.4 codex-collab-hardening');
|
|
63
|
+
console.error('');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
getCurrentBranch,
|
|
69
|
+
getStagedFiles,
|
|
70
|
+
resolveDefaultBranch,
|
|
71
|
+
shouldBlockCommit,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (require.main === module) {
|
|
75
|
+
main();
|
|
76
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync, execSync, spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
function execGit(command) {
|
|
7
|
+
return execSync(command, {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
+
}).trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveDefaultBranch() {
|
|
14
|
+
try {
|
|
15
|
+
const remoteHead = execGit('git symbolic-ref --short refs/remotes/origin/HEAD');
|
|
16
|
+
return remoteHead.replace(/^origin\//, '');
|
|
17
|
+
} catch {
|
|
18
|
+
return 'main';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getCurrentBranch() {
|
|
23
|
+
return execGit('git rev-parse --abbrev-ref HEAD');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parsePushRefs(stdinBuffer) {
|
|
27
|
+
const payload = stdinBuffer.toString('utf8').trim();
|
|
28
|
+
if (!payload) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return payload
|
|
33
|
+
.split('\n')
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((line) => {
|
|
36
|
+
const [localRef, localSha, remoteRef, remoteSha] = line.trim().split(/\s+/);
|
|
37
|
+
return { localRef, localSha, remoteRef, remoteSha };
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isProtectedPushTarget(remoteRef, defaultBranch) {
|
|
42
|
+
return remoteRef === `refs/heads/${defaultBranch}` || remoteRef === 'refs/heads/master';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function branchContainsRemoteDefault(defaultBranch) {
|
|
46
|
+
const result = spawnSync('git', ['merge-base', '--is-ancestor', `origin/${defaultBranch}`, 'HEAD'], {
|
|
47
|
+
stdio: 'ignore',
|
|
48
|
+
});
|
|
49
|
+
return result.status === 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fetchRemoteDefault(defaultBranch) {
|
|
53
|
+
execFileSync('git', ['fetch', 'origin', defaultBranch, '--quiet'], {
|
|
54
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function main() {
|
|
59
|
+
const defaultBranch = resolveDefaultBranch();
|
|
60
|
+
const branchName = getCurrentBranch();
|
|
61
|
+
const refs = parsePushRefs(fs.readFileSync(0));
|
|
62
|
+
|
|
63
|
+
if (branchName === defaultBranch || branchName === 'master') {
|
|
64
|
+
console.error('');
|
|
65
|
+
console.error(`Pre-push Safety: push blocked on protected branch '${branchName}'.`);
|
|
66
|
+
console.error('Open a feature branch or worktree first.');
|
|
67
|
+
console.error('');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (refs.some((ref) => isProtectedPushTarget(ref.remoteRef, defaultBranch))) {
|
|
72
|
+
console.error('');
|
|
73
|
+
console.error(`Pre-push Safety: direct push to '${defaultBranch}' is blocked.`);
|
|
74
|
+
console.error('Open a pull request instead.');
|
|
75
|
+
console.error('');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
fetchRemoteDefault(defaultBranch);
|
|
81
|
+
} catch {
|
|
82
|
+
console.error('');
|
|
83
|
+
console.error(`Pre-push Safety: could not fetch origin/${defaultBranch}.`);
|
|
84
|
+
console.error('Sync with the remote before pushing.');
|
|
85
|
+
console.error('');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!branchContainsRemoteDefault(defaultBranch)) {
|
|
90
|
+
console.error('');
|
|
91
|
+
console.error(`Pre-push Safety: your branch is missing commits from origin/${defaultBranch}.`);
|
|
92
|
+
console.error(`Merge or rebase origin/${defaultBranch} before pushing.`);
|
|
93
|
+
console.error('');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
branchContainsRemoteDefault,
|
|
102
|
+
getCurrentBranch,
|
|
103
|
+
isProtectedPushTarget,
|
|
104
|
+
parsePushRefs,
|
|
105
|
+
resolveDefaultBranch,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (require.main === module) {
|
|
109
|
+
main();
|
|
110
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync, execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const BLOCKED_ENV_FILE_PATTERN = /(^|\/)\.env(\..+)?$/i;
|
|
6
|
+
const SAFE_ENV_FILE_PATTERN = /(^|\/)\.env\.(example|sample|template)$/i;
|
|
7
|
+
const SECRET_PATTERNS = [
|
|
8
|
+
{ label: 'private key', pattern: /BEGIN (RSA|DSA|EC|OPENSSH|PGP) PRIVATE KEY/ },
|
|
9
|
+
{ label: 'generic private key', pattern: /BEGIN PRIVATE KEY/ },
|
|
10
|
+
{ label: 'GitHub personal token', pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/ },
|
|
11
|
+
{ label: 'GitHub fine-grained token', pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ },
|
|
12
|
+
{ label: 'OpenAI key', pattern: /\bsk-(proj-)?[A-Za-z0-9_-]{20,}\b/ },
|
|
13
|
+
{ label: 'AWS access key', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
14
|
+
{ label: 'Google API key', pattern: /\bAIza[0-9A-Za-z\-_]{35}\b/ },
|
|
15
|
+
{ label: 'Slack token', pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function getStagedFiles() {
|
|
19
|
+
try {
|
|
20
|
+
const output = execSync('git diff --cached --name-only --diff-filter=ACMR', {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
23
|
+
}).trim();
|
|
24
|
+
return output ? output.split('\n').filter(Boolean) : [];
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isBlockedEnvFile(filePath) {
|
|
31
|
+
return BLOCKED_ENV_FILE_PATTERN.test(filePath) && !SAFE_ENV_FILE_PATTERN.test(filePath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readStagedFile(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
return execFileSync('git', ['show', `:${filePath}`], {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findSecretMatches(content) {
|
|
47
|
+
const matches = [];
|
|
48
|
+
for (const descriptor of SECRET_PATTERNS) {
|
|
49
|
+
if (descriptor.pattern.test(content)) {
|
|
50
|
+
matches.push(descriptor.label);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return matches;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function scanStagedFiles(files) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
|
|
59
|
+
for (const filePath of files) {
|
|
60
|
+
if (isBlockedEnvFile(filePath)) {
|
|
61
|
+
findings.push({ filePath, reason: 'environment file' });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const content = readStagedFile(filePath);
|
|
66
|
+
const matches = findSecretMatches(content);
|
|
67
|
+
for (const match of matches) {
|
|
68
|
+
findings.push({ filePath, reason: match });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function main() {
|
|
76
|
+
const stagedFiles = getStagedFiles();
|
|
77
|
+
if (stagedFiles.length === 0) {
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const findings = scanStagedFiles(stagedFiles);
|
|
82
|
+
if (findings.length === 0) {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.error('');
|
|
87
|
+
console.error('Staged Secret Scan: commit blocked.');
|
|
88
|
+
console.error('');
|
|
89
|
+
for (const finding of findings) {
|
|
90
|
+
console.error(`- ${finding.filePath}: ${finding.reason}`);
|
|
91
|
+
}
|
|
92
|
+
console.error('');
|
|
93
|
+
console.error('Remove the sensitive content before committing.');
|
|
94
|
+
console.error('');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
SECRET_PATTERNS,
|
|
100
|
+
findSecretMatches,
|
|
101
|
+
getStagedFiles,
|
|
102
|
+
isBlockedEnvFile,
|
|
103
|
+
scanStagedFiles,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (require.main === module) {
|
|
107
|
+
main();
|
|
108
|
+
}
|