ship-safe 9.2.0 → 9.2.1
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/cli/bin/ship-safe.js +5 -3
- package/cli/commands/shell.js +96 -5
- package/package.json +1 -1
package/cli/bin/ship-safe.js
CHANGED
|
@@ -663,7 +663,9 @@ How it works:
|
|
|
663
663
|
// No command + interactive TTY → drop into the REPL.
|
|
664
664
|
// Help banner is still available via `--help` and shown when stdin is piped.
|
|
665
665
|
if (process.argv.length === 2 && process.stdin.isTTY) {
|
|
666
|
-
|
|
666
|
+
// Await shell before exiting; do NOT fall through to program.parse() or it
|
|
667
|
+
// will print the help banner concurrently with the REPL banner.
|
|
668
|
+
shellCommand('.', {}).then(() => process.exit(0)).catch(() => process.exit(1));
|
|
667
669
|
} else if (process.argv.length === 2) {
|
|
668
670
|
console.log(banner);
|
|
669
671
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
@@ -707,6 +709,6 @@ if (process.argv.length === 2 && process.stdin.isTTY) {
|
|
|
707
709
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
708
710
|
console.log();
|
|
709
711
|
process.exit(0);
|
|
712
|
+
} else {
|
|
713
|
+
program.parse();
|
|
710
714
|
}
|
|
711
|
-
|
|
712
|
-
program.parse();
|
package/cli/commands/shell.js
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
import { createInterface } from 'readline';
|
|
22
22
|
import { execFileSync, spawnSync } from 'child_process';
|
|
23
|
+
import { readFileSync as fsReadFileSync } from 'fs';
|
|
23
24
|
import path from 'path';
|
|
25
|
+
import { fileURLToPath } from 'url';
|
|
24
26
|
import chalk from 'chalk';
|
|
25
27
|
import ora from 'ora';
|
|
26
28
|
import { autoDetectProvider } from '../providers/llm-provider.js';
|
|
@@ -31,6 +33,84 @@ import * as output from '../utils/output.js';
|
|
|
31
33
|
|
|
32
34
|
const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
33
35
|
|
|
36
|
+
// Read version from package.json so the banner stays in sync with releases.
|
|
37
|
+
const PKG_VERSION = (() => {
|
|
38
|
+
try {
|
|
39
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
const pkg = JSON.parse(fsReadFileSync(path.join(here, '..', '..', 'package.json'), 'utf8'));
|
|
41
|
+
return pkg.version;
|
|
42
|
+
} catch { return ''; }
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
const BANNER_LINES = [
|
|
46
|
+
' ███████╗██╗ ██╗██╗██████╗ ███████╗ █████╗ ███████╗███████╗',
|
|
47
|
+
' ██╔════╝██║ ██║██║██╔══██╗ ██╔════╝██╔══██╗██╔════╝██╔════╝',
|
|
48
|
+
' ███████╗███████║██║██████╔╝ ███████╗███████║█████╗ █████╗ ',
|
|
49
|
+
' ╚════██║██╔══██║██║██╔═══╝ ╚════██║██╔══██║██╔══╝ ██╔══╝ ',
|
|
50
|
+
' ███████║██║ ██║██║██║ ███████║██║ ██║██║ ███████╗',
|
|
51
|
+
' ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Characters drawn from the same box-drawing + block set the banner uses,
|
|
55
|
+
// so random frames look plausibly related rather than pure noise.
|
|
56
|
+
const GLITCH_CHARS = '█▓▒░╔╗╚╝╠╣╦╩╬═║┼┤├┬┴┐└┘┌─│╱╲╳';
|
|
57
|
+
|
|
58
|
+
function glitchFrame(line, reveal) {
|
|
59
|
+
// `reveal` is 0.0..1.0 — characters left of the reveal frontier are real,
|
|
60
|
+
// everything to the right is randomised from the glitch set.
|
|
61
|
+
return line.split('').map((ch, i) => {
|
|
62
|
+
const pos = i / line.length;
|
|
63
|
+
if (pos < reveal || ch === ' ') return ch;
|
|
64
|
+
return GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
|
|
65
|
+
}).join('');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise(r => setTimeout(r, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function shipSafeBannerAnimated() {
|
|
73
|
+
const cols = process.stdout.columns;
|
|
74
|
+
const narrow = typeof cols === 'number' && cols > 0 && cols < 70;
|
|
75
|
+
if (narrow) {
|
|
76
|
+
console.log(chalk.cyan.bold(' S H I P S A F E'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const lines = BANNER_LINES;
|
|
81
|
+
const FRAMES = 8; // glitch passes before the line locks in
|
|
82
|
+
const FRAME_MS = 18; // ms between frames within a line
|
|
83
|
+
const LINE_MS = 28; // ms between lines starting
|
|
84
|
+
|
|
85
|
+
// Print an empty placeholder so we know exactly where each line lives.
|
|
86
|
+
// We'll overwrite them in-place using cursor-up escape codes.
|
|
87
|
+
process.stdout.write('\n');
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
process.stdout.write(' '.repeat(line.length) + '\n');
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write('\n');
|
|
92
|
+
|
|
93
|
+
for (let li = 0; li < lines.length; li++) {
|
|
94
|
+
const line = lines[li];
|
|
95
|
+
// Move cursor up to this line's row (from the blank line after the banner).
|
|
96
|
+
const stepsUp = lines.length - li + 1;
|
|
97
|
+
process.stdout.write(`\x1B[${stepsUp}A`); // cursor up N
|
|
98
|
+
|
|
99
|
+
// Animate: reveal marches from 0 → 1 across FRAMES frames.
|
|
100
|
+
for (let f = 0; f <= FRAMES; f++) {
|
|
101
|
+
const reveal = f / FRAMES;
|
|
102
|
+
const scrambled = glitchFrame(line, reveal);
|
|
103
|
+
process.stdout.write('\r' + chalk.cyan(scrambled));
|
|
104
|
+
if (f < FRAMES) await sleep(FRAME_MS);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Write the final clean line and move back down to the bottom blank line.
|
|
108
|
+
process.stdout.write('\r' + chalk.cyan(line));
|
|
109
|
+
process.stdout.write(`\x1B[${stepsUp}B`); // cursor down N
|
|
110
|
+
await sleep(LINE_MS);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
34
114
|
export async function shellCommand(targetPath = '.', options = {}) {
|
|
35
115
|
const root = path.resolve(targetPath);
|
|
36
116
|
|
|
@@ -49,13 +129,18 @@ export async function shellCommand(targetPath = '.', options = {}) {
|
|
|
49
129
|
think: options.think || false,
|
|
50
130
|
});
|
|
51
131
|
|
|
132
|
+
// Branded entry: big wordmark, version + provider + cwd, then a CTA hint.
|
|
133
|
+
// Modeled on Claude Code / Gemini CLI / Aider — establish the tool's identity
|
|
134
|
+
// in the first second, then get out of the way.
|
|
52
135
|
console.log();
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
136
|
+
await shipSafeBannerAnimated();
|
|
137
|
+
// trailing blank line is already written by the animation loop
|
|
138
|
+
const ver = PKG_VERSION ? `v${PKG_VERSION}` : '';
|
|
139
|
+
const prov = state.provider ? chalk.cyan(state.provider.name) : chalk.yellow('no provider');
|
|
140
|
+
const cwd = chalk.gray(prettyCwd(root));
|
|
141
|
+
console.log(` ${chalk.bold(ver)} ${chalk.gray('·')} ${prov} ${chalk.gray('·')} ${cwd}`);
|
|
56
142
|
console.log();
|
|
57
|
-
console.log(chalk.gray('
|
|
58
|
-
console.log(chalk.gray(' /quit or Ctrl-D to exit.'));
|
|
143
|
+
console.log(chalk.gray(' /scan to find issues · /agent to fix them · /help for more'));
|
|
59
144
|
console.log();
|
|
60
145
|
|
|
61
146
|
const rl = createInterface({
|
|
@@ -407,6 +492,12 @@ function sevTag(sev) {
|
|
|
407
492
|
}
|
|
408
493
|
}
|
|
409
494
|
|
|
495
|
+
function prettyCwd(p) {
|
|
496
|
+
const home = process.env.HOME || '';
|
|
497
|
+
if (home && p.startsWith(home + '/')) return '~' + p.slice(home.length);
|
|
498
|
+
return p;
|
|
499
|
+
}
|
|
500
|
+
|
|
410
501
|
function gradeColor(score) {
|
|
411
502
|
if (score == null) return chalk.gray;
|
|
412
503
|
if (score >= 80) return chalk.green;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ship-safe",
|
|
3
|
-
"version": "9.2.
|
|
3
|
+
"version": "9.2.1",
|
|
4
4
|
"description": "AI-powered multi-agent security platform. 23 agents scan 80+ attack classes including AI integration supply chain (Vercel-class attacks), Hermes Agent deployments (ASI-01–ASI-10), tool registry poisoning, function-call injection, skill permission drift, and agent attestation. Ship Safe × Hermes Agent.",
|
|
5
5
|
"main": "cli/index.js",
|
|
6
6
|
"bin": {
|