openthrottle 0.1.3 → 0.1.5
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 +482 -0
- package/dist/types.js +8 -0
- package/package.json +13 -5
- package/index.mjs +0 -346
- package/init.mjs +0 -479
package/dist/init.js
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// openthrottle init — Set up Open Throttle in any Node.js project.
|
|
3
|
+
//
|
|
4
|
+
// Detects the project, prompts for config, generates .openthrottle.yml +
|
|
5
|
+
// wake-sandbox.yml, creates a Daytona snapshot, and prints next steps.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import prompts from 'prompts';
|
|
11
|
+
import { stringify } from 'yaml';
|
|
12
|
+
import { getErrorMessage } from './types.js';
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// 1. Detect project
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function detectProject() {
|
|
18
|
+
const pkgPath = join(cwd, 'package.json');
|
|
19
|
+
if (!existsSync(pkgPath)) {
|
|
20
|
+
console.error('No package.json found. openthrottle init currently supports Node.js projects only.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
let pkg;
|
|
24
|
+
try {
|
|
25
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.error('Could not parse package.json. Is it valid JSON?');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const scripts = pkg.scripts || {};
|
|
32
|
+
const rawName = pkg.name?.replace(/^@[^/]+\//, '') || 'project';
|
|
33
|
+
const name = rawName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
34
|
+
// Detect package manager
|
|
35
|
+
let pm = 'npm';
|
|
36
|
+
if (pkg.packageManager?.startsWith('pnpm'))
|
|
37
|
+
pm = 'pnpm';
|
|
38
|
+
else if (pkg.packageManager?.startsWith('yarn'))
|
|
39
|
+
pm = 'yarn';
|
|
40
|
+
else if (existsSync(join(cwd, 'pnpm-lock.yaml')))
|
|
41
|
+
pm = 'pnpm';
|
|
42
|
+
else if (existsSync(join(cwd, 'yarn.lock')))
|
|
43
|
+
pm = 'yarn';
|
|
44
|
+
else if (existsSync(join(cwd, 'package-lock.json')))
|
|
45
|
+
pm = 'npm';
|
|
46
|
+
// Detect base branch
|
|
47
|
+
let baseBranch = 'main';
|
|
48
|
+
try {
|
|
49
|
+
const head = execFileSync('git', ['remote', 'show', 'origin'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
50
|
+
const match = head.match(/HEAD branch:\s*(\S+)/);
|
|
51
|
+
if (match?.[1])
|
|
52
|
+
baseBranch = match[1];
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Not a git repo or no remote — default to main
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
pm,
|
|
60
|
+
baseBranch,
|
|
61
|
+
test: scripts.test ? `${pm} test` : '',
|
|
62
|
+
build: scripts.build ? `${pm} build` : '',
|
|
63
|
+
lint: scripts.lint ? `${pm} lint` : '',
|
|
64
|
+
format: scripts.format ? `${pm} run format` : (pkg.devDependencies?.prettier ? 'npx prettier --write .' : ''),
|
|
65
|
+
dev: scripts.dev ? `${pm} dev --port 8080 --hostname 0.0.0.0` : '',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// 1b. Detect .env files and extract key names
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
function detectEnvFiles() {
|
|
72
|
+
const envFiles = {};
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
function scan(dir) {
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = readdirSync(dir);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry === 'node_modules' || entry === '.git' || entry === '.next' || entry === 'dist')
|
|
84
|
+
continue;
|
|
85
|
+
const full = join(dir, entry);
|
|
86
|
+
let stat;
|
|
87
|
+
try {
|
|
88
|
+
stat = statSync(full);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (stat.isDirectory()) {
|
|
94
|
+
scan(full);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!entry.startsWith('.env'))
|
|
98
|
+
continue;
|
|
99
|
+
// Skip .env.example, .env.sample, .env.template
|
|
100
|
+
if (/\.(example|sample|template)$/i.test(entry))
|
|
101
|
+
continue;
|
|
102
|
+
const relPath = relative(cwd, full);
|
|
103
|
+
const keys = [];
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(full, 'utf8');
|
|
106
|
+
for (const line of content.split('\n')) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
109
|
+
continue;
|
|
110
|
+
const match = trimmed.replace(/^export\s+/, '').match(/^([a-zA-Z_][a-zA-Z0-9_]*)=/);
|
|
111
|
+
if (match?.[1])
|
|
112
|
+
keys.push(match[1]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(`warning: could not read ${relPath}: ${getErrorMessage(err)}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (keys.length > 0) {
|
|
120
|
+
envFiles[relPath] = keys;
|
|
121
|
+
keys.forEach(k => seen.add(k));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
scan(cwd);
|
|
126
|
+
return { envFiles, allKeys: [...seen].sort() };
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// 2. Prompt for config
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
async function promptConfig(detected) {
|
|
132
|
+
console.log(`\n Detected: package.json (${detected.pm})\n`);
|
|
133
|
+
const response = await prompts([
|
|
134
|
+
{ type: 'text', name: 'baseBranch', message: 'Base branch', initial: detected.baseBranch },
|
|
135
|
+
{ type: 'text', name: 'test', message: 'Test command', initial: detected.test },
|
|
136
|
+
{ type: 'text', name: 'build', message: 'Build command', initial: detected.build },
|
|
137
|
+
{ type: 'text', name: 'lint', message: 'Lint command', initial: detected.lint },
|
|
138
|
+
{ type: 'text', name: 'format', message: 'Format command', initial: detected.format },
|
|
139
|
+
{ type: 'text', name: 'dev', message: 'Dev command', initial: detected.dev },
|
|
140
|
+
{ type: 'text', name: 'postBootstrap', message: 'Post-bootstrap command', initial: `${detected.pm} install` },
|
|
141
|
+
{
|
|
142
|
+
type: 'select', name: 'agent', message: 'Agent runtime',
|
|
143
|
+
choices: [
|
|
144
|
+
{ title: 'Claude', value: 'claude' },
|
|
145
|
+
{ title: 'Codex', value: 'codex' },
|
|
146
|
+
{ title: 'Aider', value: 'aider' },
|
|
147
|
+
],
|
|
148
|
+
initial: 0,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'select', name: 'notifications', message: 'Notifications',
|
|
152
|
+
choices: [
|
|
153
|
+
{ title: 'Telegram', value: 'telegram' },
|
|
154
|
+
{ title: 'None', value: 'none' },
|
|
155
|
+
],
|
|
156
|
+
initial: 0,
|
|
157
|
+
},
|
|
158
|
+
{ type: 'number', name: 'maxTurns', message: 'Max turns per agent run', initial: 200, min: 1 },
|
|
159
|
+
{ type: 'number', name: 'maxBudgetUsd', message: 'Max budget per run in USD (API only)', initial: 5, min: 0 },
|
|
160
|
+
{ type: 'confirm', name: 'reviewEnabled', message: 'Enable automated PR review?', initial: true },
|
|
161
|
+
{
|
|
162
|
+
type: (prev) => prev ? 'number' : null,
|
|
163
|
+
name: 'maxRounds', message: 'Max review rounds', initial: 3, min: 1, max: 10,
|
|
164
|
+
},
|
|
165
|
+
{ type: 'text', name: 'snapshotName', message: 'Daytona snapshot name', initial: 'openthrottle' },
|
|
166
|
+
], { onCancel: () => { console.log('\nCancelled.'); process.exit(0); } });
|
|
167
|
+
const { baseBranch, test, build, lint, format, dev, postBootstrap, agent, notifications, maxTurns, maxBudgetUsd, reviewEnabled, maxRounds, snapshotName } = response;
|
|
168
|
+
return {
|
|
169
|
+
...detected,
|
|
170
|
+
baseBranch: baseBranch || detected.baseBranch,
|
|
171
|
+
test: test ?? detected.test,
|
|
172
|
+
build: build ?? detected.build,
|
|
173
|
+
lint: lint ?? detected.lint,
|
|
174
|
+
format: format ?? detected.format,
|
|
175
|
+
dev: dev ?? detected.dev,
|
|
176
|
+
postBootstrap: postBootstrap,
|
|
177
|
+
agent: agent,
|
|
178
|
+
notifications: notifications,
|
|
179
|
+
maxTurns: maxTurns,
|
|
180
|
+
maxBudgetUsd: maxBudgetUsd,
|
|
181
|
+
reviewEnabled: reviewEnabled,
|
|
182
|
+
maxRounds: maxRounds,
|
|
183
|
+
snapshotName: snapshotName,
|
|
184
|
+
envFiles: {},
|
|
185
|
+
envAllKeys: [],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// 3. Generate .openthrottle.yml
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
function generateConfig(config) {
|
|
192
|
+
const doc = {
|
|
193
|
+
base_branch: config.baseBranch,
|
|
194
|
+
test: config.test || undefined,
|
|
195
|
+
dev: config.dev || undefined,
|
|
196
|
+
format: config.format || undefined,
|
|
197
|
+
lint: config.lint || undefined,
|
|
198
|
+
build: config.build || undefined,
|
|
199
|
+
notifications: config.notifications === 'none' ? undefined : config.notifications,
|
|
200
|
+
agent: config.agent,
|
|
201
|
+
snapshot: config.snapshotName || 'openthrottle',
|
|
202
|
+
post_bootstrap: [config.postBootstrap],
|
|
203
|
+
mcp_servers: {},
|
|
204
|
+
env_files: config.envFiles && Object.keys(config.envFiles).length > 0
|
|
205
|
+
? config.envFiles
|
|
206
|
+
: undefined,
|
|
207
|
+
limits: {
|
|
208
|
+
max_turns: config.maxTurns ?? 200,
|
|
209
|
+
max_budget_usd: config.maxBudgetUsd ?? 5,
|
|
210
|
+
},
|
|
211
|
+
review: {
|
|
212
|
+
enabled: config.reviewEnabled,
|
|
213
|
+
max_rounds: config.maxRounds ?? 3,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
// Remove undefined fields
|
|
217
|
+
for (const key of Object.keys(doc)) {
|
|
218
|
+
if (doc[key] === undefined)
|
|
219
|
+
delete doc[key];
|
|
220
|
+
}
|
|
221
|
+
const header = [
|
|
222
|
+
'# openthrottle.yml — project config for Open Throttle (Daytona runtime)',
|
|
223
|
+
'# Generated by openthrottle init. Committed to the repo so the',
|
|
224
|
+
'# sandbox knows how to work with this project.',
|
|
225
|
+
'',
|
|
226
|
+
].join('\n');
|
|
227
|
+
return header + stringify(doc);
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// 4. Copy wake-sandbox.yml
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
function copyWorkflow(config) {
|
|
233
|
+
const src = new URL('../templates/wake-sandbox.yml', import.meta.url);
|
|
234
|
+
const destDir = join(cwd, '.github', 'workflows');
|
|
235
|
+
const dest = join(destDir, 'wake-sandbox.yml');
|
|
236
|
+
mkdirSync(destDir, { recursive: true });
|
|
237
|
+
let content = readFileSync(src, 'utf8');
|
|
238
|
+
// Inject project-specific secrets into the workflow
|
|
239
|
+
const allKeys = config.envAllKeys || [];
|
|
240
|
+
if (allKeys.length > 0) {
|
|
241
|
+
// Add env: entries for secrets
|
|
242
|
+
const envSecrets = allKeys
|
|
243
|
+
.map(k => ` ${k}: \${{ secrets.${k} }}`)
|
|
244
|
+
.join('\n');
|
|
245
|
+
content = content.replace(/ # @@ENV_SECRETS@@ — scaffolder inserts project-specific secrets here/, envSecrets);
|
|
246
|
+
// Add --env flags for daytona create
|
|
247
|
+
const envFlags = allKeys
|
|
248
|
+
.map(k => ` --env ${k}=\${${k}} \\`)
|
|
249
|
+
.join('\n');
|
|
250
|
+
content = content.replace(/ # @@ENV_FLAGS@@ — scaffolder inserts --env flags for project secrets here/, envFlags);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// No project secrets — remove the placeholder comments
|
|
254
|
+
content = content.replace(/ # @@ENV_SECRETS@@ — scaffolder inserts project-specific secrets here\n/, '');
|
|
255
|
+
content = content.replace(/ # @@ENV_FLAGS@@ — scaffolder inserts --env flags for project secrets here\n/, '');
|
|
256
|
+
}
|
|
257
|
+
writeFileSync(dest, content);
|
|
258
|
+
return dest;
|
|
259
|
+
}
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// 5. Create Daytona snapshot from pre-built image
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
function setupDaytona(config) {
|
|
264
|
+
const snapshotName = config.snapshotName || 'openthrottle';
|
|
265
|
+
const image = 'knoxgraeme/openthrottle:v1';
|
|
266
|
+
// Check daytona CLI is available
|
|
267
|
+
try {
|
|
268
|
+
execFileSync('daytona', ['--version'], { stdio: 'pipe' });
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
console.log(`\n daytona CLI not found. Install it, then run:`);
|
|
272
|
+
console.log(` daytona snapshot create ${snapshotName} --image ${image} --cpu 2 --memory 4 --disk 10\n`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// Create snapshot from pre-built image
|
|
276
|
+
try {
|
|
277
|
+
execFileSync('daytona', [
|
|
278
|
+
'snapshot', 'create', snapshotName,
|
|
279
|
+
'--image', image,
|
|
280
|
+
'--cpu', '2', '--memory', '4', '--disk', '10',
|
|
281
|
+
], { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
282
|
+
console.log(` Created Daytona snapshot: ${snapshotName}`);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
const execErr = err;
|
|
286
|
+
if (execErr.stderr?.toString().includes('already exists')) {
|
|
287
|
+
console.log(` Snapshot already exists: ${snapshotName}`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
console.log(` Snapshot creation failed. You can create it manually:`);
|
|
291
|
+
console.log(` daytona snapshot create ${snapshotName} --image ${image} --cpu 2 --memory 4 --disk 10`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// 6. Push .env secrets to GitHub repo secrets
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
async function pushSecrets(config) {
|
|
299
|
+
const envFiles = config.envFiles || {};
|
|
300
|
+
const paths = Object.keys(envFiles);
|
|
301
|
+
if (paths.length === 0)
|
|
302
|
+
return;
|
|
303
|
+
// Check gh is available and authenticated
|
|
304
|
+
try {
|
|
305
|
+
execFileSync('gh', ['auth', 'status'], { stdio: 'pipe' });
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
console.log('\n gh CLI not authenticated — skipping secret push.');
|
|
309
|
+
console.log(' Run "gh auth login" then set secrets manually.\n');
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
// Detect repo
|
|
313
|
+
let repo;
|
|
314
|
+
try {
|
|
315
|
+
repo = execFileSync('gh', ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], {
|
|
316
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
317
|
+
}).trim();
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
console.log('\n Could not detect GitHub repo — skipping secret push.\n');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Show what we'd push
|
|
324
|
+
console.log(`\n Push .env secrets to GitHub repo secrets? (${repo})`);
|
|
325
|
+
console.log(' Values are encrypted at rest — not readable after upload.\n');
|
|
326
|
+
for (const [path, keys] of Object.entries(envFiles)) {
|
|
327
|
+
console.log(` ${path} (${keys.length} keys): ${keys.join(', ')}`);
|
|
328
|
+
}
|
|
329
|
+
console.log('');
|
|
330
|
+
const { confirm } = await prompts({
|
|
331
|
+
type: 'confirm', name: 'confirm',
|
|
332
|
+
message: `Push ${config.envAllKeys.length} secret(s) to ${repo}?`, initial: false,
|
|
333
|
+
}, { onCancel: () => { } });
|
|
334
|
+
if (!confirm) {
|
|
335
|
+
console.log(' Skipped secret push.');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let pushed = 0;
|
|
339
|
+
let failed = 0;
|
|
340
|
+
for (const [path, keys] of Object.entries(envFiles)) {
|
|
341
|
+
const fullPath = join(cwd, path);
|
|
342
|
+
let content;
|
|
343
|
+
try {
|
|
344
|
+
content = readFileSync(fullPath, 'utf8');
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
console.log(` Could not read ${path} — skipping`);
|
|
348
|
+
failed += keys.length;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// Parse key=value pairs
|
|
352
|
+
for (const line of content.split('\n')) {
|
|
353
|
+
const trimmed = line.trim();
|
|
354
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
355
|
+
continue;
|
|
356
|
+
const cleaned = trimmed.replace(/^export\s+/, '');
|
|
357
|
+
const eqIdx = cleaned.indexOf('=');
|
|
358
|
+
if (eqIdx === -1)
|
|
359
|
+
continue;
|
|
360
|
+
const key = cleaned.slice(0, eqIdx);
|
|
361
|
+
let value = cleaned.slice(eqIdx + 1);
|
|
362
|
+
// Strip surrounding quotes
|
|
363
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
364
|
+
value = value.slice(1, -1);
|
|
365
|
+
}
|
|
366
|
+
if (!keys.includes(key))
|
|
367
|
+
continue;
|
|
368
|
+
try {
|
|
369
|
+
execFileSync('gh', ['secret', 'set', key, '--repo', repo], {
|
|
370
|
+
input: value,
|
|
371
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
372
|
+
});
|
|
373
|
+
pushed++;
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
console.log(` Failed to set ${key}: ${getErrorMessage(err)}`);
|
|
377
|
+
failed++;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
console.log(` Pushed ${pushed} secret(s) to ${repo}${failed > 0 ? ` (${failed} failed)` : ''}`);
|
|
382
|
+
}
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// 7. Print next steps
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
function printNextSteps(config) {
|
|
387
|
+
const agentSecret = config.agent === 'claude'
|
|
388
|
+
? ' ANTHROPIC_API_KEY <- option a: pay-per-use API key\n CLAUDE_CODE_OAUTH_TOKEN <- option b: subscription token (claude setup-token)'
|
|
389
|
+
: config.agent === 'codex'
|
|
390
|
+
? ' OPENAI_API_KEY <- required for Codex'
|
|
391
|
+
: ' OPENAI_API_KEY <- or ANTHROPIC_API_KEY (depends on your Aider model)';
|
|
392
|
+
const secrets = [
|
|
393
|
+
' DAYTONA_API_KEY <- required',
|
|
394
|
+
agentSecret,
|
|
395
|
+
];
|
|
396
|
+
// Project-specific secrets from env_files
|
|
397
|
+
const projectKeys = config.envAllKeys || [];
|
|
398
|
+
const projectSecrets = projectKeys.length > 0
|
|
399
|
+
? '\n\n Project secrets (from .env files):\n' +
|
|
400
|
+
projectKeys.map(k => ` ${k}`).join('\n')
|
|
401
|
+
: '';
|
|
402
|
+
console.log(`
|
|
403
|
+
Next steps:
|
|
404
|
+
|
|
405
|
+
1. Set GitHub repo secrets:
|
|
406
|
+
${secrets.join('\n')}
|
|
407
|
+
TELEGRAM_BOT_TOKEN <- optional (notifications)
|
|
408
|
+
TELEGRAM_CHAT_ID <- optional (notifications)${projectSecrets}
|
|
409
|
+
|
|
410
|
+
2. Commit and push:
|
|
411
|
+
git add .openthrottle.yml .github/workflows/wake-sandbox.yml
|
|
412
|
+
git commit -m "feat: add openthrottle config"
|
|
413
|
+
git push
|
|
414
|
+
|
|
415
|
+
3. Ship your first prompt:
|
|
416
|
+
npx openthrottle ship docs/prds/my-feature.md
|
|
417
|
+
`);
|
|
418
|
+
}
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Main (exported for use by index.ts)
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
export default async function init() {
|
|
423
|
+
console.log('\n openthrottle init\n');
|
|
424
|
+
// Step 1: Detect
|
|
425
|
+
const detected = detectProject();
|
|
426
|
+
const { envFiles, allKeys: envAllKeys } = detectEnvFiles();
|
|
427
|
+
if (Object.keys(envFiles).length > 0) {
|
|
428
|
+
console.log(` Found ${Object.keys(envFiles).length} .env file(s):`);
|
|
429
|
+
for (const [path, keys] of Object.entries(envFiles)) {
|
|
430
|
+
console.log(` ${path} (${keys.length} keys)`);
|
|
431
|
+
}
|
|
432
|
+
console.log('');
|
|
433
|
+
}
|
|
434
|
+
// Step 2: Prompt
|
|
435
|
+
const config = await promptConfig(detected);
|
|
436
|
+
config.envFiles = envFiles;
|
|
437
|
+
config.envAllKeys = envAllKeys;
|
|
438
|
+
// Step 3: Generate config
|
|
439
|
+
const configPath = join(cwd, '.openthrottle.yml');
|
|
440
|
+
if (existsSync(configPath)) {
|
|
441
|
+
const { overwrite } = await prompts({
|
|
442
|
+
type: 'confirm', name: 'overwrite',
|
|
443
|
+
message: '.openthrottle.yml already exists. Overwrite?', initial: false,
|
|
444
|
+
}, { onCancel: () => { console.log('\nCancelled.'); process.exit(0); } });
|
|
445
|
+
if (!overwrite) {
|
|
446
|
+
console.log(' Skipped .openthrottle.yml');
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
writeFileSync(configPath, generateConfig(config));
|
|
450
|
+
console.log(' Generated .openthrottle.yml');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
writeFileSync(configPath, generateConfig(config));
|
|
455
|
+
console.log(' Generated .openthrottle.yml');
|
|
456
|
+
}
|
|
457
|
+
// Step 4: Copy workflow
|
|
458
|
+
const workflowPath = join(cwd, '.github', 'workflows', 'wake-sandbox.yml');
|
|
459
|
+
if (existsSync(workflowPath)) {
|
|
460
|
+
const { overwrite } = await prompts({
|
|
461
|
+
type: 'confirm', name: 'overwrite',
|
|
462
|
+
message: 'wake-sandbox.yml already exists. Overwrite?', initial: false,
|
|
463
|
+
}, { onCancel: () => { console.log('\nCancelled.'); process.exit(0); } });
|
|
464
|
+
if (!overwrite) {
|
|
465
|
+
console.log(' Skipped wake-sandbox.yml');
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
copyWorkflow(config);
|
|
469
|
+
console.log(' Copied .github/workflows/wake-sandbox.yml');
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
copyWorkflow(config);
|
|
474
|
+
console.log(' Copied .github/workflows/wake-sandbox.yml');
|
|
475
|
+
}
|
|
476
|
+
// Step 5: Create Daytona snapshot
|
|
477
|
+
setupDaytona(config);
|
|
478
|
+
// Step 6: Push secrets
|
|
479
|
+
await pushSecrets(config);
|
|
480
|
+
// Step 7: Next steps
|
|
481
|
+
printNextSteps(config);
|
|
482
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openthrottle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "CLI for Open Throttle — ship prompts to Daytona sandboxes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"openthrottle": "./index.
|
|
7
|
+
"openthrottle": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"init.mjs",
|
|
10
|
+
"dist/",
|
|
12
11
|
"templates/"
|
|
13
12
|
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
14
17
|
"dependencies": {
|
|
15
18
|
"prompts": "^2.4.2",
|
|
16
19
|
"yaml": "^2.4.0"
|
|
@@ -28,5 +31,10 @@
|
|
|
28
31
|
"daytona",
|
|
29
32
|
"agent",
|
|
30
33
|
"cli"
|
|
31
|
-
]
|
|
34
|
+
],
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.5.0",
|
|
37
|
+
"@types/prompts": "^2.4.9",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
}
|
|
32
40
|
}
|