Skip to content

Commit f9c5da4

Browse files
konardclaude
andcommitted
feat: Sync JS implementation with Rust for timeline naming and virtual commands
JavaScript implementation updates: - Renamed "spine" terminology to "timeline" throughout output-blocks.js - SPINE constant → TIMELINE_MARKER (old name deprecated) - createSpineLine() → createTimelineLine() (old name deprecated) - createEmptySpineLine() → createEmptyTimelineLine() (old name deprecated) - Added virtual command visualization for Docker image pulls - When Docker isolation requires pulling an image, it's shown as `$ docker pull <image>` - Pull output is streamed in real-time with result markers (✓/✗) - Only displayed when image actually needs to be pulled (conditional display) - New API additions: - createVirtualCommandBlock() - for formatting virtual commands - createVirtualCommandResult() - for result markers - createTimelineSeparator() - for separator between virtual and user commands - dockerImageExists() - check if image is available locally - dockerPullImage() - pull with streaming output - createStartBlock({ deferCommand }) - defer command display for multi-step execution - Renamed "isolation backend" to "isolation environment" in docs and error messages - All deprecated items have backward-compatible aliases for smooth migration Fixes #70 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6bd59ef commit f9c5da4

File tree

8 files changed

+327
-89
lines changed

8 files changed

+327
-89
lines changed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Variables like `$packageName`, `$version`, `$repository` are captured and used i
9696

9797
### Automatic Logging
9898

99-
All command output is automatically saved to your system's temporary directory. Output uses a "status spine" format with clear visual distinction:
99+
All command output is automatically saved to your system's temporary directory. Output uses a "timeline" format with clear visual distinction:
100100

101101
```
102102
│ session abc-123-def-456-ghi
@@ -205,28 +205,28 @@ The `--isolated-user` option:
205205
- Runs the command as that user
206206
- Automatically deletes the user after the command completes (unless `--keep-user` is specified)
207207
- Requires sudo access without password (NOPASSWD configuration)
208-
- Works with screen and tmux isolation backends (not docker)
208+
- Works with screen and tmux isolation environments (not docker)
209209

210210
This is useful for:
211211

212212
- Running untrusted code in isolation
213213
- Testing with a clean user environment
214214
- Ensuring commands don't affect your user's files
215215

216-
#### Supported Backends
216+
#### Supported Isolation Environments
217217

218-
| Backend | Description | Installation |
219-
| -------- | ---------------------------------------------- | ---------------------------------------------------------- |
220-
| `screen` | GNU Screen terminal multiplexer | `apt install screen` / `brew install screen` |
221-
| `tmux` | Modern terminal multiplexer | `apt install tmux` / `brew install tmux` |
222-
| `docker` | Container isolation (requires --image) | [Docker Installation](https://docs.docker.com/get-docker/) |
223-
| `ssh` | Remote execution via SSH (requires --endpoint) | `apt install openssh-client` / `brew install openssh` |
218+
| Environment | Description | Installation |
219+
| ----------- | ---------------------------------------------- | ---------------------------------------------------------- |
220+
| `screen` | GNU Screen terminal multiplexer | `apt install screen` / `brew install screen` |
221+
| `tmux` | Modern terminal multiplexer | `apt install tmux` / `brew install tmux` |
222+
| `docker` | Container isolation (requires --image) | [Docker Installation](https://docs.docker.com/get-docker/) |
223+
| `ssh` | Remote execution via SSH (requires --endpoint) | `apt install openssh-client` / `brew install openssh` |
224224

225225
#### Isolation Options
226226

227227
| Option | Description |
228228
| -------------------------------- | --------------------------------------------------------- |
229-
| `--isolated, -i` | Isolation backend (screen, tmux, docker, ssh) |
229+
| `--isolated, -i` | Isolation environment (screen, tmux, docker, ssh) |
230230
| `--attached, -a` | Run in attached/foreground mode (default) |
231231
| `--detached, -d` | Run in detached/background mode |
232232
| `--session, -s` | Custom session/container name |
@@ -266,7 +266,7 @@ The tool works in any environment:
266266
- **No `gh-upload-log`?** - Issue can still be created with local log reference
267267
- **Repository not detected?** - Command runs normally with logging
268268
- **No permission to create issue?** - Skipped with a clear message
269-
- **Isolation backend not installed?** - Clear error message with installation instructions
269+
- **Isolation environment not installed?** - Clear error message with installation instructions
270270

271271
## Requirements
272272

@@ -335,7 +335,7 @@ You can create your own substitution patterns by placing a `substitutions.lino`
335335

336336
## Log File Format
337337

338-
Log files are saved as `start-command-{timestamp}-{random}.log` and contain the command output along with metadata. The console output uses a "status spine" format:
338+
Log files are saved as `start-command-{timestamp}-{random}.log` and contain the command output along with metadata. The console output uses a "timeline" format:
339339

340340
```
341341
│ session abc-123-def-456-ghi

js/src/bin/cli.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,12 +512,15 @@ async function runWithIsolation(
512512
}
513513

514514
// Print start block with session ID and isolation info
515+
// For docker isolation, defer command printing to allow virtual commands (like docker pull) to be shown first
516+
const deferCommand = environment === 'docker';
515517
console.log(
516518
createStartBlock({
517519
sessionId,
518520
timestamp: startTime,
519521
command: cmd,
520522
extraLines,
523+
deferCommand,
521524
})
522525
);
523526
console.log('');
@@ -558,8 +561,8 @@ async function runWithIsolation(
558561
let result;
559562

560563
if (environment) {
561-
// Run in isolation backend (screen, tmux, docker, ssh)
562-
// Note: Isolation backends currently use native spawn/execSync
564+
// Run in isolation environment (screen, tmux, docker, ssh)
565+
// Note: Isolation environments currently use native spawn/execSync
563566
// Future: Add command-stream support with raw() function for multiplexers
564567
result = await runIsolated(environment, cmd, {
565568
session: options.session,
@@ -571,7 +574,7 @@ async function runWithIsolation(
571574
autoRemoveDockerContainer: options.autoRemoveDockerContainer,
572575
});
573576
} else if (createdUser) {
574-
// Run directly as the created user (no isolation backend)
577+
// Run directly as the created user (no isolation environment)
575578
result = await runAsIsolatedUser(cmd, createdUser);
576579
} else {
577580
// This shouldn't happen in isolation mode, but handle gracefully

js/src/lib/args-parser.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const DEBUG =
3030
process.env.START_DEBUG === '1' || process.env.START_DEBUG === 'true';
3131

3232
/**
33-
* Valid isolation backends
33+
* Valid isolation environments
3434
*/
3535
const VALID_BACKENDS = ['screen', 'tmux', 'docker', 'ssh'];
3636

@@ -80,7 +80,7 @@ function generateUUID() {
8080
*/
8181
function parseArgs(args) {
8282
const wrapperOptions = {
83-
isolated: null, // Isolation backend: screen, tmux, docker, ssh
83+
isolated: null, // Isolation environment: screen, tmux, docker, ssh
8484
attached: false, // Run in attached mode
8585
detached: false, // Run in detached mode
8686
session: null, // Session name
@@ -375,11 +375,11 @@ function validateOptions(options) {
375375
);
376376
}
377377

378-
// Validate isolation backend
378+
// Validate isolation environment
379379
if (options.isolated !== null) {
380380
if (!VALID_BACKENDS.includes(options.isolated)) {
381381
throw new Error(
382-
`Invalid isolation backend: "${options.isolated}". Valid options are: ${VALID_BACKENDS.join(', ')}`
382+
`Invalid isolation environment: "${options.isolated}". Valid options are: ${VALID_BACKENDS.join(', ')}`
383383
);
384384
}
385385

js/src/lib/isolation.js

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,70 @@ function runInSsh(command, options = {}) {
618618
}
619619
}
620620

621+
/**
622+
* Check if a Docker image exists locally
623+
* @param {string} image - Docker image name
624+
* @returns {boolean} True if image exists locally
625+
*/
626+
function dockerImageExists(image) {
627+
try {
628+
execSync(`docker image inspect ${image}`, {
629+
stdio: ['pipe', 'pipe', 'pipe'],
630+
});
631+
return true;
632+
} catch {
633+
return false;
634+
}
635+
}
636+
637+
/**
638+
* Pull a Docker image with output streaming
639+
* Displays the pull operation as a virtual command in the timeline
640+
* @param {string} image - Docker image to pull
641+
* @returns {{success: boolean, output: string}} Pull result
642+
*/
643+
function dockerPullImage(image) {
644+
const {
645+
createVirtualCommandBlock,
646+
createVirtualCommandResult,
647+
createTimelineSeparator,
648+
} = require('./output-blocks');
649+
650+
// Print the virtual command line
651+
console.log(createVirtualCommandBlock(`docker pull ${image}`));
652+
console.log();
653+
654+
let output = '';
655+
let success = false;
656+
657+
try {
658+
// Run docker pull with inherited stdio for real-time output
659+
const result = spawnSync('docker', ['pull', image], {
660+
stdio: ['pipe', 'inherit', 'inherit'],
661+
});
662+
663+
success = result.status === 0;
664+
665+
if (result.stdout) {
666+
output = result.stdout.toString();
667+
}
668+
if (result.stderr) {
669+
output += result.stderr.toString();
670+
}
671+
} catch (err) {
672+
console.error(`Failed to run docker pull: ${err.message}`);
673+
output = err.message;
674+
success = false;
675+
}
676+
677+
// Print result marker and separator
678+
console.log();
679+
console.log(createVirtualCommandResult(success));
680+
console.log(createTimelineSeparator());
681+
682+
return { success, output };
683+
}
684+
621685
/**
622686
* Run command in Docker container
623687
* @param {string} command - Command to execute
@@ -644,6 +708,24 @@ function runInDocker(command, options = {}) {
644708

645709
const containerName = options.session || generateSessionName('docker');
646710

711+
// Check if image exists locally; if not, pull it as a virtual command
712+
if (!dockerImageExists(options.image)) {
713+
const pullResult = dockerPullImage(options.image);
714+
if (!pullResult.success) {
715+
return Promise.resolve({
716+
success: false,
717+
containerName: null,
718+
message: `Failed to pull Docker image: ${options.image}`,
719+
exitCode: 1,
720+
});
721+
}
722+
}
723+
724+
// Print the user command (this appears after any virtual commands like docker pull)
725+
const { createCommandLine } = require('./output-blocks');
726+
console.log(createCommandLine(command));
727+
console.log();
728+
647729
try {
648730
if (options.detached) {
649731
// Detached mode: docker run -d --name <name> [--user <user>] <image> <shell> -c '<command>'
@@ -753,8 +835,8 @@ function runInDocker(command, options = {}) {
753835
}
754836

755837
/**
756-
* Run command in the specified isolation backend
757-
* @param {string} backend - Isolation backend (screen, tmux, docker, ssh)
838+
* Run command in the specified isolation environment
839+
* @param {string} backend - Isolation environment (screen, tmux, docker, ssh)
758840
* @param {string} command - Command to execute
759841
* @param {object} options - Options
760842
* @returns {Promise<{success: boolean, message: string}>}
@@ -772,7 +854,7 @@ function runIsolated(backend, command, options = {}) {
772854
default:
773855
return Promise.resolve({
774856
success: false,
775-
message: `Unknown isolation backend: ${backend}`,
857+
message: `Unknown isolation environment: ${backend}`,
776858
});
777859
}
778860
}
@@ -885,7 +967,7 @@ function resetScreenVersionCache() {
885967
}
886968

887969
/**
888-
* Run command as an isolated user (without isolation backend)
970+
* Run command as an isolated user (without isolation environment)
889971
* Uses sudo -u to switch users
890972
* @param {string} cmd - Command to execute
891973
* @param {string} username - User to run as
@@ -990,4 +1072,7 @@ module.exports = {
9901072
resetScreenVersionCache,
9911073
canRunLinuxDockerImages,
9921074
getDefaultDockerImage,
1075+
// Docker image utilities for virtual command visualization
1076+
dockerImageExists,
1077+
dockerPullImage,
9931078
};

0 commit comments

Comments
 (0)