@imdeadpool/guardex 7.0.43 → 7.1.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/README.md +26 -0
- package/package.json +2 -1
- package/skills/gx-act/SKILL.md +82 -0
- package/src/agents/inspect.js +17 -4
- package/src/agents/launch.js +10 -1
- package/src/agents/status.js +9 -6
- package/src/budget/index.js +2 -1
- package/src/cli/args.js +52 -2
- package/src/cli/commands/agents.js +364 -0
- package/src/cli/commands/bootstrap.js +92 -0
- package/src/cli/commands/branch.js +127 -0
- package/src/cli/commands/claude.js +674 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/finish.js +26 -0
- package/src/cli/commands/mcp.js +122 -0
- package/src/cli/commands/misc.js +304 -0
- package/src/cli/commands/pr.js +439 -0
- package/src/cli/commands/prompt.js +92 -0
- package/src/cli/commands/release.js +305 -0
- package/src/cli/commands/report.js +244 -0
- package/src/cli/commands/review.js +32 -0
- package/src/cli/commands/setup.js +242 -0
- package/src/cli/commands/status.js +338 -0
- package/src/cli/commands/watch.js +234 -0
- package/src/cli/main.js +68 -3726
- package/src/cli/shared/repo-env.js +161 -0
- package/src/cli/shared/sandbox.js +417 -0
- package/src/cli/shared/scaffolding.js +535 -0
- package/src/cli/shared/toolchain-shims.js +420 -0
- package/src/context.js +229 -11
- package/src/core/runtime.js +6 -1
- package/src/doctor/index.js +42 -13
- package/src/finish/index.js +147 -5
- package/src/finish/preflight.js +177 -0
- package/src/finish/review-gate.js +182 -0
- package/src/git/index.js +446 -4
- package/src/hooks/index.js +0 -64
- package/src/mcp/collect.js +370 -0
- package/src/mcp/server.js +157 -0
- package/src/output/index.js +67 -1
- package/src/pr-review.js +23 -0
- package/src/pr.js +381 -0
- package/src/sandbox/index.js +13 -2
- package/src/scaffold/agent-worktree-prep.js +213 -0
- package/src/scaffold/index.js +108 -10
- package/src/speckit/index.js +226 -0
- package/src/terminal/index.js +1 -76
- package/src/terminal/tmux.js +0 -1
- package/src/toolchain/index.js +20 -0
- package/templates/AGENTS.monorepo-apps.md +26 -0
- package/templates/AGENTS.multiagent-safety.md +61 -347
- package/templates/AGENTS.multiagent-safety.min.md +11 -0
- package/templates/codex/skills/gx-act/SKILL.md +82 -0
- package/templates/githooks/pre-commit +22 -19
- package/templates/scripts/agent-branch-finish.sh +8 -30
- package/templates/scripts/agent-branch-merge.sh +4 -1
- package/templates/scripts/agent-branch-start.sh +88 -3
- package/templates/scripts/agent-preflight.sh +31 -5
- package/templates/scripts/agent-worktree-prune.sh +1 -1
- package/templates/scripts/codex-agent.sh +0 -91
- package/src/agents/detect.js +0 -160
- package/src/cockpit/keybindings.js +0 -224
- package/src/cockpit/layout.js +0 -224
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// `gx release` — maintainer-only GitHub release sync from README. Pure
|
|
2
|
+
// code-motion from src/cli/main.js.
|
|
3
|
+
const {
|
|
4
|
+
fs,
|
|
5
|
+
path,
|
|
6
|
+
TOOL_NAME,
|
|
7
|
+
GH_BIN,
|
|
8
|
+
MAINTAINER_RELEASE_REPO,
|
|
9
|
+
} = require('../../context');
|
|
10
|
+
const {
|
|
11
|
+
gitRun,
|
|
12
|
+
resolveRepoRoot,
|
|
13
|
+
readGitConfig,
|
|
14
|
+
} = require('../../git');
|
|
15
|
+
const { run } = require('../../core/runtime');
|
|
16
|
+
const {
|
|
17
|
+
parseVersionString,
|
|
18
|
+
compareParsedVersions,
|
|
19
|
+
} = require('../../core/versions');
|
|
20
|
+
const {
|
|
21
|
+
inferGithubRepoSlug,
|
|
22
|
+
isCommandAvailable,
|
|
23
|
+
} = require('../shared/repo-env');
|
|
24
|
+
|
|
25
|
+
function ensureMainBranch(repoRoot) {
|
|
26
|
+
const branchResult = gitRun(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD'], { allowFailure: true });
|
|
27
|
+
if (branchResult.status !== 0) {
|
|
28
|
+
throw new Error(`Unable to detect current branch in ${repoRoot}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const branch = branchResult.stdout.trim();
|
|
32
|
+
if (branch !== 'main') {
|
|
33
|
+
throw new Error(`Release blocked: current branch is '${branch}' (required: 'main')`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ensureCleanWorkingTree(repoRoot) {
|
|
38
|
+
const statusResult = gitRun(repoRoot, ['status', '--porcelain'], { allowFailure: true });
|
|
39
|
+
if (statusResult.status !== 0) {
|
|
40
|
+
throw new Error(`Unable to read git status in ${repoRoot}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dirty = statusResult.stdout.trim();
|
|
44
|
+
if (dirty.length > 0) {
|
|
45
|
+
throw new Error('Release blocked: working tree is not clean');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readReleaseRepoPackageJson(repoRoot) {
|
|
50
|
+
const manifestPath = path.join(repoRoot, 'package.json');
|
|
51
|
+
if (!fs.existsSync(manifestPath)) {
|
|
52
|
+
throw new Error(`Release blocked: package.json missing in ${repoRoot}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Release blocked: unable to parse package.json in ${repoRoot}: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveReleaseGithubRepo(repoRoot) {
|
|
63
|
+
const releasePackageJson = readReleaseRepoPackageJson(repoRoot);
|
|
64
|
+
const fromManifest = inferGithubRepoSlug(
|
|
65
|
+
releasePackageJson.repository &&
|
|
66
|
+
(releasePackageJson.repository.url || releasePackageJson.repository),
|
|
67
|
+
);
|
|
68
|
+
if (fromManifest) {
|
|
69
|
+
return fromManifest;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fromOrigin = inferGithubRepoSlug(readGitConfig(repoRoot, 'remote.origin.url'));
|
|
73
|
+
if (fromOrigin) {
|
|
74
|
+
return fromOrigin;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Release blocked: unable to resolve GitHub repo from package.json repository URL or origin remote.',
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readRepoReadme(repoRoot) {
|
|
83
|
+
const readmePath = path.join(repoRoot, 'README.md');
|
|
84
|
+
if (!fs.existsSync(readmePath)) {
|
|
85
|
+
throw new Error(`Release blocked: README.md missing in ${repoRoot}`);
|
|
86
|
+
}
|
|
87
|
+
return fs.readFileSync(readmePath, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseReadmeReleaseEntries(readmeContent) {
|
|
91
|
+
const releaseNotesIndex = String(readmeContent || '').indexOf('## Release notes');
|
|
92
|
+
if (releaseNotesIndex < 0) {
|
|
93
|
+
throw new Error('Release blocked: README.md is missing the "## Release notes" section');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const releaseNotesContent = String(readmeContent || '').slice(releaseNotesIndex);
|
|
97
|
+
const entries = [];
|
|
98
|
+
const lines = releaseNotesContent.split(/\r?\n/);
|
|
99
|
+
let currentTag = '';
|
|
100
|
+
let currentLines = [];
|
|
101
|
+
|
|
102
|
+
function flushEntry() {
|
|
103
|
+
if (!currentTag) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const body = currentLines.join('\n').trim();
|
|
107
|
+
if (body) {
|
|
108
|
+
entries.push({ tag: currentTag, body, version: parseVersionString(currentTag) });
|
|
109
|
+
}
|
|
110
|
+
currentTag = '';
|
|
111
|
+
currentLines = [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
const headingMatch = line.match(/^###\s+(v\d+\.\d+\.\d+)\s*$/);
|
|
116
|
+
if (headingMatch) {
|
|
117
|
+
flushEntry();
|
|
118
|
+
currentTag = headingMatch[1];
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!currentTag) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (/^<\/details>\s*$/.test(line) || /^##\s+/.test(line)) {
|
|
127
|
+
flushEntry();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
currentLines.push(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
flushEntry();
|
|
135
|
+
|
|
136
|
+
if (entries.length === 0) {
|
|
137
|
+
throw new Error('Release blocked: README.md did not yield any versioned release-note sections');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return entries;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolvePreviousPublishedReleaseTag(repoSlug, currentTag) {
|
|
144
|
+
const result = run(GH_BIN, ['release', 'list', '--repo', repoSlug, '--limit', '20'], {
|
|
145
|
+
timeout: 20_000,
|
|
146
|
+
});
|
|
147
|
+
if (result.error) {
|
|
148
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} release list': ${result.error.message}`);
|
|
149
|
+
}
|
|
150
|
+
if (result.status !== 0) {
|
|
151
|
+
const details = (result.stderr || result.stdout || '').trim();
|
|
152
|
+
throw new Error(`Release blocked: unable to list GitHub releases.${details ? `\n${details}` : ''}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const tags = String(result.stdout || '')
|
|
156
|
+
.split('\n')
|
|
157
|
+
.map((line) => line.split('\t')[0].trim())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
|
|
160
|
+
return tags.find((tag) => tag !== currentTag) || '';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function selectReleaseEntriesForWindow(entries, currentTag, previousTag) {
|
|
164
|
+
const currentVersion = parseVersionString(currentTag);
|
|
165
|
+
if (!currentVersion) {
|
|
166
|
+
throw new Error(`Release blocked: invalid current version tag '${currentTag}'`);
|
|
167
|
+
}
|
|
168
|
+
const previousVersion = previousTag ? parseVersionString(previousTag) : null;
|
|
169
|
+
|
|
170
|
+
const selected = entries.filter((entry) => {
|
|
171
|
+
if (!entry.version) return false;
|
|
172
|
+
if (compareParsedVersions(entry.version, currentVersion) > 0) return false;
|
|
173
|
+
if (!previousVersion) return entry.tag === currentTag;
|
|
174
|
+
return compareParsedVersions(entry.version, previousVersion) > 0;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!selected.some((entry) => entry.tag === currentTag)) {
|
|
178
|
+
throw new Error(`Release blocked: README.md is missing release notes for ${currentTag}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return selected;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderGeneratedReleaseNotes(entries, currentTag, previousTag) {
|
|
185
|
+
const intro = previousTag ? `Changes since ${previousTag}.` : `Changes in ${currentTag}.`;
|
|
186
|
+
const sections = entries
|
|
187
|
+
.map((entry) => `### ${entry.tag}\n${entry.body}`)
|
|
188
|
+
.join('\n\n');
|
|
189
|
+
return `GitGuardex ${currentTag}\n\n${intro}\n\n${sections}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function describeGhAuthFailure(ghBin, authStatus) {
|
|
193
|
+
if (authStatus.error) {
|
|
194
|
+
return `unable to run '${ghBin} auth status': ${authStatus.error.message}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const authDetails = (authStatus.stderr || authStatus.stdout || '').trim();
|
|
198
|
+
const apiProbe = run(ghBin, ['api', 'user', '--jq', '.login'], { timeout: 20_000 });
|
|
199
|
+
if (apiProbe.status === 0) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const apiDetails = (apiProbe.stderr || apiProbe.stdout || apiProbe.error?.message || '').trim();
|
|
204
|
+
if (/error connecting to api\.github\.com|could not resolve host|failed to connect|network is unreachable|connection timed out|temporary failure in name resolution/i.test(apiDetails)) {
|
|
205
|
+
return `GitHub API is unreachable, so '${ghBin} auth status' cannot validate the stored token. This is a network or sandbox connectivity problem, not proof that the token is invalid.${apiDetails ? `\n${apiDetails}` : ''}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return `'${ghBin}' auth is unavailable.${authDetails ? `\n${authDetails}` : ''}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag) {
|
|
212
|
+
const readme = readRepoReadme(repoRoot);
|
|
213
|
+
const entries = parseReadmeReleaseEntries(readme);
|
|
214
|
+
const selected = selectReleaseEntriesForWindow(entries, currentTag, previousTag);
|
|
215
|
+
return renderGeneratedReleaseNotes(selected, currentTag, previousTag);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function release(rawArgs) {
|
|
219
|
+
if (rawArgs.length > 0) {
|
|
220
|
+
throw new Error(`Unknown option: ${rawArgs[0]}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const repoRoot = resolveRepoRoot(process.cwd());
|
|
224
|
+
if (path.resolve(repoRoot) !== MAINTAINER_RELEASE_REPO) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Release blocked: command only allowed in ${MAINTAINER_RELEASE_REPO} (current: ${repoRoot})`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
ensureMainBranch(repoRoot);
|
|
231
|
+
ensureCleanWorkingTree(repoRoot);
|
|
232
|
+
|
|
233
|
+
if (!isCommandAvailable(GH_BIN)) {
|
|
234
|
+
throw new Error(`Release blocked: '${GH_BIN}' is not available`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const ghAuthStatus = run(GH_BIN, ['auth', 'status'], { timeout: 20_000 });
|
|
238
|
+
if (ghAuthStatus.status !== 0) {
|
|
239
|
+
const ghAuthFailure = describeGhAuthFailure(GH_BIN, ghAuthStatus);
|
|
240
|
+
if (ghAuthFailure) {
|
|
241
|
+
throw new Error(`Release blocked: ${ghAuthFailure}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const releasePackageJson = readReleaseRepoPackageJson(repoRoot);
|
|
246
|
+
const repoSlug = resolveReleaseGithubRepo(repoRoot);
|
|
247
|
+
const currentTag = `v${releasePackageJson.version}`;
|
|
248
|
+
const previousTag = resolvePreviousPublishedReleaseTag(repoSlug, currentTag);
|
|
249
|
+
const notes = buildReleaseNotesFromReadme(repoRoot, currentTag, previousTag);
|
|
250
|
+
const headCommit = gitRun(repoRoot, ['rev-parse', 'HEAD']).stdout.trim();
|
|
251
|
+
|
|
252
|
+
const existingRelease = run(GH_BIN, ['release', 'view', currentTag, '--repo', repoSlug], {
|
|
253
|
+
timeout: 20_000,
|
|
254
|
+
});
|
|
255
|
+
if (existingRelease.error) {
|
|
256
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} release view': ${existingRelease.error.message}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const releaseArgs =
|
|
260
|
+
existingRelease.status === 0
|
|
261
|
+
? ['release', 'edit', currentTag, '--repo', repoSlug, '--title', currentTag, '--notes', notes]
|
|
262
|
+
: [
|
|
263
|
+
'release',
|
|
264
|
+
'create',
|
|
265
|
+
currentTag,
|
|
266
|
+
'--repo',
|
|
267
|
+
repoSlug,
|
|
268
|
+
'--target',
|
|
269
|
+
headCommit,
|
|
270
|
+
'--title',
|
|
271
|
+
currentTag,
|
|
272
|
+
'--notes',
|
|
273
|
+
notes,
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
console.log(
|
|
277
|
+
`[${TOOL_NAME}] ${existingRelease.status === 0 ? 'Updating' : 'Creating'} GitHub release ${currentTag} on ${repoSlug}`,
|
|
278
|
+
);
|
|
279
|
+
if (previousTag) {
|
|
280
|
+
console.log(`[${TOOL_NAME}] Aggregating README release notes newer than ${previousTag}.`);
|
|
281
|
+
} else {
|
|
282
|
+
console.log(`[${TOOL_NAME}] No earlier published GitHub release found; using only ${currentTag}.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const releaseResult = run(GH_BIN, releaseArgs, { cwd: repoRoot, timeout: 60_000 });
|
|
286
|
+
if (releaseResult.error) {
|
|
287
|
+
throw new Error(`Release blocked: unable to run '${GH_BIN} release': ${releaseResult.error.message}`);
|
|
288
|
+
}
|
|
289
|
+
if (releaseResult.status !== 0) {
|
|
290
|
+
const details = (releaseResult.stderr || releaseResult.stdout || '').trim();
|
|
291
|
+
throw new Error(`GitHub release command failed.${details ? `\n${details}` : ''}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const releaseUrl = String(releaseResult.stdout || '').trim();
|
|
295
|
+
if (releaseUrl) {
|
|
296
|
+
console.log(releaseUrl);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log(`[${TOOL_NAME}] ✅ GitHub release ${currentTag} is synced to the README history.`);
|
|
300
|
+
process.exitCode = 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
release,
|
|
305
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// `gx report` — OpenSSF Scorecard + session-severity reports. Pure
|
|
2
|
+
// code-motion from src/cli/main.js.
|
|
3
|
+
const {
|
|
4
|
+
fs,
|
|
5
|
+
path,
|
|
6
|
+
TOOL_NAME,
|
|
7
|
+
SCORECARD_BIN,
|
|
8
|
+
SCORECARD_RISK_BY_CHECK,
|
|
9
|
+
} = require('../../context');
|
|
10
|
+
const { resolveRepoRoot } = require('../../git');
|
|
11
|
+
const { run } = require('../../core/runtime');
|
|
12
|
+
const sessionSeverityReport = require('../../report/session-severity');
|
|
13
|
+
const { parseReportArgs } = require('../args');
|
|
14
|
+
const {
|
|
15
|
+
todayDateStamp,
|
|
16
|
+
inferGithubRepoFromOrigin,
|
|
17
|
+
} = require('../shared/repo-env');
|
|
18
|
+
|
|
19
|
+
function resolveScorecardRepo(repoRoot, explicitRepo) {
|
|
20
|
+
if (explicitRepo) {
|
|
21
|
+
return explicitRepo.trim();
|
|
22
|
+
}
|
|
23
|
+
const inferred = inferGithubRepoFromOrigin(repoRoot);
|
|
24
|
+
if (inferred) return inferred;
|
|
25
|
+
throw new Error(
|
|
26
|
+
'Unable to infer GitHub repo from origin remote. Pass --repo github.com/<owner>/<repo>.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runScorecardJson(repo) {
|
|
31
|
+
const result = run(SCORECARD_BIN, ['--repo', repo, '--format', 'json'], { allowFailure: true });
|
|
32
|
+
if (result.status !== 0) {
|
|
33
|
+
const details = (result.stderr || result.stdout || '').trim();
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Failed to run scorecard CLI ('${SCORECARD_BIN} --repo ${repo} --format json').${details ? `\n${details}` : ''}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(result.stdout || '{}');
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`Unable to parse scorecard JSON output: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readScorecardJsonFile(filePath) {
|
|
47
|
+
const absolute = path.resolve(filePath);
|
|
48
|
+
if (!fs.existsSync(absolute)) {
|
|
49
|
+
throw new Error(`scorecard JSON file not found: ${absolute}`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(absolute, 'utf8'));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`Unable to parse scorecard JSON file: ${error.message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeScorecardChecks(payload) {
|
|
59
|
+
const rawChecks = Array.isArray(payload?.checks) ? payload.checks : [];
|
|
60
|
+
return rawChecks.map((check) => {
|
|
61
|
+
const name = String(check?.name || 'Unknown');
|
|
62
|
+
const rawScore = Number(check?.score);
|
|
63
|
+
const score = Number.isFinite(rawScore) ? rawScore : 0;
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
score,
|
|
67
|
+
risk: SCORECARD_RISK_BY_CHECK[name] || 'Unknown',
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderScorecardBaselineMarkdown({ repo, score, checks, capturedAt, scorecardVersion, reportDate }) {
|
|
73
|
+
const rows = checks
|
|
74
|
+
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
75
|
+
.join('\n');
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
'# OpenSSF Scorecard Baseline Report',
|
|
79
|
+
'',
|
|
80
|
+
`- **Repository:** \`${repo}\``,
|
|
81
|
+
'- **Source:** generated by `gx report scorecard`',
|
|
82
|
+
`- **Captured at:** ${capturedAt}`,
|
|
83
|
+
`- **Scorecard version:** \`${scorecardVersion}\``,
|
|
84
|
+
`- **Overall score:** **${score} / 10**`,
|
|
85
|
+
'',
|
|
86
|
+
'## Check breakdown',
|
|
87
|
+
'',
|
|
88
|
+
'| Check | Score | Risk |',
|
|
89
|
+
'|---|---:|---|',
|
|
90
|
+
rows || '| (none) | 0 | Unknown |',
|
|
91
|
+
'',
|
|
92
|
+
`## Report date`,
|
|
93
|
+
'',
|
|
94
|
+
`- ${reportDate}`,
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderScorecardRemediationPlanMarkdown({ baselineRelativePath, checks }) {
|
|
100
|
+
const failing = checks.filter((item) => item.score < 10);
|
|
101
|
+
const failingRows = failing
|
|
102
|
+
.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name))
|
|
103
|
+
.map((item) => `| ${item.name} | ${item.score} | ${item.risk} |`)
|
|
104
|
+
.join('\n');
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
'# OpenSSF Scorecard Remediation Plan',
|
|
108
|
+
'',
|
|
109
|
+
`Based on baseline report: \`${baselineRelativePath}\`.`,
|
|
110
|
+
'',
|
|
111
|
+
'## Failing checks',
|
|
112
|
+
'',
|
|
113
|
+
'| Check | Score | Risk |',
|
|
114
|
+
'|---|---:|---|',
|
|
115
|
+
(failingRows || '| None | 10 | N/A |'),
|
|
116
|
+
'',
|
|
117
|
+
'## Priority order',
|
|
118
|
+
'',
|
|
119
|
+
'1. Fix **High** risk checks first (especially score 0 items).',
|
|
120
|
+
'2. Then close **Medium** risk checks with score < 10.',
|
|
121
|
+
'3. Finally address **Low** risk ecosystem/process checks.',
|
|
122
|
+
'',
|
|
123
|
+
'## Verification loop',
|
|
124
|
+
'',
|
|
125
|
+
'1. Run scorecard again.',
|
|
126
|
+
'2. Re-generate baseline + remediation files.',
|
|
127
|
+
'3. Compare score deltas and track improved checks.',
|
|
128
|
+
'',
|
|
129
|
+
].join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function report(rawArgs) {
|
|
133
|
+
const options = parseReportArgs(rawArgs);
|
|
134
|
+
const subcommand = options.subcommand || 'help';
|
|
135
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
136
|
+
const sessionSeverityHelpDetails = sessionSeverityReport.renderSessionSeverityHelpDetails()
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map((line) => ` ${line}`)
|
|
139
|
+
.join('\n');
|
|
140
|
+
console.log(
|
|
141
|
+
`${TOOL_NAME} report commands:\n` +
|
|
142
|
+
` ${TOOL_NAME} report scorecard [--target <path>] [--repo github.com/<owner>/<repo>] [--scorecard-json <file>] [--output-dir <path>] [--date YYYY-MM-DD] [--dry-run] [--json]\n` +
|
|
143
|
+
` ${sessionSeverityReport.renderSessionSeverityCommand(TOOL_NAME)}\n` +
|
|
144
|
+
`${sessionSeverityHelpDetails}\n` +
|
|
145
|
+
`\n` +
|
|
146
|
+
`Examples:\n` +
|
|
147
|
+
` ${TOOL_NAME} report scorecard --repo github.com/recodeecom/multiagent-safety\n` +
|
|
148
|
+
` ${TOOL_NAME} report scorecard --scorecard-json ./scorecard.json --date 2026-04-10\n` +
|
|
149
|
+
` ${sessionSeverityReport.renderSessionSeverityExample(TOOL_NAME)}`,
|
|
150
|
+
);
|
|
151
|
+
process.exitCode = 0;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (subcommand === 'session-severity') {
|
|
156
|
+
const payload = sessionSeverityReport.buildSessionSeverityReport(options);
|
|
157
|
+
if (options.json) {
|
|
158
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
159
|
+
process.exitCode = 0;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(sessionSeverityReport.renderSessionSeverityReport(payload));
|
|
163
|
+
process.exitCode = 0;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (subcommand !== 'scorecard') {
|
|
168
|
+
throw new Error(`Unknown report subcommand: ${subcommand}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
172
|
+
const repo = resolveScorecardRepo(repoRoot, options.repo);
|
|
173
|
+
const payload = options.scorecardJson
|
|
174
|
+
? readScorecardJsonFile(options.scorecardJson)
|
|
175
|
+
: runScorecardJson(repo);
|
|
176
|
+
|
|
177
|
+
const reportDate = options.date || todayDateStamp();
|
|
178
|
+
const outputDir = path.resolve(options.outputDir || path.join(repoRoot, 'docs', 'reports'));
|
|
179
|
+
const baselinePath = path.join(outputDir, `openssf-scorecard-baseline-${reportDate}.md`);
|
|
180
|
+
const remediationPath = path.join(outputDir, `openssf-scorecard-remediation-plan-${reportDate}.md`);
|
|
181
|
+
|
|
182
|
+
const checks = normalizeScorecardChecks(payload);
|
|
183
|
+
const rawScore = Number(payload?.score);
|
|
184
|
+
const score = Number.isFinite(rawScore) ? rawScore : 0;
|
|
185
|
+
const capturedAt = String(payload?.date || new Date().toISOString());
|
|
186
|
+
const scorecardVersion = String(payload?.scorecard?.version || payload?.version || 'unknown');
|
|
187
|
+
|
|
188
|
+
const baselineMarkdown = renderScorecardBaselineMarkdown({
|
|
189
|
+
repo,
|
|
190
|
+
score,
|
|
191
|
+
checks,
|
|
192
|
+
capturedAt,
|
|
193
|
+
scorecardVersion,
|
|
194
|
+
reportDate,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const remediationMarkdown = renderScorecardRemediationPlanMarkdown({
|
|
198
|
+
baselineRelativePath: path.relative(repoRoot, baselinePath) || path.basename(baselinePath),
|
|
199
|
+
checks,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!options.dryRun) {
|
|
203
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
204
|
+
fs.writeFileSync(baselinePath, baselineMarkdown, 'utf8');
|
|
205
|
+
fs.writeFileSync(remediationPath, remediationMarkdown, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (options.json) {
|
|
209
|
+
process.stdout.write(
|
|
210
|
+
JSON.stringify(
|
|
211
|
+
{
|
|
212
|
+
repoRoot,
|
|
213
|
+
repo,
|
|
214
|
+
score,
|
|
215
|
+
checks: checks.length,
|
|
216
|
+
outputDir,
|
|
217
|
+
baselinePath,
|
|
218
|
+
remediationPath,
|
|
219
|
+
dryRun: Boolean(options.dryRun),
|
|
220
|
+
},
|
|
221
|
+
null,
|
|
222
|
+
2,
|
|
223
|
+
) + '\n',
|
|
224
|
+
);
|
|
225
|
+
process.exitCode = 0;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(`[${TOOL_NAME}] Report target: ${repoRoot}`);
|
|
230
|
+
console.log(`[${TOOL_NAME}] Scorecard repo: ${repo}`);
|
|
231
|
+
console.log(`[${TOOL_NAME}] Score: ${score}/10`);
|
|
232
|
+
if (options.dryRun) {
|
|
233
|
+
console.log(`[${TOOL_NAME}] Dry run report paths:`);
|
|
234
|
+
} else {
|
|
235
|
+
console.log(`[${TOOL_NAME}] Generated reports:`);
|
|
236
|
+
}
|
|
237
|
+
console.log(` - ${baselinePath}`);
|
|
238
|
+
console.log(` - ${remediationPath}`);
|
|
239
|
+
process.exitCode = 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = {
|
|
243
|
+
report,
|
|
244
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// `gx review` (deprecated) and `gx pr-review`. Pure code-motion from
|
|
2
|
+
// src/cli/main.js.
|
|
3
|
+
const { resolveRepoRoot } = require('../../git');
|
|
4
|
+
const { runReviewBotCommand } = require('../../core/runtime');
|
|
5
|
+
const prReviewModule = require('../../pr-review');
|
|
6
|
+
const { parseReviewArgs, parsePrReviewArgs } = require('../args');
|
|
7
|
+
const { isSpawnFailure } = require('../shared/sandbox');
|
|
8
|
+
|
|
9
|
+
function review(rawArgs) {
|
|
10
|
+
const options = parseReviewArgs(rawArgs);
|
|
11
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
12
|
+
const result = runReviewBotCommand(repoRoot, options.passthroughArgs);
|
|
13
|
+
if (isSpawnFailure(result)) {
|
|
14
|
+
throw result.error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
18
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
19
|
+
process.exitCode = typeof result.status === 'number' ? result.status : 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function prReview(rawArgs) {
|
|
23
|
+
const options = parsePrReviewArgs(rawArgs);
|
|
24
|
+
const result = prReviewModule.runPrReview(options);
|
|
25
|
+
prReviewModule.printPrReviewResult(result);
|
|
26
|
+
process.exitCode = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
review,
|
|
31
|
+
prReview,
|
|
32
|
+
};
|