@exreve/exk 1.0.0

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/appRunner.ts ADDED
@@ -0,0 +1,475 @@
1
+ import { spawn, ChildProcess } from 'child_process'
2
+ import fs from 'fs/promises'
3
+ import path from 'path'
4
+ import os from 'os'
5
+ import type { ProjectApp } from './shared/types'
6
+
7
+ /** Cross-platform: run a shell command (sh -c on Unix, cmd /c on Windows) */
8
+ function shellSpawnOpts(command: string): { shell: string; args: string[] } {
9
+ if (process.platform === 'win32') {
10
+ return { shell: process.env.COMSPEC || 'cmd.exe', args: ['/c', command] }
11
+ }
12
+ return { shell: 'sh', args: ['-c', command] }
13
+ }
14
+ import Fastify, { FastifyInstance } from 'fastify'
15
+ import fastifyStatic from '@fastify/static'
16
+
17
+ // ============ Types ============
18
+
19
+ export interface RunnerStats {
20
+ startTime: number
21
+ requests: number
22
+ errors: number
23
+ lastRequestTime?: number
24
+ memoryUsage?: NodeJS.MemoryUsage
25
+ }
26
+
27
+ export interface RunnerOutput {
28
+ type: 'stdout' | 'stderr' | 'system'
29
+ data: string
30
+ timestamp: number
31
+ }
32
+
33
+ export interface RunnerCallbacks {
34
+ onOutput?: (output: RunnerOutput) => void
35
+ onError?: (error: string) => void
36
+ onExit?: (code: number | null) => void
37
+ onStats?: (stats: RunnerStats) => void
38
+ }
39
+
40
+ // ============ Base Runner ============
41
+
42
+ export abstract class BaseRunner {
43
+ protected app: ProjectApp
44
+ protected projectPath: string
45
+ protected projectId: string
46
+ protected callbacks: RunnerCallbacks
47
+ protected stats: RunnerStats
48
+ protected process: ChildProcess | null = null
49
+ protected logFile: string
50
+
51
+ constructor(
52
+ app: ProjectApp,
53
+ projectPath: string,
54
+ projectId: string,
55
+ callbacks: RunnerCallbacks
56
+ ) {
57
+ this.app = app
58
+ this.projectPath = projectPath
59
+ this.projectId = projectId
60
+ this.callbacks = callbacks
61
+ this.stats = {
62
+ startTime: Date.now(),
63
+ requests: 0,
64
+ errors: 0,
65
+ }
66
+ this.logFile = path.join(os.homedir(), '.talk-to-code', 'app-logs', `${projectId}-${app.name}.log`)
67
+ }
68
+
69
+ abstract start(): Promise<{ success: boolean; pid?: number; error?: string }>
70
+ abstract stop(): Promise<{ success: boolean; error?: string }>
71
+ abstract getStats(): RunnerStats
72
+
73
+ protected async writeLog(type: 'stdout' | 'stderr' | 'system', data: string): Promise<void> {
74
+ const timestamp = new Date().toISOString()
75
+ const logLine = `[${timestamp}] [${type.toUpperCase()}] ${data}\n`
76
+
77
+ try {
78
+ await fs.appendFile(this.logFile, logLine, 'utf-8')
79
+ } catch (error) {
80
+ // Ignore log write errors
81
+ }
82
+
83
+ this.callbacks.onOutput?.({
84
+ type,
85
+ data,
86
+ timestamp: Date.now(),
87
+ })
88
+ }
89
+
90
+ protected updateStats(): void {
91
+ if (this.process) {
92
+ try {
93
+ const memUsage = process.memoryUsage()
94
+ this.stats.memoryUsage = memUsage
95
+ } catch {
96
+ // Ignore
97
+ }
98
+ }
99
+ this.callbacks.onStats?.(this.stats)
100
+ }
101
+ }
102
+
103
+ // ============ Static Frontend Runner ============
104
+
105
+ export class StaticFrontendRunner extends BaseRunner {
106
+ private fastify: FastifyInstance | null = null
107
+ private port: number
108
+
109
+ constructor(
110
+ app: ProjectApp,
111
+ projectPath: string,
112
+ projectId: string,
113
+ callbacks: RunnerCallbacks
114
+ ) {
115
+ super(app, projectPath, projectId, callbacks)
116
+ this.port = app.port || 3000
117
+ }
118
+
119
+ async start(): Promise<{ success: boolean; pid?: number; error?: string }> {
120
+ try {
121
+ await this.writeLog('system', `Starting static frontend server for ${this.app.name} on port ${this.port}`)
122
+
123
+ // Determine build directory (use buildDir from config if provided)
124
+ const buildDirName = this.app.buildDir || 'dist'
125
+ const buildDir = this.app.directory
126
+ ? path.join(this.projectPath, this.app.directory, buildDirName)
127
+ : path.join(this.projectPath, buildDirName)
128
+
129
+ // Check if specified build dir exists, fallback to dist, build, or public
130
+ let staticDir = buildDir
131
+ const dirsToTry = buildDirName !== 'dist' ? [buildDirName, 'dist', 'build', 'public'] : ['dist', 'build', 'public']
132
+
133
+ let found = false
134
+ for (const dirName of dirsToTry) {
135
+ const testDir = this.app.directory
136
+ ? path.join(this.projectPath, this.app.directory, dirName)
137
+ : path.join(this.projectPath, dirName)
138
+ try {
139
+ await fs.access(testDir)
140
+ staticDir = testDir
141
+ found = true
142
+ break
143
+ } catch {
144
+ continue
145
+ }
146
+ }
147
+
148
+ if (!found) {
149
+ return {
150
+ success: false,
151
+ error: `No build directory found. Tried: ${dirsToTry.join(', ')}`
152
+ }
153
+ }
154
+
155
+ await this.writeLog('system', `Serving static files from: ${staticDir}`)
156
+
157
+ // Create Fastify instance
158
+ this.fastify = Fastify({
159
+ logger: {
160
+ level: 'info',
161
+ transport: {
162
+ target: 'pino-pretty',
163
+ options: {
164
+ translateTime: 'HH:MM:ss Z',
165
+ ignore: 'pid,hostname',
166
+ },
167
+ },
168
+ },
169
+ })
170
+
171
+ // Register static file serving
172
+ await this.fastify.register(fastifyStatic, {
173
+ root: staticDir,
174
+ prefix: '/',
175
+ })
176
+
177
+ // Add request logging middleware
178
+ this.fastify.addHook('onRequest', async (request, reply) => {
179
+ this.stats.requests++
180
+ this.stats.lastRequestTime = Date.now()
181
+ this.updateStats()
182
+
183
+ await this.writeLog('stdout', `${request.method} ${request.url} - ${reply.statusCode}`)
184
+ })
185
+
186
+ // Add error handling
187
+ this.fastify.setErrorHandler(async (error: unknown, request, reply) => {
188
+ this.stats.errors++
189
+ this.updateStats()
190
+
191
+ const errorMessage = error instanceof Error ? error.message : String(error)
192
+ await this.writeLog('stderr', `Error: ${errorMessage} - ${request.method} ${request.url}`)
193
+
194
+ reply.status(500).send({ error: 'Internal Server Error' })
195
+ })
196
+
197
+ // Start server
198
+ await this.fastify.listen({ port: this.port, host: '0.0.0.0' })
199
+
200
+ await this.writeLog('system', `Static server started successfully on port ${this.port}`)
201
+
202
+ // Create a mock process for compatibility
203
+ this.process = {
204
+ pid: process.pid,
205
+ kill: (signal?: NodeJS.Signals) => {
206
+ this.stop()
207
+ },
208
+ } as ChildProcess
209
+
210
+ return {
211
+ success: true,
212
+ pid: process.pid,
213
+ }
214
+ } catch (error: any) {
215
+ await this.writeLog('stderr', `Failed to start static server: ${error.message}`)
216
+ this.callbacks.onError?.(error.message)
217
+ return {
218
+ success: false,
219
+ error: error.message || 'Failed to start static server'
220
+ }
221
+ }
222
+ }
223
+
224
+ async stop(): Promise<{ success: boolean; error?: string }> {
225
+ try {
226
+ if (this.fastify) {
227
+ await this.writeLog('system', `Stopping static server for ${this.app.name}`)
228
+ await this.fastify.close()
229
+ this.fastify = null
230
+ await this.writeLog('system', `Static server stopped`)
231
+ this.callbacks.onExit?.(0)
232
+ return { success: true }
233
+ }
234
+ return { success: true }
235
+ } catch (error: any) {
236
+ await this.writeLog('stderr', `Error stopping static server: ${error.message}`)
237
+ return {
238
+ success: false,
239
+ error: error.message || 'Failed to stop static server'
240
+ }
241
+ }
242
+ }
243
+
244
+ getStats(): RunnerStats {
245
+ return {
246
+ ...this.stats,
247
+ memoryUsage: process.memoryUsage(),
248
+ }
249
+ }
250
+ }
251
+
252
+ // ============ Backend Runner (Express/Fastify wrapper) ============
253
+
254
+ export class BackendRunner extends BaseRunner {
255
+ // process is inherited from BaseRunner as protected
256
+
257
+ async start(): Promise<{ success: boolean; pid?: number; error?: string }> {
258
+ try {
259
+ await this.writeLog('system', `Starting backend app: ${this.app.name}`)
260
+
261
+ // Determine working directory
262
+ const workingDir = this.app.directory
263
+ ? path.join(this.projectPath, this.app.directory)
264
+ : this.projectPath
265
+
266
+ // Check if directory exists
267
+ try {
268
+ await fs.access(workingDir)
269
+ } catch {
270
+ return {
271
+ success: false,
272
+ error: `App directory does not exist: ${this.app.directory || 'root'}`
273
+ }
274
+ }
275
+
276
+ // Prepare environment variables
277
+ const env = {
278
+ ...process.env,
279
+ ...this.app.env,
280
+ NODE_ENV: this.app.env?.NODE_ENV || process.env.NODE_ENV || 'development',
281
+ PORT: this.app.port?.toString() || process.env.PORT || '3000',
282
+ }
283
+
284
+ // Ensure log directory exists
285
+ const logDir = path.dirname(this.logFile)
286
+ await fs.mkdir(logDir, { recursive: true })
287
+
288
+ // Spawn the backend process (cross-platform shell)
289
+ const { shell, args } = shellSpawnOpts(this.app.startCommand)
290
+ this.process = spawn(shell, args, {
291
+ cwd: workingDir,
292
+ env,
293
+ stdio: ['ignore', 'pipe', 'pipe'],
294
+ detached: false,
295
+ })
296
+
297
+ if (!this.process.pid) {
298
+ return {
299
+ success: false,
300
+ error: 'Failed to spawn process'
301
+ }
302
+ }
303
+
304
+ await this.writeLog('system', `Backend process started with PID: ${this.process.pid}`)
305
+
306
+ // Capture stdout
307
+ this.process.stdout?.on('data', async (data: Buffer) => {
308
+ const output = data.toString()
309
+ await this.writeLog('stdout', output)
310
+ })
311
+
312
+ // Capture stderr
313
+ this.process.stderr?.on('data', async (data: Buffer) => {
314
+ const output = data.toString()
315
+ this.stats.errors++
316
+ this.updateStats()
317
+ await this.writeLog('stderr', output)
318
+ })
319
+
320
+ // Handle process exit
321
+ this.process.on('exit', async (code) => {
322
+ await this.writeLog('system', `Process exited with code ${code}`)
323
+ this.process = null
324
+ this.callbacks.onExit?.(code)
325
+ })
326
+
327
+ // Handle process errors
328
+ this.process.on('error', async (error) => {
329
+ this.stats.errors++
330
+ this.updateStats()
331
+ await this.writeLog('stderr', `Process error: ${error.message}`)
332
+ this.callbacks.onError?.(error.message)
333
+ })
334
+
335
+ // Start stats collection interval
336
+ const statsInterval = setInterval(() => {
337
+ if (this.process) {
338
+ this.updateStats()
339
+ } else {
340
+ clearInterval(statsInterval)
341
+ }
342
+ }, 5000)
343
+
344
+ return {
345
+ success: true,
346
+ pid: this.process.pid,
347
+ }
348
+ } catch (error: any) {
349
+ await this.writeLog('stderr', `Failed to start backend: ${error.message}`)
350
+ this.callbacks.onError?.(error.message)
351
+ return {
352
+ success: false,
353
+ error: error.message || 'Failed to start backend'
354
+ }
355
+ }
356
+ }
357
+
358
+ async stop(): Promise<{ success: boolean; error?: string }> {
359
+ try {
360
+ if (!this.process) {
361
+ return { success: true }
362
+ }
363
+
364
+ await this.writeLog('system', `Stopping backend app: ${this.app.name}`)
365
+
366
+ // Try custom stop command first
367
+ if (this.app.stopCommand) {
368
+ try {
369
+ const workingDir = this.app.directory
370
+ ? path.join(this.projectPath, this.app.directory)
371
+ : this.projectPath
372
+
373
+ const { shell: stopShell, args: stopArgs } = shellSpawnOpts(this.app.stopCommand)
374
+ const stopProcess = spawn(stopShell, stopArgs, {
375
+ cwd: workingDir,
376
+ stdio: 'ignore',
377
+ })
378
+
379
+ // Wait for graceful shutdown
380
+ await new Promise(resolve => setTimeout(resolve, 2000))
381
+
382
+ // If still running, kill it
383
+ if (this.process && this.process.pid) {
384
+ try {
385
+ process.kill(this.process.pid, 0) // Check if still alive
386
+ this.process.kill('SIGTERM')
387
+ await new Promise(resolve => setTimeout(resolve, 2000))
388
+ if (this.process && this.process.pid) {
389
+ try {
390
+ process.kill(this.process.pid, 0)
391
+ this.process.kill('SIGKILL')
392
+ } catch {
393
+ // Process already dead
394
+ }
395
+ }
396
+ } catch {
397
+ // Process already dead
398
+ }
399
+ }
400
+ } catch (error) {
401
+ // Fall back to killing process
402
+ if (this.process) {
403
+ this.process.kill('SIGTERM')
404
+ await new Promise(resolve => setTimeout(resolve, 2000))
405
+ if (this.process) {
406
+ this.process.kill('SIGKILL')
407
+ }
408
+ }
409
+ }
410
+ } else {
411
+ // Default: kill the process
412
+ this.process.kill('SIGTERM')
413
+ await new Promise(resolve => setTimeout(resolve, 2000))
414
+ if (this.process) {
415
+ this.process.kill('SIGKILL')
416
+ }
417
+ }
418
+
419
+ this.process = null
420
+ await this.writeLog('system', `Backend app stopped`)
421
+ return { success: true }
422
+ } catch (error: any) {
423
+ await this.writeLog('stderr', `Error stopping backend: ${error.message}`)
424
+ return {
425
+ success: false,
426
+ error: error.message || 'Failed to stop backend'
427
+ }
428
+ }
429
+ }
430
+
431
+ getStats(): RunnerStats {
432
+ return {
433
+ ...this.stats,
434
+ memoryUsage: this.process ? process.memoryUsage() : undefined,
435
+ }
436
+ }
437
+ }
438
+
439
+ // ============ Runner Factory ============
440
+
441
+ export function createRunner(
442
+ app: ProjectApp,
443
+ projectPath: string,
444
+ projectId: string,
445
+ callbacks: RunnerCallbacks
446
+ ): BaseRunner {
447
+ // Use explicit appType if provided, otherwise detect
448
+ if (app.appType === 'static-frontend') {
449
+ return new StaticFrontendRunner(app, projectPath, projectId, callbacks)
450
+ }
451
+
452
+ if (app.appType === 'backend') {
453
+ return new BackendRunner(app, projectPath, projectId, callbacks)
454
+ }
455
+
456
+ // Auto-detect based on framework and other indicators
457
+ const framework = app.framework?.toLowerCase() || ''
458
+ const type = app.type?.toLowerCase() || ''
459
+
460
+ // Static frontend apps
461
+ if (
462
+ framework.includes('react') ||
463
+ framework.includes('vue') ||
464
+ framework.includes('angular') ||
465
+ framework.includes('svelte') ||
466
+ (framework.includes('next') && type === 'http') ||
467
+ app.name.toLowerCase().includes('frontend') ||
468
+ app.name.toLowerCase().includes('client')
469
+ ) {
470
+ return new StaticFrontendRunner(app, projectPath, projectId, callbacks)
471
+ }
472
+
473
+ // Backend apps (Express, Fastify, etc.)
474
+ return new BackendRunner(app, projectPath, projectId, callbacks)
475
+ }
package/bin/exk ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ // exk CLI - entry point for npm global install
3
+ // Runs the TypeScript CLI via tsx
4
+
5
+ const { resolve, dirname } = require('path')
6
+ const { spawn } = require('child_process')
7
+ const fs = require('fs')
8
+
9
+ const cliDir = dirname(__filename)
10
+ const pkgDir = resolve(cliDir, '..')
11
+ const entryPoint = resolve(pkgDir, 'index.ts')
12
+
13
+ // Find tsx binary
14
+ let tsxBin = resolve(pkgDir, 'node_modules', '.bin', 'tsx')
15
+ if (!fs.existsSync(tsxBin)) {
16
+ // Walk up to find tsx in parent node_modules (npm global structure)
17
+ let dir = pkgDir
18
+ for (let i = 0; i < 10; i++) {
19
+ const candidate = resolve(dir, 'node_modules', '.bin', 'tsx')
20
+ if (fs.existsSync(candidate)) {
21
+ tsxBin = candidate
22
+ break
23
+ }
24
+ const parent = resolve(dir, '..')
25
+ if (parent === dir) break
26
+ dir = parent
27
+ }
28
+ }
29
+
30
+ const args = process.argv.slice(2)
31
+
32
+ const child = spawn(tsxBin, [entryPoint, ...args], {
33
+ stdio: 'inherit',
34
+ env: { ...process.env },
35
+ cwd: pkgDir
36
+ })
37
+
38
+ child.on('exit', (code) => {
39
+ process.exit(code || 0)
40
+ })
41
+
42
+ child.on('error', (err) => {
43
+ console.error('Failed to start exk:', err.message)
44
+ process.exit(1)
45
+ })
@@ -0,0 +1,177 @@
1
+ #!/bin/sh
2
+ # Don't use set -e, we want to handle errors gracefully
3
+
4
+ # Container entrypoint script for TTC CLI
5
+ # This script runs the TTC CLI daemon inside a container
6
+
7
+ echo "=== TTC Container Entrypoint ==="
8
+
9
+ # Check required environment variables
10
+ if [ -z "$TTC_AUTH_TOKEN" ]; then
11
+ echo "ERROR: TTC_AUTH_TOKEN environment variable is required"
12
+ exit 1
13
+ fi
14
+
15
+ if [ -z "$TTC_SERVER_URL" ]; then
16
+ echo "ERROR: TTC_SERVER_URL environment variable is required"
17
+ exit 1
18
+ fi
19
+
20
+ # Check for Node.js
21
+ if ! command -v node >/dev/null 2>&1; then
22
+ echo "ERROR: Node.js is required but not found. Please use a base image with Node.js (e.g., node:20-slim)"
23
+ exit 1
24
+ fi
25
+
26
+ echo "Node.js version: $(node --version)"
27
+
28
+ # TTC CLI directory (mounted from host)
29
+ TTC_DIR="/opt/ttc"
30
+
31
+ # Check if CLI is mounted - look for index.js in root or dist directory
32
+ CLI_FILE=""
33
+ if [ -f "$TTC_DIR/index.js" ]; then
34
+ CLI_FILE="$TTC_DIR/index.js"
35
+ elif [ -f "$TTC_DIR/dist/index.js" ]; then
36
+ CLI_FILE="$TTC_DIR/dist/index.js"
37
+ elif [ -f "$TTC_DIR/dist/cli/index.js" ]; then
38
+ CLI_FILE="$TTC_DIR/dist/cli/index.js"
39
+ elif [ -f "$TTC_DIR/index.ts" ]; then
40
+ echo "WARNING: Found TypeScript source file. CLI needs to be built first."
41
+ echo "Please build the CLI before mounting: cd cli && npm run build"
42
+ exit 1
43
+ else
44
+ echo "ERROR: TTC CLI not found at $TTC_DIR"
45
+ echo "The CLI directory must be mounted into the container."
46
+ echo "Expected files: $TTC_DIR/index.js, $TTC_DIR/dist/index.js, or $TTC_DIR/dist/cli/index.js"
47
+ echo "Example: docker run -v /path/to/cli:$TTC_DIR:ro ..."
48
+ echo ""
49
+ echo "Contents of $TTC_DIR:"
50
+ ls -la "$TTC_DIR/" 2>/dev/null || echo "Directory not accessible"
51
+ exit 1
52
+ fi
53
+
54
+ echo "TTC CLI found at $CLI_FILE"
55
+
56
+ # Determine config directory based on current user
57
+ # Try common locations for non-root users
58
+ CURRENT_USER=$(id -u)
59
+ CURRENT_HOME=$(eval echo ~$(id -un 2>/dev/null || echo "user"))
60
+
61
+ # Use HOME if available, otherwise try common locations
62
+ # NOTE: CLI uses '.talk-to-code' directory, not '.ttc'
63
+ if [ -n "$HOME" ] && [ -d "$HOME" ]; then
64
+ CONFIG_DIR="$HOME/.talk-to-code"
65
+ elif [ "$CURRENT_USER" = "1000" ] && [ -d "/home/node" ]; then
66
+ CONFIG_DIR="/home/node/.talk-to-code"
67
+ elif [ -d "$CURRENT_HOME" ]; then
68
+ CONFIG_DIR="$CURRENT_HOME/.talk-to-code"
69
+ else
70
+ # Fallback to /tmp if no home directory
71
+ CONFIG_DIR="/tmp/.talk-to-code"
72
+ fi
73
+
74
+ echo "Using config directory: $CONFIG_DIR"
75
+ mkdir -p "$CONFIG_DIR" || {
76
+ echo "WARNING: Could not create $CONFIG_DIR, trying /tmp/.talk-to-code"
77
+ CONFIG_DIR="/tmp/.talk-to-code"
78
+ mkdir -p "$CONFIG_DIR"
79
+ }
80
+
81
+ # Configure API URL
82
+ echo "{\"apiUrl\":\"$TTC_SERVER_URL\"}" > "$CONFIG_DIR/config.json" || {
83
+ echo "ERROR: Could not write config file"
84
+ exit 1
85
+ }
86
+
87
+ # Generate unique deviceId for this container (if not provided)
88
+ DEVICE_ID_FILE="$CONFIG_DIR/device-id.json"
89
+ if [ ! -f "$DEVICE_ID_FILE" ]; then
90
+ # Generate UUID using Node.js
91
+ DEVICE_ID=$(node -e "const {randomUUID}=require('crypto');console.log(randomUUID())" 2>/dev/null) || {
92
+ echo "ERROR: Could not generate device ID"
93
+ exit 1
94
+ }
95
+ echo "{\"deviceId\":\"$DEVICE_ID\"}" > "$DEVICE_ID_FILE" || {
96
+ echo "ERROR: Could not write device ID file"
97
+ exit 1
98
+ }
99
+ echo "Generated device ID: $DEVICE_ID"
100
+ else
101
+ DEVICE_ID=$(node -e "console.log(require('$DEVICE_ID_FILE').deviceId)" 2>/dev/null) || {
102
+ echo "ERROR: Could not read device ID file"
103
+ exit 1
104
+ }
105
+ echo "Using existing device ID: $DEVICE_ID"
106
+ fi
107
+
108
+ # Set container name from environment (if provided)
109
+ CONTAINER_NAME="${CONTAINER_NAME:-container}"
110
+ HOSTNAME="${HOSTNAME:-$CONTAINER_NAME}"
111
+
112
+ echo "Container name: $CONTAINER_NAME"
113
+ echo "Hostname: $HOSTNAME"
114
+ echo "Server URL: $TTC_SERVER_URL"
115
+ echo "Device ID: $DEVICE_ID"
116
+ echo ""
117
+
118
+ # Ensure common project directories exist
119
+ # This prevents ENOENT errors when SDK tries to spawn with these as cwd
120
+ # Create directories that work regardless of user (under /tmp)
121
+ COMMON_PROJECT_DIRS="/tmp/project /tmp/workspace /tmp/abc"
122
+ for dir in $COMMON_PROJECT_DIRS; do
123
+ if [ ! -d "$dir" ]; then
124
+ echo "Creating project directory: $dir"
125
+ mkdir -p "$dir" 2>/dev/null || echo "WARNING: Could not create $dir"
126
+ fi
127
+ done
128
+
129
+ # Create /home/abc if we're root (setup phase)
130
+ # This directory is commonly used as project path in containers
131
+ if [ "$(id -u)" = "0" ]; then
132
+ if [ ! -d "/home/abc" ]; then
133
+ echo "Creating /home/abc (as root)"
134
+ mkdir -p "/home/abc"
135
+ chown -R node:node "/home/abc"
136
+ echo "Created /home/abc with node:node ownership"
137
+ fi
138
+ else
139
+ # Not root - can't create in /home, but /tmp/abc is already created above
140
+ echo "Not running as root, /home/abc creation skipped (using /tmp/abc as fallback)"
141
+ fi
142
+ echo ""
143
+
144
+ # Change to CLI directory (use the directory containing the CLI file)
145
+ CLI_DIR=$(dirname "$CLI_FILE")
146
+ cd "$CLI_DIR" || {
147
+ echo "ERROR: Could not change to CLI directory: $CLI_DIR"
148
+ exit 1
149
+ }
150
+
151
+ # Verify we can read the CLI file
152
+ if [ ! -r "$CLI_FILE" ]; then
153
+ echo "ERROR: CLI file not readable: $CLI_FILE"
154
+ ls -la "$CLI_DIR/" || true
155
+ exit 1
156
+ fi
157
+
158
+ # Run TTC daemon (non-foreground mode for containers)
159
+ echo "Starting TTC CLI daemon..."
160
+ echo "Working directory: $(pwd)"
161
+ echo "CLI file: $CLI_FILE"
162
+ echo "User: $(id -un) ($(id -u))"
163
+ echo "HOME: $HOME"
164
+ echo "Node version: $(node --version)"
165
+ echo ""
166
+
167
+ # Execute the CLI daemon
168
+ # Note: exec replaces the shell process, so any code after this won't run
169
+ # If exec fails, the container will exit with the error code
170
+
171
+ # If running as root, switch to node user for security
172
+ if [ "$(id -u)" = "0" ]; then
173
+ echo "Switching to node user (UID 1000)..."
174
+ exec su-exec node:node node "$CLI_FILE" daemon
175
+ else
176
+ exec node "$CLI_FILE" daemon
177
+ fi