create-openthrottle 1.3.3 → 1.3.4
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/index.mjs +101 -7
- package/package.json +1 -1
- package/templates/docker/entrypoint.sh +34 -9
- package/templates/wake-sandbox.yml +4 -2
package/index.mjs
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// wake-sandbox.yml, creates a Daytona snapshot from GHCR, and prints next steps.
|
|
9
9
|
// =============================================================================
|
|
10
10
|
|
|
11
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
|
|
12
|
-
import { join, dirname } from 'node:path';
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
+
import { join, dirname, relative } from 'node:path';
|
|
13
13
|
import { execFileSync } from 'node:child_process';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import prompts from 'prompts';
|
|
@@ -70,6 +70,50 @@ function detectProject() {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// 1b. Detect .env files and extract key names
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function detectEnvFiles() {
|
|
78
|
+
const envFiles = {};
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
|
|
81
|
+
function scan(dir) {
|
|
82
|
+
let entries;
|
|
83
|
+
try { entries = readdirSync(dir); } catch { return; }
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (entry === 'node_modules' || entry === '.git' || entry === '.next' || entry === 'dist') continue;
|
|
86
|
+
const full = join(dir, entry);
|
|
87
|
+
let stat;
|
|
88
|
+
try { stat = statSync(full); } catch { continue; }
|
|
89
|
+
if (stat.isDirectory()) { scan(full); continue; }
|
|
90
|
+
if (!entry.startsWith('.env')) continue;
|
|
91
|
+
// Skip .env.example, .env.sample, .env.template
|
|
92
|
+
if (/\.(example|sample|template)$/i.test(entry)) continue;
|
|
93
|
+
|
|
94
|
+
const relPath = relative(cwd, full);
|
|
95
|
+
const keys = [];
|
|
96
|
+
try {
|
|
97
|
+
const content = readFileSync(full, 'utf8');
|
|
98
|
+
for (const line of content.split('\n')) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
101
|
+
const match = trimmed.replace(/^export\s+/, '').match(/^([a-zA-Z_][a-zA-Z0-9_]*)=/);
|
|
102
|
+
if (match) keys.push(match[1]);
|
|
103
|
+
}
|
|
104
|
+
} catch { continue; }
|
|
105
|
+
|
|
106
|
+
if (keys.length > 0) {
|
|
107
|
+
envFiles[relPath] = keys;
|
|
108
|
+
keys.forEach(k => seen.add(k));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
scan(cwd);
|
|
114
|
+
return { envFiles, allKeys: [...seen].sort() };
|
|
115
|
+
}
|
|
116
|
+
|
|
73
117
|
// ---------------------------------------------------------------------------
|
|
74
118
|
// 2. Prompt for config
|
|
75
119
|
// ---------------------------------------------------------------------------
|
|
@@ -130,6 +174,9 @@ function generateConfig(config) {
|
|
|
130
174
|
snapshot: config.snapshotName || 'openthrottle',
|
|
131
175
|
post_bootstrap: [config.postBootstrap],
|
|
132
176
|
mcp_servers: {},
|
|
177
|
+
env_files: config.envFiles && Object.keys(config.envFiles).length > 0
|
|
178
|
+
? config.envFiles
|
|
179
|
+
: undefined,
|
|
133
180
|
review: {
|
|
134
181
|
enabled: config.reviewEnabled,
|
|
135
182
|
max_rounds: config.maxRounds ?? 3,
|
|
@@ -155,12 +202,41 @@ function generateConfig(config) {
|
|
|
155
202
|
// 4. Copy wake-sandbox.yml
|
|
156
203
|
// ---------------------------------------------------------------------------
|
|
157
204
|
|
|
158
|
-
function copyWorkflow() {
|
|
205
|
+
function copyWorkflow(config) {
|
|
159
206
|
const src = join(__dirname, 'templates', 'wake-sandbox.yml');
|
|
160
207
|
const destDir = join(cwd, '.github', 'workflows');
|
|
161
208
|
const dest = join(destDir, 'wake-sandbox.yml');
|
|
162
209
|
mkdirSync(destDir, { recursive: true });
|
|
163
|
-
|
|
210
|
+
|
|
211
|
+
let content = readFileSync(src, 'utf8');
|
|
212
|
+
|
|
213
|
+
// Inject project-specific secrets into the workflow
|
|
214
|
+
const allKeys = config.envAllKeys || [];
|
|
215
|
+
if (allKeys.length > 0) {
|
|
216
|
+
// Add env: entries for secrets
|
|
217
|
+
const envSecrets = allKeys
|
|
218
|
+
.map(k => ` ${k}: \${{ secrets.${k} }}`)
|
|
219
|
+
.join('\n');
|
|
220
|
+
content = content.replace(
|
|
221
|
+
/ # @@ENV_SECRETS@@ — scaffolder inserts project-specific secrets here/,
|
|
222
|
+
envSecrets
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Add --env flags for daytona create
|
|
226
|
+
const envFlags = allKeys
|
|
227
|
+
.map(k => ` --env ${k}=\${${k}} \\`)
|
|
228
|
+
.join('\n');
|
|
229
|
+
content = content.replace(
|
|
230
|
+
/ # @@ENV_FLAGS@@ — scaffolder inserts --env flags for project secrets here/,
|
|
231
|
+
envFlags
|
|
232
|
+
);
|
|
233
|
+
} else {
|
|
234
|
+
// No project secrets — remove the placeholder comments
|
|
235
|
+
content = content.replace(/ # @@ENV_SECRETS@@ — scaffolder inserts project-specific secrets here\n/, '');
|
|
236
|
+
content = content.replace(/ # @@ENV_FLAGS@@ — scaffolder inserts --env flags for project secrets here\n/, '');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
writeFileSync(dest, content);
|
|
164
240
|
return dest;
|
|
165
241
|
}
|
|
166
242
|
|
|
@@ -217,13 +293,20 @@ function printNextSteps(config) {
|
|
|
217
293
|
agentSecret,
|
|
218
294
|
];
|
|
219
295
|
|
|
296
|
+
// Project-specific secrets from env_files
|
|
297
|
+
const projectKeys = config.envAllKeys || [];
|
|
298
|
+
const projectSecrets = projectKeys.length > 0
|
|
299
|
+
? '\n\n Project secrets (from .env files):\n' +
|
|
300
|
+
projectKeys.map(k => ` ${k}`).join('\n')
|
|
301
|
+
: '';
|
|
302
|
+
|
|
220
303
|
console.log(`
|
|
221
304
|
Next steps:
|
|
222
305
|
|
|
223
306
|
1. Set GitHub repo secrets:
|
|
224
307
|
${secrets.join('\n')}
|
|
225
308
|
TELEGRAM_BOT_TOKEN ← optional (notifications)
|
|
226
|
-
TELEGRAM_CHAT_ID ← optional (notifications)
|
|
309
|
+
TELEGRAM_CHAT_ID ← optional (notifications)${projectSecrets}
|
|
227
310
|
|
|
228
311
|
2. Commit and push:
|
|
229
312
|
git add .openthrottle.yml .github/workflows/wake-sandbox.yml
|
|
@@ -246,9 +329,20 @@ async function main() {
|
|
|
246
329
|
|
|
247
330
|
// Step 1: Detect
|
|
248
331
|
const detected = detectProject();
|
|
332
|
+
const { envFiles, allKeys: envAllKeys } = detectEnvFiles();
|
|
333
|
+
|
|
334
|
+
if (Object.keys(envFiles).length > 0) {
|
|
335
|
+
console.log(` Found ${Object.keys(envFiles).length} .env file(s):`);
|
|
336
|
+
for (const [path, keys] of Object.entries(envFiles)) {
|
|
337
|
+
console.log(` ${path} (${keys.length} keys)`);
|
|
338
|
+
}
|
|
339
|
+
console.log('');
|
|
340
|
+
}
|
|
249
341
|
|
|
250
342
|
// Step 2: Prompt
|
|
251
343
|
const config = await promptConfig(detected);
|
|
344
|
+
config.envFiles = envFiles;
|
|
345
|
+
config.envAllKeys = envAllKeys;
|
|
252
346
|
|
|
253
347
|
// Step 3: Generate config
|
|
254
348
|
const configPath = join(cwd, '.openthrottle.yml');
|
|
@@ -272,9 +366,9 @@ async function main() {
|
|
|
272
366
|
message: 'wake-sandbox.yml already exists. Overwrite?', initial: false,
|
|
273
367
|
}, { onCancel: () => { console.log('\nCancelled.'); process.exit(0); } });
|
|
274
368
|
if (!overwrite) { console.log(' Skipped wake-sandbox.yml'); }
|
|
275
|
-
else { copyWorkflow(); console.log(' ✓ Copied .github/workflows/wake-sandbox.yml'); }
|
|
369
|
+
else { copyWorkflow(config); console.log(' ✓ Copied .github/workflows/wake-sandbox.yml'); }
|
|
276
370
|
} else {
|
|
277
|
-
copyWorkflow();
|
|
371
|
+
copyWorkflow(config);
|
|
278
372
|
console.log(' ✓ Copied .github/workflows/wake-sandbox.yml');
|
|
279
373
|
}
|
|
280
374
|
|
package/package.json
CHANGED
|
@@ -60,7 +60,32 @@ LINT_CMD=$(read_config '.lint' '')
|
|
|
60
60
|
AGENT=$(read_config '.agent' 'claude')
|
|
61
61
|
|
|
62
62
|
# ---------------------------------------------------------------------------
|
|
63
|
-
# 3.
|
|
63
|
+
# 3. Write .env files from env_files config
|
|
64
|
+
# Maps sandbox env vars to .env files at project-specific paths.
|
|
65
|
+
# Keys are defined in .openthrottle.yml; values come from --env flags
|
|
66
|
+
# passed by the GitHub Action (sourced from GitHub repo secrets).
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
ENV_FILES_COUNT=$(yq -r '.env_files // {} | keys | length' "$CONFIG" 2>/dev/null || echo "0")
|
|
69
|
+
if [[ "$ENV_FILES_COUNT" -gt 0 ]]; then
|
|
70
|
+
log "Writing ${ENV_FILES_COUNT} .env file(s)"
|
|
71
|
+
yq -r '.env_files // {} | to_entries[] | .key' "$CONFIG" | while IFS= read -r filepath; do
|
|
72
|
+
target="${REPO}/${filepath}"
|
|
73
|
+
mkdir -p "$(dirname "$target")"
|
|
74
|
+
# Write each key=value pair
|
|
75
|
+
yq -r ".env_files[\"${filepath}\"][]" "$CONFIG" | while IFS= read -r key; do
|
|
76
|
+
value="${!key:-}"
|
|
77
|
+
if [[ -n "$value" ]]; then
|
|
78
|
+
echo "${key}=${value}" >> "$target"
|
|
79
|
+
else
|
|
80
|
+
log " WARNING: ${key} not set in environment — skipping for ${filepath}"
|
|
81
|
+
fi
|
|
82
|
+
done
|
|
83
|
+
log " Wrote ${filepath}"
|
|
84
|
+
done
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# 4. Run post_bootstrap commands (as daytona user, not root)
|
|
64
89
|
# ---------------------------------------------------------------------------
|
|
65
90
|
POST_BOOTSTRAP=$(yq -r '.post_bootstrap // [] | .[]' "$CONFIG") || {
|
|
66
91
|
log "FATAL: Failed to parse post_bootstrap from .openthrottle.yml — check YAML syntax"
|
|
@@ -78,17 +103,17 @@ if [[ -n "$POST_BOOTSTRAP" ]]; then
|
|
|
78
103
|
fi
|
|
79
104
|
|
|
80
105
|
# ---------------------------------------------------------------------------
|
|
81
|
-
#
|
|
106
|
+
# 5. Configure agent settings (per-agent)
|
|
82
107
|
# ---------------------------------------------------------------------------
|
|
83
108
|
log "Configuring agent settings (${AGENT})"
|
|
84
109
|
|
|
85
|
-
#
|
|
110
|
+
# 5a. Install universal git hooks and seal git config (works for ALL agent runtimes)
|
|
86
111
|
git -C "$REPO" config core.hooksPath /opt/openthrottle/git-hooks
|
|
87
112
|
log "Installed git hooks (pre-push)"
|
|
88
113
|
# Seal .git/config to prevent agents from changing core.hooksPath
|
|
89
114
|
seal_file "${REPO}/.git/config" 2>/dev/null || true
|
|
90
115
|
|
|
91
|
-
#
|
|
116
|
+
# 5b. Per-agent configuration
|
|
92
117
|
case "$AGENT" in
|
|
93
118
|
claude)
|
|
94
119
|
SETTINGS_DIR="${SANDBOX_HOME}/.claude"
|
|
@@ -214,7 +239,7 @@ AIDEREOF
|
|
|
214
239
|
esac
|
|
215
240
|
|
|
216
241
|
# ---------------------------------------------------------------------------
|
|
217
|
-
#
|
|
242
|
+
# 6. Seal settings (immutable — only root can undo)
|
|
218
243
|
# ---------------------------------------------------------------------------
|
|
219
244
|
|
|
220
245
|
# Seal agent-specific settings
|
|
@@ -233,7 +258,7 @@ elif [[ "$AGENT" == "aider" ]] && [[ -f "${SANDBOX_HOME}/.aider.conf.yml" ]]; th
|
|
|
233
258
|
fi
|
|
234
259
|
|
|
235
260
|
# ---------------------------------------------------------------------------
|
|
236
|
-
#
|
|
261
|
+
# 7. Install skills into Claude's skill directory
|
|
237
262
|
# Baked-in skills from the image are installed first, then any repo-level
|
|
238
263
|
# skills override them (allows user customization).
|
|
239
264
|
# ---------------------------------------------------------------------------
|
|
@@ -265,12 +290,12 @@ if [[ -d "${REPO}/skills" ]]; then
|
|
|
265
290
|
fi
|
|
266
291
|
|
|
267
292
|
# ---------------------------------------------------------------------------
|
|
268
|
-
#
|
|
293
|
+
# 8. Fix ownership (skip sealed files — chattr prevents chown on them)
|
|
269
294
|
# ---------------------------------------------------------------------------
|
|
270
295
|
chown -R daytona:daytona "$SANDBOX_HOME" 2>/dev/null || true
|
|
271
296
|
|
|
272
297
|
# ---------------------------------------------------------------------------
|
|
273
|
-
#
|
|
298
|
+
# 9. Start heartbeat (resets autoStopInterval every 5 min)
|
|
274
299
|
#
|
|
275
300
|
# Daytona auto-stop only resets on Toolbox SDK API calls, NOT on internal
|
|
276
301
|
# process activity. The Toolbox agent runs inside the sandbox on port 63650.
|
|
@@ -296,7 +321,7 @@ chown -R daytona:daytona "$SANDBOX_HOME" 2>/dev/null || true
|
|
|
296
321
|
HEARTBEAT_PID=$!
|
|
297
322
|
|
|
298
323
|
# ---------------------------------------------------------------------------
|
|
299
|
-
#
|
|
324
|
+
# 10. Drop to daytona user and run the appropriate runner
|
|
300
325
|
# ---------------------------------------------------------------------------
|
|
301
326
|
log "Task: ${TASK_TYPE} #${WORK_ITEM} (agent: ${AGENT})"
|
|
302
327
|
|
|
@@ -87,6 +87,7 @@ jobs:
|
|
|
87
87
|
echo "resume_session=$RESUME_SESSION" >> "$GITHUB_OUTPUT"
|
|
88
88
|
|
|
89
89
|
- name: Validate config
|
|
90
|
+
id: config
|
|
90
91
|
run: |
|
|
91
92
|
SNAPSHOT=$(yq '.snapshot // ""' .openthrottle.yml)
|
|
92
93
|
if [[ -z "$SNAPSHOT" || "$SNAPSHOT" == "null" ]]; then
|
|
@@ -94,7 +95,6 @@ jobs:
|
|
|
94
95
|
exit 1
|
|
95
96
|
fi
|
|
96
97
|
echo "snapshot=$SNAPSHOT" >> "$GITHUB_OUTPUT"
|
|
97
|
-
id: config
|
|
98
98
|
|
|
99
99
|
- name: Activate snapshot (reactivates if idle >2 weeks)
|
|
100
100
|
env:
|
|
@@ -110,8 +110,9 @@ jobs:
|
|
|
110
110
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
111
111
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
112
112
|
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
|
|
113
|
+
# @@ENV_SECRETS@@ — scaffolder inserts project-specific secrets here
|
|
113
114
|
run: |
|
|
114
|
-
# Create ephemeral sandbox
|
|
115
|
+
# Create ephemeral sandbox
|
|
115
116
|
OUTPUT=$(daytona create \
|
|
116
117
|
--snapshot "${{ steps.config.outputs.snapshot }}" \
|
|
117
118
|
--auto-delete 0 \
|
|
@@ -132,6 +133,7 @@ jobs:
|
|
|
132
133
|
--env WORK_ITEM="${{ steps.work.outputs.item }}" \
|
|
133
134
|
--env TASK_TYPE="${{ steps.work.outputs.task_type }}" \
|
|
134
135
|
--env RESUME_SESSION="${{ steps.work.outputs.resume_session }}" \
|
|
136
|
+
# @@ENV_FLAGS@@ — scaffolder inserts --env flags for project secrets here
|
|
135
137
|
2>&1) || {
|
|
136
138
|
# Redact secrets from error output
|
|
137
139
|
SAFE_OUTPUT=$(echo "$OUTPUT" | sed \
|