Files
nanoclaw/setup/set-env.ts
Koshkoshinsk 712a0e1e01 feat(new-setup): wrap node/docker installs and add generic set-env step
Adds three allowlist-friendly setup helpers so /new-setup and /new-setup-2
don't hit unmatchable commands during a fresh install:

- setup/install-node.sh — idempotent Node 22 install wrapper (macOS via brew,
  Linux via NodeSource + apt). Replaces the raw `curl | sudo -E bash -` flow
  whose stdin-consuming `bash -` segment can't be pre-approved.
- setup/install-docker.sh — same pattern for Docker (brew --cask on macOS,
  get.docker.com on Linux + usermod).
- setup/set-env.ts — generic `--step set-env` that writes KEY=VALUE to .env
  (and optionally syncs to data/env/env) so channel-install flows don't
  invent `grep && sed && rm` pipelines, which split at each && and can't be
  tightly allowlisted.

new-setup-2's Telegram path now uses set-env for TELEGRAM_BOT_TOKEN and
explicitly skips /add-telegram's Credentials section. new-setup step 1 and
step 2 now call the install wrappers; the raw curl/apt entries are gone from
the allowed-tools list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:19:09 +00:00

78 lines
2.2 KiB
TypeScript

/**
* Step: set-env — Write or update a KEY=VALUE in .env, with optional sync to
* data/env/env (the container-mounted copy).
*
* Usage:
* pnpm exec tsx setup/index.ts --step set-env -- \
* --key TELEGRAM_BOT_TOKEN --value "<token>" [--sync-container]
*
* Exists so channel-install flows don't have to invent grep/sed/rm pipelines
* (which can't be allowlisted tightly — sed can read any file, and each
* segment of an && chain is matched separately).
*
* Logs the key but never the value.
*/
import fs from 'fs';
import path from 'path';
import { log } from '../src/log.js';
import { emitStatus } from './status.js';
export async function run(args: string[]): Promise<void> {
const keyIdx = args.indexOf('--key');
const valueIdx = args.indexOf('--value');
const syncContainer = args.includes('--sync-container');
if (keyIdx === -1 || !args[keyIdx + 1]) {
throw new Error('--key <KEY> is required');
}
if (valueIdx === -1 || args[valueIdx + 1] === undefined) {
throw new Error('--value <VALUE> is required');
}
const key = args[keyIdx + 1];
const value = args[valueIdx + 1];
if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
throw new Error(`Invalid env key: ${key} (must be UPPER_SNAKE_CASE)`);
}
const projectRoot = process.cwd();
const envFile = path.join(projectRoot, '.env');
let content = '';
if (fs.existsSync(envFile)) {
content = fs.readFileSync(envFile, 'utf-8');
}
const lineRegex = new RegExp(`^${key}=.*$`, 'm');
const newLine = `${key}=${value}`;
const existed = lineRegex.test(content);
if (existed) {
content = content.replace(lineRegex, newLine);
} else {
const sep = content && !content.endsWith('\n') ? '\n' : '';
content = content + sep + newLine + '\n';
}
fs.writeFileSync(envFile, content);
log.info('Updated .env', { key, existed });
let synced = false;
if (syncContainer) {
const dataEnvDir = path.join(projectRoot, 'data', 'env');
fs.mkdirSync(dataEnvDir, { recursive: true });
fs.copyFileSync(envFile, path.join(dataEnvDir, 'env'));
synced = true;
log.info('Synced .env to container mount', { path: 'data/env/env' });
}
emitStatus('SET_ENV', {
KEY: key,
EXISTED: existed,
SYNCED_TO_CONTAINER: synced,
STATUS: 'success',
});
}