start-command 0.7.6 → 0.10.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/src/bin/cli.js CHANGED
@@ -15,12 +15,19 @@ const {
15
15
  } = require('../lib/args-parser');
16
16
  const {
17
17
  runIsolated,
18
+ runAsIsolatedUser,
18
19
  getTimestamp,
19
20
  createLogHeader,
20
21
  createLogFooter,
21
22
  writeLogFile,
22
23
  createLogPath,
23
24
  } = require('../lib/isolation');
25
+ const {
26
+ createIsolatedUser,
27
+ deleteUser,
28
+ hasSudoAccess,
29
+ getCurrentUserGroups,
30
+ } = require('../lib/user-manager');
24
31
 
25
32
  // Configuration from environment variables
26
33
  const config = {
@@ -211,31 +218,33 @@ function getToolVersion(toolName, versionFlag, verbose = false) {
211
218
  return firstLine || null;
212
219
  }
213
220
 
214
- /**
215
- * Print usage information
216
- */
221
+ /** Print usage information */
217
222
  function printUsage() {
218
- console.log('Usage: $ [options] [--] <command> [args...]');
219
- console.log(' $ <command> [args...]');
220
- console.log('');
221
- console.log('Options:');
222
- console.log(
223
- ' --isolated, -i <environment> Run in isolated environment (screen, tmux, docker)'
224
- );
225
- console.log(' --attached, -a Run in attached mode (foreground)');
226
- console.log(' --detached, -d Run in detached mode (background)');
227
- console.log(' --session, -s <name> Session name for isolation');
228
- console.log(
229
- ' --image <image> Docker image (required for docker isolation)'
230
- );
231
- console.log(' --version, -v Show version information');
232
- console.log('');
233
- console.log('Examples:');
234
- console.log(' $ echo "Hello World"');
235
- console.log(' $ bun test');
236
- console.log(' $ --isolated tmux -- bun start');
237
- console.log(' $ -i screen -d bun start');
238
- console.log(' $ --isolated docker --image oven/bun:latest -- bun install');
223
+ console.log(`Usage: $ [options] [--] <command> [args...]
224
+ $ <command> [args...]
225
+
226
+ Options:
227
+ --isolated, -i <env> Run in isolated environment (screen, tmux, docker)
228
+ --attached, -a Run in attached mode (foreground)
229
+ --detached, -d Run in detached mode (background)
230
+ --session, -s <name> Session name for isolation
231
+ --image <image> Docker image (required for docker isolation)
232
+ --isolated-user, -u [name] Create isolated user with same permissions
233
+ --keep-user Keep isolated user after command completes
234
+ --keep-alive, -k Keep isolation environment alive after command exits
235
+ --auto-remove-docker-container Auto-remove docker container after exit
236
+ --version, -v Show version information
237
+
238
+ Examples:
239
+ $ echo "Hello World"
240
+ $ bun test
241
+ $ --isolated tmux -- bun start
242
+ $ -i screen -d bun start
243
+ $ --isolated docker --image oven/bun:latest -- bun install
244
+ $ --isolated-user -- npm test # Create isolated user
245
+ $ -u myuser -- npm start # Custom username
246
+ $ -i screen --isolated-user -- npm test # Combine with process isolation
247
+ $ --isolated-user --keep-user -- npm start`);
239
248
  console.log('');
240
249
  console.log('Piping with $:');
241
250
  console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
@@ -305,8 +314,8 @@ if (!config.disableSubstitutions) {
305
314
 
306
315
  // Main execution
307
316
  (async () => {
308
- // Check if running in isolation mode
309
- if (hasIsolation(wrapperOptions)) {
317
+ // Check if running in isolation mode or with user isolation
318
+ if (hasIsolation(wrapperOptions) || wrapperOptions.user) {
310
319
  await runWithIsolation(wrapperOptions, command);
311
320
  } else {
312
321
  await runDirect(command);
@@ -324,43 +333,112 @@ async function runWithIsolation(options, cmd) {
324
333
  const startTime = getTimestamp();
325
334
 
326
335
  // Create log file path
327
- const logFilePath = createLogPath(environment);
336
+ const logFilePath = createLogPath(environment || 'direct');
328
337
 
329
338
  // Get session name (will be generated by runIsolated if not provided)
330
339
  const sessionName =
331
340
  options.session ||
332
- `${environment}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
341
+ `${environment || 'start'}-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
342
+
343
+ // Handle --isolated-user option: create a new user with same permissions
344
+ let createdUser = null;
345
+
346
+ if (options.user) {
347
+ // Check for sudo access
348
+ if (!hasSudoAccess()) {
349
+ console.error(
350
+ 'Error: --isolated-user requires sudo access without password.'
351
+ );
352
+ console.error(
353
+ 'Configure NOPASSWD in sudoers or run with appropriate permissions.'
354
+ );
355
+ process.exit(1);
356
+ }
357
+
358
+ // Get current user groups to show what will be inherited
359
+ const currentGroups = getCurrentUserGroups();
360
+ const importantGroups = ['sudo', 'docker', 'wheel', 'admin'].filter((g) =>
361
+ currentGroups.includes(g)
362
+ );
363
+
364
+ console.log(`[User Isolation] Creating new user with same permissions...`);
365
+ if (importantGroups.length > 0) {
366
+ console.log(
367
+ `[User Isolation] Inheriting groups: ${importantGroups.join(', ')}`
368
+ );
369
+ }
370
+
371
+ // Create the isolated user
372
+ const userResult = createIsolatedUser(options.userName);
373
+ if (!userResult.success) {
374
+ console.error(
375
+ `Error: Failed to create isolated user: ${userResult.message}`
376
+ );
377
+ process.exit(1);
378
+ }
379
+
380
+ createdUser = userResult.username;
381
+ console.log(`[User Isolation] Created user: ${createdUser}`);
382
+ if (userResult.groups && userResult.groups.length > 0) {
383
+ console.log(
384
+ `[User Isolation] User groups: ${userResult.groups.join(', ')}`
385
+ );
386
+ }
387
+ if (options.keepUser) {
388
+ console.log(`[User Isolation] User will be kept after command completes`);
389
+ }
390
+ console.log('');
391
+ }
333
392
 
334
393
  // Print start message (unified format)
335
394
  console.log(`[${startTime}] Starting: ${cmd}`);
336
395
  console.log('');
337
396
 
338
397
  // Log isolation info
339
- console.log(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
398
+ if (environment) {
399
+ console.log(`[Isolation] Environment: ${environment}, Mode: ${mode}`);
400
+ }
340
401
  if (options.session) {
341
402
  console.log(`[Isolation] Session: ${options.session}`);
342
403
  }
343
404
  if (options.image) {
344
405
  console.log(`[Isolation] Image: ${options.image}`);
345
406
  }
407
+ if (createdUser) {
408
+ console.log(`[Isolation] User: ${createdUser} (isolated)`);
409
+ }
346
410
  console.log('');
347
411
 
348
412
  // Create log content
349
413
  let logContent = createLogHeader({
350
414
  command: cmd,
351
- environment,
415
+ environment: environment || 'direct',
352
416
  mode,
353
417
  sessionName,
354
418
  image: options.image,
419
+ user: createdUser,
355
420
  startTime,
356
421
  });
357
422
 
358
- // Run in isolation
359
- const result = await runIsolated(environment, cmd, {
360
- session: options.session,
361
- image: options.image,
362
- detached: mode === 'detached',
363
- });
423
+ let result;
424
+
425
+ if (environment) {
426
+ // Run in isolation backend (screen, tmux, docker)
427
+ result = await runIsolated(environment, cmd, {
428
+ session: options.session,
429
+ image: options.image,
430
+ detached: mode === 'detached',
431
+ user: createdUser,
432
+ keepAlive: options.keepAlive,
433
+ autoRemoveDockerContainer: options.autoRemoveDockerContainer,
434
+ });
435
+ } else if (createdUser) {
436
+ // Run directly as the created user (no isolation backend)
437
+ result = await runAsIsolatedUser(cmd, createdUser);
438
+ } else {
439
+ // This shouldn't happen in isolation mode, but handle gracefully
440
+ result = { success: false, message: 'No isolation configuration provided' };
441
+ }
364
442
 
365
443
  // Get exit code
366
444
  const exitCode =
@@ -382,6 +460,23 @@ async function runWithIsolation(options, cmd) {
382
460
  console.log(`Exit code: ${exitCode}`);
383
461
  console.log(`Log saved: ${logFilePath}`);
384
462
 
463
+ // Cleanup: delete the created user if we created one (unless --keep-user)
464
+ if (createdUser && !options.keepUser) {
465
+ console.log('');
466
+ console.log(`[User Isolation] Cleaning up user: ${createdUser}`);
467
+ const deleteResult = deleteUser(createdUser, { removeHome: true });
468
+ if (deleteResult.success) {
469
+ console.log(`[User Isolation] User deleted successfully`);
470
+ } else {
471
+ console.log(`[User Isolation] Warning: ${deleteResult.message}`);
472
+ }
473
+ } else if (createdUser && options.keepUser) {
474
+ console.log('');
475
+ console.log(
476
+ `[User Isolation] Keeping user: ${createdUser} (use 'sudo userdel -r ${createdUser}' to delete)`
477
+ );
478
+ }
479
+
385
480
  process.exit(exitCode);
386
481
  }
387
482
 
@@ -6,11 +6,15 @@
6
6
  * 2. $ [wrapper-options] command [command-options]
7
7
  *
8
8
  * Wrapper Options:
9
- * --isolated, -i <backend> Run in isolated environment (screen, tmux, docker)
10
- * --attached, -a Run in attached mode (foreground)
11
- * --detached, -d Run in detached mode (background)
12
- * --session, -s <name> Session name for isolation
13
- * --image <image> Docker image (required for docker isolation)
9
+ * --isolated, -i <backend> Run in isolated environment (screen, tmux, docker)
10
+ * --attached, -a Run in attached mode (foreground)
11
+ * --detached, -d Run in detached mode (background)
12
+ * --session, -s <name> Session name for isolation
13
+ * --image <image> Docker image (required for docker isolation)
14
+ * --isolated-user, -u [username] Create isolated user with same permissions (auto-generated name if not specified)
15
+ * --keep-user Keep isolated user after command completes (don't delete)
16
+ * --keep-alive, -k Keep isolation environment alive after command exits
17
+ * --auto-remove-docker-container Automatically remove docker container after exit (disabled by default)
14
18
  */
15
19
 
16
20
  // Debug mode from environment
@@ -34,6 +38,11 @@ function parseArgs(args) {
34
38
  detached: false, // Run in detached mode
35
39
  session: null, // Session name
36
40
  image: null, // Docker image
41
+ user: false, // Create isolated user
42
+ userName: null, // Optional custom username for isolated user
43
+ keepUser: false, // Keep isolated user after command completes (don't delete)
44
+ keepAlive: false, // Keep environment alive after command exits
45
+ autoRemoveDockerContainer: false, // Auto-remove docker container after exit
37
46
  };
38
47
 
39
48
  let commandArgs = [];
@@ -171,6 +180,47 @@ function parseOption(args, index, options) {
171
180
  return 1;
172
181
  }
173
182
 
183
+ // --isolated-user or -u [optional-username] - creates isolated user with same permissions
184
+ if (arg === '--isolated-user' || arg === '-u') {
185
+ options.user = true;
186
+ // Check if next arg is an optional username (not starting with -)
187
+ if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
188
+ // Check if next arg looks like a username (not a command)
189
+ const nextArg = args[index + 1];
190
+ // If next arg matches username format, consume it
191
+ if (/^[a-zA-Z0-9_-]+$/.test(nextArg) && nextArg.length <= 32) {
192
+ options.userName = nextArg;
193
+ return 2;
194
+ }
195
+ }
196
+ return 1;
197
+ }
198
+
199
+ // --isolated-user=<value>
200
+ if (arg.startsWith('--isolated-user=')) {
201
+ options.user = true;
202
+ options.userName = arg.split('=')[1];
203
+ return 1;
204
+ }
205
+
206
+ // --keep-user - keep isolated user after command completes
207
+ if (arg === '--keep-user') {
208
+ options.keepUser = true;
209
+ return 1;
210
+ }
211
+
212
+ // --keep-alive or -k
213
+ if (arg === '--keep-alive' || arg === '-k') {
214
+ options.keepAlive = true;
215
+ return 1;
216
+ }
217
+
218
+ // --auto-remove-docker-container
219
+ if (arg === '--auto-remove-docker-container') {
220
+ options.autoRemoveDockerContainer = true;
221
+ return 1;
222
+ }
223
+
174
224
  // Not a recognized wrapper option
175
225
  return 0;
176
226
  }
@@ -213,6 +263,46 @@ function validateOptions(options) {
213
263
  if (options.image && options.isolated !== 'docker') {
214
264
  throw new Error('--image option is only valid with --isolated docker');
215
265
  }
266
+
267
+ // Keep-alive is only valid with isolation
268
+ if (options.keepAlive && !options.isolated) {
269
+ throw new Error('--keep-alive option is only valid with --isolated');
270
+ }
271
+
272
+ // Auto-remove-docker-container is only valid with docker isolation
273
+ if (options.autoRemoveDockerContainer && options.isolated !== 'docker') {
274
+ throw new Error(
275
+ '--auto-remove-docker-container option is only valid with --isolated docker'
276
+ );
277
+ }
278
+
279
+ // User isolation validation
280
+ if (options.user) {
281
+ // User isolation is not supported with Docker (Docker has its own user mechanism)
282
+ if (options.isolated === 'docker') {
283
+ throw new Error(
284
+ '--isolated-user is not supported with Docker isolation. Docker uses its own user namespace for isolation.'
285
+ );
286
+ }
287
+ // Validate custom username if provided
288
+ if (options.userName) {
289
+ if (!/^[a-zA-Z0-9_-]+$/.test(options.userName)) {
290
+ throw new Error(
291
+ `Invalid username format for --isolated-user: "${options.userName}". Username should contain only letters, numbers, hyphens, and underscores.`
292
+ );
293
+ }
294
+ if (options.userName.length > 32) {
295
+ throw new Error(
296
+ `Username too long for --isolated-user: "${options.userName}". Maximum length is 32 characters.`
297
+ );
298
+ }
299
+ }
300
+ }
301
+
302
+ // Keep-user validation
303
+ if (options.keepUser && !options.user) {
304
+ throw new Error('--keep-user option is only valid with --isolated-user');
305
+ }
216
306
  }
217
307
 
218
308
  /**