Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/cli/src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ import { createWebUI } from '@tinyclaw/web';
import QRCode from 'qrcode';
import { showBanner } from '../ui/banner.js';
import { theme } from '../ui/theme.js';
import { existsSync, readFileSync } from 'fs';

/**
* Detect if running inside a Docker container or CI environment.
*/
function isRunningInContainer(): boolean {
if (process.env.CI || process.env.CONTAINER || process.env.DOCKER_CONTAINER) {
return true;
}
try {
if (existsSync('/.dockerenv')) {
return true;
}
} catch { /* ignore */ }
try {
const cgroup = readFileSync('/proc/1/cgroup', 'utf8');
if (/docker|containerd|kubepods|lxc|podman/i.test(cgroup)) {
return true;
}
} catch { /* ignore */ }
return false;
}

/**
* Copy text to the system clipboard.
Expand Down Expand Up @@ -205,6 +227,29 @@ export async function setupCommand(): Promise<void> {

showBanner();

// --- Container environment warning ----------------------------------

if (isRunningInContainer()) {
p.note(
theme.warn('Container Environment Detected') + '\n\n' +
'Interactive CLI setup may not work properly in Docker/containers.\n' +
'If prompts freeze or fail, cancel and run:\n\n' +
' ' + theme.cmd('tinyclaw setup --docker') + '\n\n' +
'Or use ' + theme.cmd('--web') + ' for browser-based setup.',
'Docker/Container'
);

const continueAnyway = await p.confirm({
message: 'Continue with interactive setup anyway?',
initialValue: false,
});

if (p.isCancel(continueAnyway) || !continueAnyway) {
p.outro(theme.dim('Setup cancelled. Use --docker or --web flag for container environments.'));
process.exit(0);
}
}

const secretsManager = await SecretsManager.create();
const configManager = await ConfigManager.create();

Expand Down
64 changes: 53 additions & 11 deletions src/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,54 @@
* Lightweight argument router. No framework — just process.argv.
*
* Usage:
* tinyclaw Show banner + help
* tinyclaw setup Interactive first-time setup wizard
* tinyclaw setup --web Start web onboarding at /setup
* tinyclaw start Boot the agent (requires setup first)
* tinyclaw purge Wipe all data for a fresh install
* tinyclaw --version Print version
* tinyclaw --help Show help
* tinyclaw Show banner + help
* tinyclaw setup Interactive first-time setup wizard
* tinyclaw setup --web Start web onboarding at /setup
* tinyclaw setup --docker Force web mode for Docker/container environments
* tinyclaw start Boot the agent (requires setup first)
* tinyclaw purge Wipe all data for a fresh install
* tinyclaw --version Print version
* tinyclaw --help Show help
*/

import { logger } from '@tinyclaw/logger';
import { getVersion, showBanner } from './ui/banner.js';
import { theme } from './ui/theme.js';
import { existsSync, readFileSync } from 'fs';

// ── Docker Detection ───────────────────────────────────────────────────

/**
* Detect if running inside a Docker container or CI environment.
* Checks for common indicators: .dockerenv, cgroup, CI env vars, container-specific env vars.
*/
function detectDockerEnvironment(): boolean {
// Check for explicit CI/container environment variables
if (process.env.CI || process.env.CONTAINER || process.env.DOCKER_CONTAINER) {
return true;
}

// Check for .dockerenv file (older Docker versions)
try {
if (existsSync('/.dockerenv')) {
return true;
}
} catch {
// Ignore errors
}

// Check cgroup for container indicators
try {
const cgroup = readFileSync('/proc/1/cgroup', 'utf8');
if (/docker|containerd|kubepods|lxc|podman/i.test(cgroup)) {
return true;
}
} catch {
// Ignore errors (file may not exist or be readable)
}

return false;
}

// ── Help text ──────────────────────────────────────────────────────────

Expand All @@ -27,9 +63,9 @@ function showHelp(): void {
console.log(` ${theme.cmd('tinyclaw')} ${theme.dim('<command>')}`);
console.log();
console.log(` ${theme.label('Commands')}`);
console.log(
` ${theme.cmd('setup')} Interactive setup wizard (use --web for browser onboarding)`,
);
console.log(` ${theme.cmd('setup')} Interactive setup wizard`);
console.log(` ${theme.dim('Use --web for browser onboarding')}`);
console.log(` ${theme.dim('Use --docker for Docker/container environments (auto-detected)')}`);
console.log(` ${theme.cmd('start')} Start the Tiny Claw agent`);
console.log(` ${theme.cmd('config')} Manage models, providers, and settings`);
console.log(` ${theme.cmd('seed')} Show your Tiny Claw's soul seed`);
Expand All @@ -39,6 +75,8 @@ function showHelp(): void {
);
console.log();
console.log(` ${theme.label('Options')}`);
console.log(` ${theme.dim('--docker')} Force web-based setup for Docker/container environments`);
console.log(` ${theme.dim('--web')} Use web-based setup instead of CLI wizard`);
console.log(` ${theme.dim('--verbose')} Show debug-level logs during start`);
console.log(` ${theme.dim('--version, -v')} Show version number`);
console.log(` ${theme.dim('--help, -h')} Show this help message`);
Expand All @@ -53,7 +91,11 @@ async function main(): Promise<void> {

switch (command) {
case 'setup': {
if (args.includes('--web')) {
// Detect Docker/container environment and auto-route to web mode
const isDocker = args.includes('--docker') || detectDockerEnvironment();
const isWeb = args.includes('--web') || isDocker;

if (isWeb) {
// Web setup goes through supervisor so the restart mechanism works
const { supervisedStart } = await import('./supervisor.js');
await supervisedStart();
Expand Down
Loading