clud-bug 0.6.34 → 0.7.0-rc.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/bin/clud-bug.js +10 -1353
- package/dist/cli/agents-md.d.ts +16 -0
- package/dist/cli/agents-md.d.ts.map +1 -0
- package/dist/cli/agents-md.js +226 -0
- package/dist/cli/agents-md.js.map +1 -0
- package/dist/cli/audit.d.ts +13 -0
- package/dist/cli/audit.d.ts.map +1 -0
- package/dist/cli/audit.js +90 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/branch-protection.d.ts +57 -0
- package/dist/cli/branch-protection.d.ts.map +1 -0
- package/dist/cli/branch-protection.js +118 -0
- package/dist/cli/branch-protection.js.map +1 -0
- package/dist/cli/edit-workflow.d.ts +18 -0
- package/dist/cli/edit-workflow.d.ts.map +1 -0
- package/dist/cli/edit-workflow.js +43 -0
- package/dist/cli/edit-workflow.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +1336 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/skill-usage.d.ts +109 -0
- package/dist/cli/skill-usage.d.ts.map +1 -0
- package/dist/cli/skill-usage.js +380 -0
- package/dist/cli/skill-usage.js.map +1 -0
- package/dist/cli/skills.d.ts +56 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +292 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/update.d.ts +29 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +186 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/usage.d.ts +142 -0
- package/dist/cli/usage.d.ts.map +1 -0
- package/dist/cli/usage.js +348 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/core/audit.d.ts +8 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +47 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/detect.d.ts +77 -0
- package/dist/core/detect.d.ts.map +1 -0
- package/dist/core/detect.js +262 -0
- package/dist/core/detect.js.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/prompts.d.ts +9 -0
- package/dist/core/prompts.d.ts.map +1 -0
- package/dist/core/prompts.js +401 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/render-review.d.ts +6 -0
- package/dist/core/render-review.d.ts.map +1 -0
- package/dist/core/render-review.js +219 -0
- package/dist/core/render-review.js.map +1 -0
- package/dist/core/render.d.ts +13 -0
- package/dist/core/render.d.ts.map +1 -0
- package/dist/core/render.js +80 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/review-schema.d.ts +42 -0
- package/dist/core/review-schema.d.ts.map +1 -0
- package/dist/core/review-schema.js +156 -0
- package/dist/core/review-schema.js.map +1 -0
- package/dist/core/skills.d.ts +80 -0
- package/dist/core/skills.d.ts.map +1 -0
- package/dist/core/skills.js +510 -0
- package/dist/core/skills.js.map +1 -0
- package/package.json +27 -4
- package/{lib/agents-md.js → src/cli/agents-md.ts} +25 -14
- package/{lib/audit.js → src/cli/audit.ts} +37 -44
- package/{lib/branch-protection.js → src/cli/branch-protection.ts} +75 -11
- package/{lib/edit-workflow.js → src/cli/edit-workflow.ts} +32 -11
- package/src/cli/index.ts +101 -0
- package/src/cli/main.ts +1376 -0
- package/{lib/skill-usage.js → src/cli/skill-usage.ts} +168 -94
- package/src/cli/skills.ts +386 -0
- package/{lib/update.js → src/cli/update.ts} +68 -27
- package/{lib/usage.js → src/cli/usage.ts} +167 -76
- package/src/core/audit.ts +53 -0
- package/{lib/detect.js → src/core/detect.ts} +100 -47
- package/src/core/index.ts +70 -0
- package/{lib/prompts.js → src/core/prompts.ts} +16 -2
- package/{lib/render-review.js → src/core/render-review.ts} +57 -25
- package/{lib/render.js → src/core/render.ts} +36 -10
- package/{lib/review-schema.js → src/core/review-schema.ts} +68 -5
- package/{lib/skills.js → src/core/skills.ts} +172 -343
- package/templates/workflow-py.yml.tmpl +2 -2
- package/templates/workflow-ts.yml.tmpl +2 -2
- package/templates/workflow.yml.tmpl +17 -8
|
@@ -31,6 +31,12 @@ const TOUCH_IF_PRESENT = [
|
|
|
31
31
|
'.continuerules',
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
export interface RenderBlockOptions {
|
|
35
|
+
version?: string | undefined;
|
|
36
|
+
strictMode?: boolean | undefined;
|
|
37
|
+
skillRelPath?: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
// Render the clud-bug block. Bundled here rather than in a template file so
|
|
35
41
|
// updates ship with the CLI itself.
|
|
36
42
|
//
|
|
@@ -52,7 +58,7 @@ const TOUCH_IF_PRESENT = [
|
|
|
52
58
|
// path `.claude/skills/...`), render the LOCAL repo path. Otherwise the
|
|
53
59
|
// link is dead in the publisher repo (this used to require a manual fix
|
|
54
60
|
// every v0.6.* propagation cycle on agent-skills).
|
|
55
|
-
export function renderBlock({ version, strictMode, skillRelPath } = {}) {
|
|
61
|
+
export function renderBlock({ version, strictMode, skillRelPath }: RenderBlockOptions = {}): string {
|
|
56
62
|
const versionLine = version ? `_Installed at clud-bug v${version}._` : '';
|
|
57
63
|
const strictNote = strictMode === true
|
|
58
64
|
? '**on** in this repo (workflow check fails on critical findings)'
|
|
@@ -85,7 +91,7 @@ ${END_MARKER}`;
|
|
|
85
91
|
// in the working tree, the AGENTS.md link should point there, not at the
|
|
86
92
|
// consumer-install `.claude/skills/...` path that doesn't exist in the
|
|
87
93
|
// publisher repo.
|
|
88
|
-
export async function detectSkillRelPath(cwd) {
|
|
94
|
+
export async function detectSkillRelPath(cwd: string): Promise<string> {
|
|
89
95
|
const publisherPath = 'skills/clud-bug-collaboration/SKILL.md';
|
|
90
96
|
const consumerPath = '.claude/skills/clud-bug-collaboration/SKILL.md';
|
|
91
97
|
if (await fileExists(join(cwd, publisherPath))) return publisherPath;
|
|
@@ -94,7 +100,7 @@ export async function detectSkillRelPath(cwd) {
|
|
|
94
100
|
|
|
95
101
|
// Replace an existing clud-bug block in `content`, OR append if absent.
|
|
96
102
|
// Idempotent: running multiple times leaves a single block.
|
|
97
|
-
export function upsertBlock(content, block) {
|
|
103
|
+
export function upsertBlock(content: string, block: string): string {
|
|
98
104
|
const startRe = new RegExp(escapeRe(START_MARKER));
|
|
99
105
|
const endRe = new RegExp(escapeRe(END_MARKER));
|
|
100
106
|
if (startRe.test(content) && endRe.test(content)) {
|
|
@@ -113,7 +119,7 @@ export function upsertBlock(content, block) {
|
|
|
113
119
|
// Matches at start-of-line (a literal `@AGENTS.md` mentioned in prose
|
|
114
120
|
// won't fire; only the import directive does). Allows trailing space
|
|
115
121
|
// (some editors trim it; some don't) and optional newline terminator.
|
|
116
|
-
export function hasAgentsMdImport(content) {
|
|
122
|
+
export function hasAgentsMdImport(content: unknown): boolean {
|
|
117
123
|
if (typeof content !== 'string') return false;
|
|
118
124
|
return /^@AGENTS\.md\s*$/m.test(content);
|
|
119
125
|
}
|
|
@@ -125,7 +131,7 @@ export function hasAgentsMdImport(content) {
|
|
|
125
131
|
//
|
|
126
132
|
// Returns the cleaned content. If no block exists, returns content
|
|
127
133
|
// unchanged. Idempotent.
|
|
128
|
-
export function removeBlock(content) {
|
|
134
|
+
export function removeBlock(content: string): string {
|
|
129
135
|
if (typeof content !== 'string') return content;
|
|
130
136
|
// Strip the block. Match \n+ before the marker so we also eat the
|
|
131
137
|
// preceding blank line — otherwise we'd leave a "stub line + blank
|
|
@@ -136,25 +142,30 @@ export function removeBlock(content) {
|
|
|
136
142
|
return content.replace(re, '');
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
function escapeRe(s) {
|
|
145
|
+
function escapeRe(s: string): string {
|
|
140
146
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
141
147
|
}
|
|
142
148
|
|
|
149
|
+
export interface ApplyToRepoResult {
|
|
150
|
+
touched: string[];
|
|
151
|
+
created: string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
143
154
|
// Touches all relevant agent-instruction files in `cwd`.
|
|
144
155
|
// Creates AGENTS.md if it doesn't exist (it's the canonical home).
|
|
145
156
|
// Updates other files only if they already exist (don't proliferate stubs;
|
|
146
157
|
// logmind or the user owns those creation decisions).
|
|
147
158
|
//
|
|
148
159
|
// Returns { touched: string[], created: string[] } for the caller to log.
|
|
149
|
-
export async function applyToRepo(cwd, blockOpts = {}) {
|
|
160
|
+
export async function applyToRepo(cwd: string, blockOpts: RenderBlockOptions = {}): Promise<ApplyToRepoResult> {
|
|
150
161
|
// v0.6.25 / gotcha #2: detect publisher repo + render local skill path.
|
|
151
162
|
// Pre-v0.6.25 always rendered the consumer install path → broke
|
|
152
163
|
// agent-skills' check-links every propagation cycle. Detection runs
|
|
153
164
|
// before block render so the path is correct from the first write.
|
|
154
165
|
const skillRelPath = blockOpts.skillRelPath ?? await detectSkillRelPath(cwd);
|
|
155
166
|
const block = renderBlock({ ...blockOpts, skillRelPath });
|
|
156
|
-
const touched = [];
|
|
157
|
-
const created = [];
|
|
167
|
+
const touched: string[] = [];
|
|
168
|
+
const created: string[] = [];
|
|
158
169
|
|
|
159
170
|
for (const path of ALWAYS_TOUCH) {
|
|
160
171
|
const full = join(cwd, path);
|
|
@@ -181,8 +192,8 @@ export async function applyToRepo(cwd, blockOpts = {}) {
|
|
|
181
192
|
// .cursor/rules/*.md — append to every file that exists.
|
|
182
193
|
const cursorRulesDir = join(cwd, '.cursor', 'rules');
|
|
183
194
|
if (await fileExists(cursorRulesDir)) {
|
|
184
|
-
let entries = [];
|
|
185
|
-
try { entries = await readdir(cursorRulesDir); } catch {}
|
|
195
|
+
let entries: string[] = [];
|
|
196
|
+
try { entries = await readdir(cursorRulesDir); } catch { /* dir missing or unreadable */ }
|
|
186
197
|
for (const name of entries) {
|
|
187
198
|
if (!name.endsWith('.md')) continue;
|
|
188
199
|
const full = join(cursorRulesDir, name);
|
|
@@ -206,11 +217,11 @@ export async function applyToRepo(cwd, blockOpts = {}) {
|
|
|
206
217
|
//
|
|
207
218
|
// AGENTS.md itself is NOT routed through here — it always gets the
|
|
208
219
|
// block (it's the canonical source).
|
|
209
|
-
function nextContentFor(prior, block) {
|
|
220
|
+
function nextContentFor(prior: string, block: string): string {
|
|
210
221
|
return hasAgentsMdImport(prior) ? removeBlock(prior) : upsertBlock(prior, block);
|
|
211
222
|
}
|
|
212
223
|
|
|
213
|
-
function seedFile(name) {
|
|
224
|
+
function seedFile(name: string): string {
|
|
214
225
|
// When AGENTS.md doesn't exist (no logmind, no prior tooling), seed with a
|
|
215
226
|
// minimal canonical header so the clud-bug block has context.
|
|
216
227
|
if (name === 'AGENTS.md') {
|
|
@@ -225,6 +236,6 @@ Claude Code, Cline, Continue, Aider, ...) read it directly.
|
|
|
225
236
|
return '';
|
|
226
237
|
}
|
|
227
238
|
|
|
228
|
-
async function fileExists(path) {
|
|
239
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
229
240
|
try { await stat(path); return true; } catch { return false; }
|
|
230
241
|
}
|
|
@@ -1,37 +1,55 @@
|
|
|
1
|
+
// CLI audit helpers — these shell out to git and walk the working tree.
|
|
2
|
+
//
|
|
3
|
+
// Split from lib/audit.js during the v0.7.0 TS migration. Pure helpers
|
|
4
|
+
// (durationToGitSince, renderAuditHeader) live in src/core/audit.ts so the
|
|
5
|
+
// App-side (clud-bug-app) can consume them without dragging child_process
|
|
6
|
+
// in. computeAuditFileSet stays CLI-only — it is only meaningful when run
|
|
7
|
+
// in a checked-out repo.
|
|
8
|
+
|
|
1
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { durationToGitSince } from '../core/audit.js';
|
|
2
11
|
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
throw new Error(`Unrecognized duration "${input}". Examples: 7d, 2w, 1mo, 1y.`);
|
|
10
|
-
}
|
|
11
|
-
const n = Number(m[1]);
|
|
12
|
-
const unit = m[2].toLowerCase();
|
|
13
|
-
const map = { d: 'day', w: 'week', mo: 'month', m: 'month', y: 'year' };
|
|
14
|
-
return `${n} ${map[unit]}${n === 1 ? '' : 's'} ago`;
|
|
12
|
+
export interface GitLinesOptions {
|
|
13
|
+
// `cwd?: string | undefined` (vs `cwd?: string`) is required by
|
|
14
|
+
// exactOptionalPropertyTypes: true — callers freely pass an inline
|
|
15
|
+
// object that may have `cwd` typed as `string | undefined`.
|
|
16
|
+
cwd?: string | undefined;
|
|
17
|
+
allowFail?: boolean | undefined;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
// Run a git command, return stdout lines split by \n. Throws on non-zero exit
|
|
18
21
|
// unless { allowFail: true }, in which case returns [].
|
|
19
|
-
|
|
22
|
+
// Note: under noUncheckedIndexedAccess: true, array element reads are typed
|
|
23
|
+
// `string | undefined`. The return type stays `string[]` (we filter falsy
|
|
24
|
+
// strings out), but callers indexing into the result must keep that in mind.
|
|
25
|
+
export function gitLines(args: string[], opts: GitLinesOptions = {}): string[] {
|
|
20
26
|
const r = spawnSync('git', args, { encoding: 'utf8', cwd: opts.cwd || process.cwd() });
|
|
21
27
|
if (r.status !== 0) {
|
|
22
28
|
if (opts.allowFail) return [];
|
|
23
|
-
|
|
29
|
+
// spawnSync with encoding:'utf8' makes stderr `string | null`; the null
|
|
30
|
+
// path only happens when the child can't be spawned at all (a different
|
|
31
|
+
// error path). When status is non-zero we have stderr.
|
|
32
|
+
const stderr = (r.stderr ?? '').toString().trim();
|
|
33
|
+
throw new Error(`git ${args.join(' ')} failed (${r.status}): ${stderr}`);
|
|
24
34
|
}
|
|
25
|
-
|
|
35
|
+
const stdout = (r.stdout ?? '').toString();
|
|
36
|
+
return stdout.split('\n').filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AuditFileSetOptions {
|
|
40
|
+
since?: string | null;
|
|
41
|
+
changedIn?: string | null;
|
|
42
|
+
scopes?: string[];
|
|
43
|
+
cwd?: string;
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
// Returns the file set the audit should consider, in repo-relative paths.
|
|
29
47
|
// Filters: optional --since (git date), optional --changed-in (duration string),
|
|
30
48
|
// optional --scope globs (one or more, repeatable).
|
|
31
|
-
export function computeAuditFileSet({ since, changedIn, scopes = [], cwd } = {}) {
|
|
49
|
+
export function computeAuditFileSet({ since, changedIn, scopes = [], cwd }: AuditFileSetOptions = {}): string[] {
|
|
32
50
|
const sinceArg = since || (changedIn ? durationToGitSince(changedIn) : null);
|
|
33
51
|
|
|
34
|
-
let files;
|
|
52
|
+
let files: string[];
|
|
35
53
|
if (sinceArg) {
|
|
36
54
|
// Files touched in any commit within the window.
|
|
37
55
|
files = [...new Set(gitLines(['log', `--since=${sinceArg}`, '--name-only', '--pretty=format:'], { cwd }))];
|
|
@@ -57,7 +75,7 @@ export function computeAuditFileSet({ since, changedIn, scopes = [], cwd } = {})
|
|
|
57
75
|
|
|
58
76
|
// Minimal glob → RegExp. Supports **, *, ?. Anchors at both ends so that
|
|
59
77
|
// 'src/**/*.ts' matches 'src/lib/foo.ts' but not 'app/src/lib/foo.ts'.
|
|
60
|
-
function globToRegex(glob) {
|
|
78
|
+
function globToRegex(glob: string): RegExp {
|
|
61
79
|
let rx = '';
|
|
62
80
|
let i = 0;
|
|
63
81
|
while (i < glob.length) {
|
|
@@ -74,7 +92,7 @@ function globToRegex(glob) {
|
|
|
74
92
|
} else if (ch === '?') {
|
|
75
93
|
rx += '[^/]';
|
|
76
94
|
i++;
|
|
77
|
-
} else if (/[.+^$|()\[\]{}\\]/.test(ch)) {
|
|
95
|
+
} else if (ch !== undefined && /[.+^$|()\[\]{}\\]/.test(ch)) {
|
|
78
96
|
rx += '\\' + ch;
|
|
79
97
|
i++;
|
|
80
98
|
} else {
|
|
@@ -84,28 +102,3 @@ function globToRegex(glob) {
|
|
|
84
102
|
}
|
|
85
103
|
return new RegExp(`^${rx}$`);
|
|
86
104
|
}
|
|
87
|
-
|
|
88
|
-
// Render the audit report's initial markdown body. The Action's Claude run
|
|
89
|
-
// will append findings under a "## Findings" section after this header.
|
|
90
|
-
export function renderAuditHeader({ date, scopeLabel, files }) {
|
|
91
|
-
const head = `# 🐛 Clud Bug audit — ${date}
|
|
92
|
-
|
|
93
|
-
A scheduled walk through the habitat. Scope: ${scopeLabel}.
|
|
94
|
-
Files surveyed: **${files.length}**.
|
|
95
|
-
|
|
96
|
-
<details>
|
|
97
|
-
<summary>File manifest (${files.length})</summary>
|
|
98
|
-
|
|
99
|
-
\`\`\`
|
|
100
|
-
${files.join('\n')}
|
|
101
|
-
\`\`\`
|
|
102
|
-
|
|
103
|
-
</details>
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## Findings
|
|
108
|
-
|
|
109
|
-
`;
|
|
110
|
-
return head;
|
|
111
|
-
}
|
|
@@ -21,35 +21,70 @@
|
|
|
21
21
|
|
|
22
22
|
import { spawn } from 'node:child_process';
|
|
23
23
|
|
|
24
|
+
export interface GhResult {
|
|
25
|
+
code: number | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GhOptions {
|
|
31
|
+
stdin?: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pluggable invoker shape — the CLI uses `defaultGh`, tests pass a mock.
|
|
35
|
+
export type GhInvoker = (args: string[], opts?: GhOptions) => Promise<GhResult>;
|
|
36
|
+
|
|
24
37
|
// Default `gh` invoker: spawns `gh <args>` and resolves with
|
|
25
38
|
// { code, stdout, stderr }. Tests pass a function with the same shape.
|
|
26
|
-
function defaultGh(args,
|
|
27
|
-
|
|
39
|
+
function defaultGh(args: string[], opts: GhOptions = {}): Promise<GhResult> {
|
|
40
|
+
const { stdin } = opts;
|
|
41
|
+
return new Promise<GhResult>((resolve, reject) => {
|
|
28
42
|
const child = spawn('gh', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
29
43
|
let stdout = '';
|
|
30
44
|
let stderr = '';
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
// Under strict typing, spawn() can return null for stdio pipes when
|
|
46
|
+
// stdio is configured to ignore the stream. We requested 'pipe' for
|
|
47
|
+
// all three so the streams are guaranteed; the `!` non-null asserts
|
|
48
|
+
// make this explicit and keep the runtime semantics identical.
|
|
49
|
+
child.stdout!.on('data', (d: Buffer | string) => { stdout += d; });
|
|
50
|
+
child.stderr!.on('data', (d: Buffer | string) => { stderr += d; });
|
|
33
51
|
child.on('error', reject);
|
|
34
52
|
child.on('close', (code) => resolve({ code, stdout, stderr }));
|
|
35
|
-
if (stdin) child.stdin
|
|
36
|
-
else child.stdin
|
|
53
|
+
if (stdin) child.stdin!.end(stdin);
|
|
54
|
+
else child.stdin!.end();
|
|
37
55
|
});
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
export interface DetectRepoOptions {
|
|
59
|
+
gh?: GhInvoker | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DetectedRepo {
|
|
63
|
+
owner: string;
|
|
64
|
+
repo: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
// Returns { owner, repo } from the local git remote. Uses
|
|
41
68
|
// `gh repo view --json owner,name` so it doesn't depend on parsing URLs.
|
|
42
|
-
export async function detectRepo({ gh = defaultGh } = {}) {
|
|
69
|
+
export async function detectRepo({ gh = defaultGh }: DetectRepoOptions = {}): Promise<DetectedRepo> {
|
|
43
70
|
const { code, stdout, stderr } = await gh(['repo', 'view', '--json', 'owner,name']);
|
|
44
71
|
if (code !== 0) {
|
|
45
72
|
throw new Error(`gh repo view failed (${code}): ${stderr.trim() || '(no stderr)'}`);
|
|
46
73
|
}
|
|
47
|
-
const parsed = JSON.parse(stdout);
|
|
74
|
+
const parsed = JSON.parse(stdout) as { owner: { login: string }; name: string };
|
|
48
75
|
return { owner: parsed.owner.login, repo: parsed.name };
|
|
49
76
|
}
|
|
50
77
|
|
|
78
|
+
export interface DetectDefaultBranchOptions {
|
|
79
|
+
owner: string;
|
|
80
|
+
repo: string;
|
|
81
|
+
gh?: GhInvoker | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
// Returns the default branch name (e.g. "main", "master", "trunk").
|
|
52
|
-
export async function detectDefaultBranch(
|
|
85
|
+
export async function detectDefaultBranch(
|
|
86
|
+
{ owner, repo, gh = defaultGh }: DetectDefaultBranchOptions,
|
|
87
|
+
): Promise<string> {
|
|
53
88
|
const { code, stdout, stderr } = await gh(['api', `repos/${owner}/${repo}`, '--jq', '.default_branch']);
|
|
54
89
|
if (code !== 0) {
|
|
55
90
|
throw new Error(`Could not read default_branch for ${owner}/${repo}: ${stderr.trim() || stdout.trim()}`);
|
|
@@ -57,6 +92,20 @@ export async function detectDefaultBranch({ owner, repo, gh = defaultGh } = {})
|
|
|
57
92
|
return stdout.trim();
|
|
58
93
|
}
|
|
59
94
|
|
|
95
|
+
export type ProtectionState =
|
|
96
|
+
| { state: 'enabled' }
|
|
97
|
+
| { state: 'disabled' }
|
|
98
|
+
| { state: 'no-protection' }
|
|
99
|
+
| { state: 'forbidden' }
|
|
100
|
+
| { state: 'unknown'; reason: string };
|
|
101
|
+
|
|
102
|
+
export interface GetProtectionStateOptions {
|
|
103
|
+
owner: string;
|
|
104
|
+
repo: string;
|
|
105
|
+
branch: string;
|
|
106
|
+
gh?: GhInvoker | undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
60
109
|
// Inspects the current required_conversation_resolution state. Returns
|
|
61
110
|
// one of:
|
|
62
111
|
// { state: 'enabled' }
|
|
@@ -68,7 +117,9 @@ export async function detectDefaultBranch({ owner, repo, gh = defaultGh } = {})
|
|
|
68
117
|
// The reason this returns a discriminated union rather than throwing is
|
|
69
118
|
// that runInit decides what to do based on the state: each value above
|
|
70
119
|
// has a different user-facing message and follow-up action.
|
|
71
|
-
export async function getProtectionState(
|
|
120
|
+
export async function getProtectionState(
|
|
121
|
+
{ owner, repo, branch, gh = defaultGh }: GetProtectionStateOptions,
|
|
122
|
+
): Promise<ProtectionState> {
|
|
72
123
|
const { code, stdout, stderr } = await gh([
|
|
73
124
|
'api',
|
|
74
125
|
`repos/${owner}/${repo}/branches/${branch}/protection`,
|
|
@@ -89,11 +140,24 @@ export async function getProtectionState({ owner, repo, branch, gh = defaultGh }
|
|
|
89
140
|
return { state: 'unknown', reason: stderr.trim() || stdout.trim() || `gh exited ${code}` };
|
|
90
141
|
}
|
|
91
142
|
|
|
143
|
+
export interface EnableConversationResolutionOptions {
|
|
144
|
+
owner: string;
|
|
145
|
+
repo: string;
|
|
146
|
+
branch: string;
|
|
147
|
+
gh?: GhInvoker | undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type EnableResult =
|
|
151
|
+
| { ok: true }
|
|
152
|
+
| { ok: false; state: 'no-protection' | 'forbidden' | 'unknown'; reason: string };
|
|
153
|
+
|
|
92
154
|
// Enables the single flag via the dedicated endpoint. Doesn't touch any
|
|
93
155
|
// other protection settings. Returns { ok: true } on success or
|
|
94
156
|
// { ok: false, state, reason } using the same state taxonomy as
|
|
95
157
|
// getProtectionState() so callers can produce a consistent message.
|
|
96
|
-
export async function enableConversationResolution(
|
|
158
|
+
export async function enableConversationResolution(
|
|
159
|
+
{ owner, repo, branch, gh = defaultGh }: EnableConversationResolutionOptions,
|
|
160
|
+
): Promise<EnableResult> {
|
|
97
161
|
const { code, stdout, stderr } = await gh([
|
|
98
162
|
'api', '-X', 'POST',
|
|
99
163
|
`repos/${owner}/${repo}/branches/${branch}/protection/required_conversation_resolution`,
|
|
@@ -7,41 +7,62 @@ import { spawnSync } from 'node:child_process';
|
|
|
7
7
|
// so claude-code-action's workflow-self-mod guard doesn't bundle them
|
|
8
8
|
// with other reviewable work.
|
|
9
9
|
|
|
10
|
-
export
|
|
10
|
+
export interface PendingWorkflowEdits {
|
|
11
|
+
allWorkflow: boolean;
|
|
12
|
+
files: string[];
|
|
13
|
+
nonWorkflow: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPendingWorkflowEdits(cwd: string = process.cwd()): PendingWorkflowEdits {
|
|
11
17
|
// --untracked-files=all so new files in new directories are reported as
|
|
12
18
|
// individual paths instead of being collapsed to the parent dir (default
|
|
13
19
|
// 'normal' mode would emit '.github/' for a brand-new clud-bug-review.yml).
|
|
14
20
|
const r = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd, encoding: 'utf8' });
|
|
15
21
|
if (r.status !== 0) {
|
|
16
|
-
|
|
22
|
+
// r.stderr is `string | null` under strict; safe to coalesce to '' for the
|
|
23
|
+
// error message — null here means the child failed to spawn entirely.
|
|
24
|
+
throw new Error(`git status failed: ${(r.stderr ?? '').trim()}`);
|
|
17
25
|
}
|
|
18
|
-
const
|
|
26
|
+
const stdout = r.stdout ?? '';
|
|
27
|
+
const lines = stdout.split('\n').filter(Boolean);
|
|
19
28
|
if (lines.length === 0) return { allWorkflow: true, files: [], nonWorkflow: [] };
|
|
20
29
|
|
|
21
|
-
const files = [];
|
|
22
|
-
const nonWorkflow = [];
|
|
30
|
+
const files: string[] = [];
|
|
31
|
+
const nonWorkflow: string[] = [];
|
|
23
32
|
for (const line of lines) {
|
|
24
33
|
// porcelain format: XY <space> <path> ; rename: XY old -> new
|
|
25
|
-
|
|
34
|
+
// .pop() on a non-empty split always yields a string here, but
|
|
35
|
+
// noUncheckedIndexedAccess makes TS pessimistic — coalesce + trim.
|
|
36
|
+
const path = (line.slice(3).split(' -> ').pop() ?? '').trim();
|
|
26
37
|
files.push(path);
|
|
27
38
|
if (!isWorkflowFile(path)) nonWorkflow.push(path);
|
|
28
39
|
}
|
|
29
40
|
return { allWorkflow: nonWorkflow.length === 0, files, nonWorkflow };
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
export function isWorkflowFile(path) {
|
|
43
|
+
export function isWorkflowFile(path: string): boolean {
|
|
33
44
|
return /^\.github\/workflows\/clud-bug-.*\.ya?ml$/.test(path);
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
export function makeBranchName(date = new Date()) {
|
|
47
|
+
export function makeBranchName(date: Date = new Date()): string {
|
|
37
48
|
const stamp = date.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
38
49
|
return `clud-bug/edit-workflow-${stamp}`;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
export
|
|
52
|
+
export interface GitOptions {
|
|
53
|
+
allowFail?: boolean | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface GitResult {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
stdout: string;
|
|
59
|
+
stderr: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function git(cwd: string, args: string[], opts: GitOptions = {}): GitResult {
|
|
42
63
|
const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
43
64
|
if (r.status !== 0 && !opts.allowFail) {
|
|
44
|
-
throw new Error(`git ${args.join(' ')} failed (${r.status}): ${r.stderr.trim()}`);
|
|
65
|
+
throw new Error(`git ${args.join(' ')} failed (${r.status}): ${(r.stderr ?? '').trim()}`);
|
|
45
66
|
}
|
|
46
|
-
return { ok: r.status === 0, stdout: r.stdout.trim(), stderr: r.stderr.trim() };
|
|
67
|
+
return { ok: r.status === 0, stdout: (r.stdout ?? '').trim(), stderr: (r.stderr ?? '').trim() };
|
|
47
68
|
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Public API surface for `clud-bug` CLI helpers (consumed via the package's
|
|
2
|
+
// `.` exports map → `dist/cli/index.js`).
|
|
3
|
+
//
|
|
4
|
+
// Each line re-exports one CLI module's public symbols. Modules are
|
|
5
|
+
// added incrementally as the v0.7.0 TypeScript migration converts each
|
|
6
|
+
// lib/* JS file.
|
|
7
|
+
|
|
8
|
+
// The top-level dispatcher main() lives in main.ts; bin/clud-bug.js
|
|
9
|
+
// imports it through this barrel and runs it. Keeping main() out of
|
|
10
|
+
// this file avoids accidental side effects when consumers
|
|
11
|
+
// `import { ... } from 'clud-bug'`.
|
|
12
|
+
export { main } from './main.js';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
detectRepo,
|
|
16
|
+
detectDefaultBranch,
|
|
17
|
+
getProtectionState,
|
|
18
|
+
enableConversationResolution,
|
|
19
|
+
type GhResult,
|
|
20
|
+
type GhOptions,
|
|
21
|
+
type GhInvoker,
|
|
22
|
+
type DetectRepoOptions,
|
|
23
|
+
type DetectedRepo,
|
|
24
|
+
type DetectDefaultBranchOptions,
|
|
25
|
+
type ProtectionState,
|
|
26
|
+
type GetProtectionStateOptions,
|
|
27
|
+
type EnableConversationResolutionOptions,
|
|
28
|
+
type EnableResult,
|
|
29
|
+
} from './branch-protection.js';
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
getPendingWorkflowEdits,
|
|
33
|
+
isWorkflowFile,
|
|
34
|
+
makeBranchName,
|
|
35
|
+
git,
|
|
36
|
+
type PendingWorkflowEdits,
|
|
37
|
+
type GitOptions,
|
|
38
|
+
type GitResult,
|
|
39
|
+
} from './edit-workflow.js';
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
renderBlock,
|
|
43
|
+
detectSkillRelPath,
|
|
44
|
+
upsertBlock,
|
|
45
|
+
hasAgentsMdImport,
|
|
46
|
+
removeBlock,
|
|
47
|
+
applyToRepo,
|
|
48
|
+
type RenderBlockOptions,
|
|
49
|
+
type ApplyToRepoResult,
|
|
50
|
+
} from './agents-md.js';
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
computeSkillUsageDelta,
|
|
54
|
+
mergeSkillUsage,
|
|
55
|
+
assessSkillHealth,
|
|
56
|
+
formatHealthDashboard,
|
|
57
|
+
DEFAULT_GH_RUNNER,
|
|
58
|
+
fetchUsageArtifacts,
|
|
59
|
+
aggregateUsageStream,
|
|
60
|
+
type SkillDelta,
|
|
61
|
+
type SkillUsageEntry,
|
|
62
|
+
type SkillDeltaMap,
|
|
63
|
+
type SkillUsageMap,
|
|
64
|
+
type SkillHealthStatus,
|
|
65
|
+
type SkillHealthRow,
|
|
66
|
+
type GhRunResult,
|
|
67
|
+
type GhRunner,
|
|
68
|
+
type FetchUsageArtifactsOptions,
|
|
69
|
+
type UsageArtifactRecord,
|
|
70
|
+
} from './skill-usage.js';
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
PRICING,
|
|
74
|
+
computeReviewCost,
|
|
75
|
+
costPerLOC,
|
|
76
|
+
cacheHitRate,
|
|
77
|
+
extractTokensFromLog,
|
|
78
|
+
rollup,
|
|
79
|
+
formatRollup,
|
|
80
|
+
type ModelPricing,
|
|
81
|
+
type TokenCounts,
|
|
82
|
+
type CostParts,
|
|
83
|
+
type ReviewCost,
|
|
84
|
+
type ExtractedTokens,
|
|
85
|
+
type ReviewRecord,
|
|
86
|
+
type RollupGroupStats,
|
|
87
|
+
type RollupTotal,
|
|
88
|
+
type RollupTrend,
|
|
89
|
+
type RollupOutlier,
|
|
90
|
+
type UnknownModelReview,
|
|
91
|
+
type Rollup,
|
|
92
|
+
type FormatRollupOptions,
|
|
93
|
+
} from './usage.js';
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
runUpdate,
|
|
97
|
+
type RunUpdateOptions,
|
|
98
|
+
type UpdateChangeRecord,
|
|
99
|
+
type UpdateSkippedRecord,
|
|
100
|
+
type RunUpdateResult,
|
|
101
|
+
} from './update.js';
|