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 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
- copyFileSync(src, dest);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-openthrottle",
3
- "version": "1.3.3",
3
+ "version": "1.3.4",
4
4
  "description": "Set up openthrottle in any Node.js project — agent-agnostic, config-driven.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,32 @@ LINT_CMD=$(read_config '.lint' '')
60
60
  AGENT=$(read_config '.agent' 'claude')
61
61
 
62
62
  # ---------------------------------------------------------------------------
63
- # 3. Run post_bootstrap commands (as daytona user, not root)
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
- # 4. Configure agent settings (per-agent)
106
+ # 5. Configure agent settings (per-agent)
82
107
  # ---------------------------------------------------------------------------
83
108
  log "Configuring agent settings (${AGENT})"
84
109
 
85
- # 4a. Install universal git hooks and seal git config (works for ALL agent runtimes)
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
- # 4b. Per-agent configuration
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
- # 5. Seal settings (immutable — only root can undo)
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
- # 6. Install skills into Claude's skill directory
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
- # 7. Fix ownership (skip sealed files — chattr prevents chown on them)
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
- # 8. Start heartbeat (resets autoStopInterval every 5 min)
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
- # 9. Drop to daytona user and run the appropriate runner
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 (capture both stdout and stderr for error reporting)
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 \