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 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
+ });