openthrottle 0.1.4 → 0.1.6
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/index.js +322 -0
- package/dist/init.js +486 -0
- package/dist/types.js +8 -0
- package/package.json +13 -5
- package/index.mjs +0 -346
- package/init.mjs +0 -479
package/dist/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// openthrottle — CLI for Open Throttle.
|
|
4
|
+
//
|
|
5
|
+
// Usage: npx openthrottle <command>
|
|
6
|
+
//
|
|
7
|
+
// Commands:
|
|
8
|
+
// ship <file.md> [--base <branch>] Ship a prompt to a Daytona sandbox
|
|
9
|
+
// status Show running, queued, and completed tasks
|
|
10
|
+
// logs Show recent GitHub Actions workflow runs
|
|
11
|
+
// =============================================================================
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
|
+
import { getErrorMessage } from './types.js';
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// 1. Constants + helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
const EXIT_OK = 0;
|
|
20
|
+
const EXIT_USER_ERROR = 1;
|
|
21
|
+
const EXIT_MISSING_DEP = 2;
|
|
22
|
+
function die(message, code = EXIT_USER_ERROR) {
|
|
23
|
+
console.error(`error: ${message}`);
|
|
24
|
+
process.exit(code);
|
|
25
|
+
}
|
|
26
|
+
function gh(args, { quiet = false } = {}) {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync('gh', args, {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const execErr = err;
|
|
35
|
+
const stderr = execErr.stderr?.toString().trim() || '';
|
|
36
|
+
if (stderr.includes('auth login')) {
|
|
37
|
+
die('gh auth expired -- run: gh auth login', EXIT_MISSING_DEP);
|
|
38
|
+
}
|
|
39
|
+
if (quiet) {
|
|
40
|
+
// Exit code 1 with no stderr = no matching results (expected)
|
|
41
|
+
if (execErr.status === 1 && !stderr)
|
|
42
|
+
return '';
|
|
43
|
+
// Real failure — warn but don't crash
|
|
44
|
+
console.error(`warning: gh ${args.slice(0, 2).join(' ')} failed: ${stderr || execErr.message}`);
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function preflight() {
|
|
51
|
+
try {
|
|
52
|
+
execFileSync('gh', ['auth', 'status'], { stdio: 'pipe' });
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
die('gh CLI not found or not authenticated.\n Install: https://cli.github.com\n Auth: gh auth login', EXIT_MISSING_DEP);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function detectRepo() {
|
|
59
|
+
try {
|
|
60
|
+
const url = execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
+
}).trim();
|
|
64
|
+
const match = url.match(/github\.com[:/](.+?\/.+?)(?:\.git)?$/);
|
|
65
|
+
if (match?.[1])
|
|
66
|
+
return match[1];
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
die('Could not detect GitHub repo. Run from a git repo with a github.com remote.');
|
|
70
|
+
}
|
|
71
|
+
function readConfig() {
|
|
72
|
+
const configPath = join(process.cwd(), '.openthrottle.yml');
|
|
73
|
+
if (!existsSync(configPath)) {
|
|
74
|
+
return { baseBranch: 'main', snapshot: 'openthrottle' };
|
|
75
|
+
}
|
|
76
|
+
let content;
|
|
77
|
+
try {
|
|
78
|
+
content = readFileSync(configPath, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
const e = err;
|
|
82
|
+
die(`Could not read .openthrottle.yml: ${e.message}`);
|
|
83
|
+
}
|
|
84
|
+
const get = (key) => {
|
|
85
|
+
const match = content.match(new RegExp(`^${key}:\\s*(.+)`, 'm'));
|
|
86
|
+
if (!match?.[1])
|
|
87
|
+
return undefined;
|
|
88
|
+
return match[1].replace(/#.*$/, '').trim().replace(/^["']|["']$/g, '');
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
baseBranch: get('base_branch') || 'main',
|
|
92
|
+
snapshot: get('snapshot') || 'openthrottle',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// 2. Command: ship
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
function cmdShip(args) {
|
|
99
|
+
let file = null;
|
|
100
|
+
let baseBranch = null;
|
|
101
|
+
for (let i = 0; i < args.length; i++) {
|
|
102
|
+
if (args[i] === '--base' && args[i + 1]) {
|
|
103
|
+
baseBranch = args[++i];
|
|
104
|
+
}
|
|
105
|
+
else if (!file) {
|
|
106
|
+
file = args[i];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!file)
|
|
110
|
+
die('Usage: openthrottle ship <file.md> [--base <branch>]');
|
|
111
|
+
file = resolve(file);
|
|
112
|
+
if (!existsSync(file))
|
|
113
|
+
die(`File not found: ${file}`);
|
|
114
|
+
if (!file.endsWith('.md'))
|
|
115
|
+
die(`Expected a markdown file, got: ${file}`);
|
|
116
|
+
const config = readConfig();
|
|
117
|
+
const base = baseBranch || config.baseBranch;
|
|
118
|
+
const repo = detectRepo();
|
|
119
|
+
// Extract title from first markdown heading
|
|
120
|
+
const content = readFileSync(file, 'utf8');
|
|
121
|
+
const headingMatch = content.match(/^#{1,6}\s+(.+)/m);
|
|
122
|
+
let title = headingMatch?.[1]?.trim() ?? file.replace(/\.md$/, '');
|
|
123
|
+
if (!title.startsWith('PRD:'))
|
|
124
|
+
title = `PRD: ${title}`;
|
|
125
|
+
// Ensure labels exist (idempotent)
|
|
126
|
+
const labels = [
|
|
127
|
+
'prd-queued', 'prd-running', 'prd-complete', 'prd-failed',
|
|
128
|
+
'needs-review', 'reviewing',
|
|
129
|
+
'bug-queued', 'bug-running', 'bug-complete', 'bug-failed',
|
|
130
|
+
];
|
|
131
|
+
for (const label of labels) {
|
|
132
|
+
try {
|
|
133
|
+
gh(['label', 'create', label, '--repo', repo, '--force']);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`warning: failed to create label "${label}": ${getErrorMessage(err)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Build label list
|
|
140
|
+
let issueLabels = 'prd-queued';
|
|
141
|
+
if (base !== 'main')
|
|
142
|
+
issueLabels += `,base:${base}`;
|
|
143
|
+
// Create the issue
|
|
144
|
+
let issueUrl;
|
|
145
|
+
try {
|
|
146
|
+
issueUrl = gh([
|
|
147
|
+
'issue', 'create',
|
|
148
|
+
'--repo', repo,
|
|
149
|
+
'--title', title,
|
|
150
|
+
'--body-file', file,
|
|
151
|
+
'--label', issueLabels,
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
die(`Failed to create issue: ${getErrorMessage(err)}`);
|
|
156
|
+
}
|
|
157
|
+
// Show queue position
|
|
158
|
+
let queueCount = 0;
|
|
159
|
+
try {
|
|
160
|
+
const raw = gh([
|
|
161
|
+
'issue', 'list', '--repo', repo,
|
|
162
|
+
'--label', 'prd-queued', '--state', 'open',
|
|
163
|
+
'--json', 'number', '--jq', 'length',
|
|
164
|
+
]);
|
|
165
|
+
queueCount = parseInt(raw, 10) || 0;
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
let runningInfo = '';
|
|
169
|
+
try {
|
|
170
|
+
runningInfo = gh([
|
|
171
|
+
'issue', 'list', '--repo', repo,
|
|
172
|
+
'--label', 'prd-running', '--state', 'open',
|
|
173
|
+
'--json', 'number,title',
|
|
174
|
+
'--jq', '.[0] | "#\\(.number) -- \\(.title)"',
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
console.log(`Shipped: ${issueUrl}`);
|
|
179
|
+
if (queueCount > 1) {
|
|
180
|
+
console.log(`Queue: ${queueCount} queued`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.log('Status: starting');
|
|
184
|
+
}
|
|
185
|
+
if (runningInfo) {
|
|
186
|
+
console.log(`Running: ${runningInfo}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// 3. Command: status
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
function cmdStatus() {
|
|
193
|
+
const repo = detectRepo();
|
|
194
|
+
console.log('RUNNING');
|
|
195
|
+
const running = gh([
|
|
196
|
+
'issue', 'list', '--repo', repo,
|
|
197
|
+
'--label', 'prd-running', '--state', 'open',
|
|
198
|
+
'--json', 'number,title',
|
|
199
|
+
'--jq', '.[] | " #\\(.number) -- \\(.title)"',
|
|
200
|
+
], { quiet: true });
|
|
201
|
+
console.log(running || ' (none)');
|
|
202
|
+
console.log('\nQUEUE');
|
|
203
|
+
const queued = gh([
|
|
204
|
+
'issue', 'list', '--repo', repo,
|
|
205
|
+
'--label', 'prd-queued', '--state', 'open',
|
|
206
|
+
'--json', 'number,title',
|
|
207
|
+
'--jq', '.[] | " #\\(.number) -- \\(.title)"',
|
|
208
|
+
], { quiet: true });
|
|
209
|
+
console.log(queued || ' (none)');
|
|
210
|
+
console.log('\nREVIEW');
|
|
211
|
+
const pending = gh([
|
|
212
|
+
'pr', 'list', '--repo', repo,
|
|
213
|
+
'--label', 'needs-review',
|
|
214
|
+
'--json', 'number,title',
|
|
215
|
+
'--jq', '.[] | " pending: #\\(.number) -- \\(.title)"',
|
|
216
|
+
], { quiet: true });
|
|
217
|
+
const reviewing = gh([
|
|
218
|
+
'pr', 'list', '--repo', repo,
|
|
219
|
+
'--label', 'reviewing',
|
|
220
|
+
'--json', 'number,title',
|
|
221
|
+
'--jq', '.[] | " active: #\\(.number) -- \\(.title)"',
|
|
222
|
+
], { quiet: true });
|
|
223
|
+
const fixes = gh([
|
|
224
|
+
'pr', 'list', '--repo', repo,
|
|
225
|
+
'--search', 'review:changes_requested',
|
|
226
|
+
'--json', 'number,title',
|
|
227
|
+
'--jq', '.[] | " fixes: #\\(.number) -- \\(.title)"',
|
|
228
|
+
], { quiet: true });
|
|
229
|
+
const reviewOutput = [pending, reviewing, fixes].filter(Boolean).join('\n');
|
|
230
|
+
console.log(reviewOutput || ' (none)');
|
|
231
|
+
console.log('\nCOMPLETED (recent)');
|
|
232
|
+
const completed = gh([
|
|
233
|
+
'issue', 'list', '--repo', repo,
|
|
234
|
+
'--label', 'prd-complete', '--state', 'closed',
|
|
235
|
+
'--limit', '5',
|
|
236
|
+
'--json', 'number,title',
|
|
237
|
+
'--jq', '.[] | " #\\(.number) -- \\(.title)"',
|
|
238
|
+
], { quiet: true });
|
|
239
|
+
console.log(completed || ' (none)');
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// 4. Command: logs
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
function cmdLogs() {
|
|
245
|
+
const repo = detectRepo();
|
|
246
|
+
let output;
|
|
247
|
+
try {
|
|
248
|
+
output = gh([
|
|
249
|
+
'run', 'list',
|
|
250
|
+
'--repo', repo,
|
|
251
|
+
'--workflow', 'Wake Sandbox',
|
|
252
|
+
'--limit', '10',
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
try {
|
|
257
|
+
output = gh([
|
|
258
|
+
'run', 'list',
|
|
259
|
+
'--repo', repo,
|
|
260
|
+
'--limit', '10',
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
die(`Failed to list workflow runs: ${getErrorMessage(err)}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (!output) {
|
|
268
|
+
console.log('No workflow runs found.');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(output);
|
|
272
|
+
}
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// 5. Main
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
const HELP = `Usage: openthrottle <command>
|
|
277
|
+
|
|
278
|
+
Commands:
|
|
279
|
+
init Set up Open Throttle in your project
|
|
280
|
+
ship <file.md> [--base <branch>] Create a GitHub issue to trigger a sandbox
|
|
281
|
+
status Show running, queued, and completed tasks
|
|
282
|
+
logs Show recent GitHub Actions workflow runs
|
|
283
|
+
|
|
284
|
+
Options:
|
|
285
|
+
--help, -h Show this help message
|
|
286
|
+
--version, -v Show version`;
|
|
287
|
+
async function main() {
|
|
288
|
+
const args = process.argv.slice(2);
|
|
289
|
+
const command = args[0];
|
|
290
|
+
if (!command || command === '--help' || command === '-h') {
|
|
291
|
+
console.log(HELP);
|
|
292
|
+
process.exit(EXIT_OK);
|
|
293
|
+
}
|
|
294
|
+
if (command === '--version' || command === '-v') {
|
|
295
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
296
|
+
console.log(pkg.version);
|
|
297
|
+
process.exit(EXIT_OK);
|
|
298
|
+
}
|
|
299
|
+
if (command === 'init') {
|
|
300
|
+
const { default: init } = await import('./init.js');
|
|
301
|
+
await init();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
preflight();
|
|
305
|
+
switch (command) {
|
|
306
|
+
case 'ship':
|
|
307
|
+
cmdShip(args.slice(1));
|
|
308
|
+
break;
|
|
309
|
+
case 'status':
|
|
310
|
+
cmdStatus();
|
|
311
|
+
break;
|
|
312
|
+
case 'logs':
|
|
313
|
+
cmdLogs();
|
|
314
|
+
break;
|
|
315
|
+
default:
|
|
316
|
+
die(`Unknown command: ${command}\n Run "openthrottle --help" for usage.`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
main().catch((err) => {
|
|
320
|
+
console.error(`error: ${err.message}`);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
});
|