shell-logo 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rafael Chiti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # shell-logo
2
+
3
+ Now that we are all coding with 1232 terminals open, is kind of hard to quickly find the one you want at a glance.
4
+ Adding a small configurable logo to quickly spot each project.
5
+
6
+ ![screen-capture](assets/mosaic.jpeg)
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm i -g shell-logo
12
+ # or
13
+ pnpm add -g shell-logo
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ shell-logo
20
+ ```
21
+
22
+ Launches an interactive menu where you can:
23
+
24
+ 1. **Generate** - create a new `.shell-logo.json` config by picking text, colors, and a font
25
+ 2. **Run** - display the logo from an existing `.shell-logo.json` in the current directory
26
+
27
+ Once the logo is displayed, use arrow keys to cycle through themes and `q` to quit.
28
+
29
+ ## Config
30
+
31
+ The generated `.shell-logo.json` looks like this:
32
+
33
+ ```json
34
+ {
35
+ "text": "My Project",
36
+ "font": "Standard",
37
+ "colors": ["#ff6b6b", "#feca57"],
38
+ "padding": 1
39
+ }
40
+ ```
41
+
42
+ | Field | Type | Default | Description |
43
+ | --------- | ---------- | ----------------------------------- | --------------------------------- |
44
+ | `text` | `string` | _(required)_ | The text to render as ASCII art |
45
+ | `font` | `string` | `"Standard"` | Figlet font name |
46
+ | `colors` | `string[]` | `["#ff6b6b", "#feca57", "#48dbfb"]` | Hex colors for the gradient (>=2) |
47
+ | `padding` | `number` | `1` | Vertical padding above the logo |
48
+
49
+ See `examples/` for more config samples.
50
+
51
+ ## Publishing to npm
52
+
53
+ ```bash
54
+ # 1. Make sure you're logged in
55
+ npm login
56
+
57
+ # 2. Do a dry run to verify what gets packed
58
+ npm pack --dry-run
59
+
60
+ # 3. Publish
61
+ npm publish
62
+
63
+ # For subsequent releases, bump the version first
64
+ npm version patch # or minor / major
65
+ npm publish
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,6 @@
1
+ {
2
+ "text": "HELLO",
3
+ "colors": ["#ff6b6b", "#feca57", "#48dbfb"],
4
+ "font": "Standard",
5
+ "padding": 1
6
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "shell-logo",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Render colorful ASCII art logos in the terminal from a config file",
6
+ "license": "MIT",
7
+ "author": "Rafael Chiti",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/rafaelchiti/shell-logo.git"
11
+ },
12
+ "homepage": "https://github.com/rafaelchiti/shell-logo",
13
+ "bugs": {
14
+ "url": "https://github.com/rafaelchiti/shell-logo/issues"
15
+ },
16
+ "bin": {
17
+ "shell-logo": "./src/index.js"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "terminal",
22
+ "ascii-art",
23
+ "logo",
24
+ "figlet",
25
+ "gradient"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "files": [
31
+ "src/",
32
+ "examples/"
33
+ ],
34
+ "dependencies": {
35
+ "chalk": "^5.4.1",
36
+ "figlet": "^1.8.0",
37
+ "@clack/prompts": "^0.11.0",
38
+ "gradient-string": "^3.0.0"
39
+ },
40
+ "scripts": {
41
+ "start": "node src/index.js"
42
+ }
43
+ }
package/src/config.js ADDED
@@ -0,0 +1,91 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+
5
+ export const DEFAULTS = {
6
+ colors: ['#ff6b6b', '#feca57', '#48dbfb'],
7
+ font: 'Standard',
8
+ };
9
+
10
+ export function tryLoadConfig() {
11
+ const configPath = join(process.cwd(), '.shell-logo.json');
12
+
13
+ let raw;
14
+ try {
15
+ raw = readFileSync(configPath, 'utf-8');
16
+ } catch {
17
+ return null;
18
+ }
19
+
20
+ let config;
21
+ try {
22
+ config = JSON.parse(raw);
23
+ } catch {
24
+ return null;
25
+ }
26
+
27
+ if (!config.text || typeof config.text !== 'string' || config.text.trim() === '') {
28
+ return null;
29
+ }
30
+
31
+ if (config.colors !== undefined) {
32
+ if (!Array.isArray(config.colors) || config.colors.length < 2) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ return {
38
+ text: config.text.trim(),
39
+ colors: config.colors ?? DEFAULTS.colors,
40
+ font: config.font ?? DEFAULTS.font,
41
+ };
42
+ }
43
+
44
+ export function loadConfig() {
45
+ const configPath = join(process.cwd(), '.shell-logo.json');
46
+
47
+ let raw;
48
+ try {
49
+ raw = readFileSync(configPath, 'utf-8');
50
+ } catch {
51
+ console.error(
52
+ chalk.red('Error: ') +
53
+ 'No .shell-logo.json found in the current directory.\n\n' +
54
+ 'Create one with at least:\n\n' +
55
+ ' { "text": "HELLO" }\n\n' +
56
+ 'Or copy an example:\n\n' +
57
+ ' cp node_modules/shell-logo/examples/.shell-logo.json .'
58
+ );
59
+ process.exit(1);
60
+ }
61
+
62
+ let config;
63
+ try {
64
+ config = JSON.parse(raw);
65
+ } catch {
66
+ console.error(chalk.red('Error: ') + '.shell-logo.json contains invalid JSON.');
67
+ process.exit(1);
68
+ }
69
+
70
+ if (!config.text || typeof config.text !== 'string' || config.text.trim() === '') {
71
+ console.error(
72
+ chalk.red('Error: ') + '"text" is required and must be a non-empty string in .shell-logo.json.'
73
+ );
74
+ process.exit(1);
75
+ }
76
+
77
+ if (config.colors !== undefined) {
78
+ if (!Array.isArray(config.colors) || config.colors.length < 2) {
79
+ console.error(
80
+ chalk.red('Error: ') + '"colors" must be an array of at least 2 color strings.'
81
+ );
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ return {
87
+ text: config.text.trim(),
88
+ colors: config.colors ?? DEFAULTS.colors,
89
+ font: config.font ?? DEFAULTS.font,
90
+ };
91
+ }
@@ -0,0 +1,8 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ export function writeConfig(config) {
5
+ const configPath = join(process.cwd(), '.shell-logo.json');
6
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
7
+ return configPath;
8
+ }
package/src/index.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { runInteractiveUI } from './ui.js';
6
+ import { writeConfig } from './generate.js';
7
+ import { render } from './renderer.js';
8
+ import { getTerminalSize, clearScreen, hideCursor, showCursor, centerContent } from './terminal.js';
9
+ import { THEMES, FONTS } from './themes.js';
10
+ import chalk from 'chalk';
11
+ import * as p from '@clack/prompts';
12
+
13
+ const { action, config } = await runInteractiveUI();
14
+
15
+ if (action === 'generate') {
16
+ const s = p.spinner();
17
+ s.start('Writing .shell-logo.json...');
18
+ writeConfig(config);
19
+ s.stop('Config saved!');
20
+
21
+ const isGitRepo = existsSync(join(process.cwd(), '.git'));
22
+ const gitignorePath = join(process.cwd(), '.gitignore');
23
+ let shouldPrompt = isGitRepo;
24
+
25
+ if (existsSync(gitignorePath)) {
26
+ const content = readFileSync(gitignorePath, 'utf-8');
27
+ const lines = content.split('\n').map(l => l.trim());
28
+ if (lines.includes('.shell-logo.json')) {
29
+ shouldPrompt = false;
30
+ }
31
+ }
32
+
33
+ if (shouldPrompt) {
34
+ const addToGitignore = await p.select({
35
+ message: 'Add .shell-logo.json to .gitignore?',
36
+ options: [
37
+ { value: true, label: 'Yes (recommended)' },
38
+ { value: false, label: 'No' },
39
+ ],
40
+ });
41
+
42
+ if (p.isCancel(addToGitignore)) {
43
+ p.cancel('Cancelled.');
44
+ process.exit(0);
45
+ }
46
+
47
+ if (addToGitignore) {
48
+ if (existsSync(gitignorePath)) {
49
+ const existing = readFileSync(gitignorePath, 'utf-8');
50
+ const separator = existing.endsWith('\n') ? '' : '\n';
51
+ writeFileSync(gitignorePath, existing + separator + '.shell-logo.json\n');
52
+ } else {
53
+ writeFileSync(gitignorePath, '.shell-logo.json\n');
54
+ }
55
+ p.log.success('.gitignore updated.');
56
+ }
57
+ }
58
+ }
59
+
60
+ startRenderLoop(config);
61
+
62
+ function startRenderLoop(config) {
63
+ let resizeTimer;
64
+
65
+ // Find the current theme index by matching colors
66
+ let themeIndex = THEMES.findIndex(
67
+ (t) => JSON.stringify(t.colors) === JSON.stringify(config.colors)
68
+ );
69
+ if (themeIndex === -1) themeIndex = 0;
70
+
71
+ let fontIndex = FONTS.indexOf(config.font);
72
+ if (fontIndex === -1) fontIndex = 0;
73
+
74
+ let showStatus = false;
75
+ let statusTimer;
76
+
77
+ function renderLoop() {
78
+ const { columns, rows } = getTerminalSize();
79
+ const art = render(config, columns, rows);
80
+ clearScreen();
81
+ process.stdout.write(centerContent(art, columns, rows));
82
+ if (showStatus) {
83
+ const status = ` ${THEMES[themeIndex].name} · ${FONTS[fontIndex]} · ↑↓ theme ←→ font q quit`;
84
+ process.stdout.write(`\x1B[${rows};1H` + chalk.dim(status));
85
+ }
86
+ }
87
+
88
+ function flashStatus() {
89
+ showStatus = true;
90
+ clearTimeout(statusTimer);
91
+ statusTimer = setTimeout(() => {
92
+ showStatus = false;
93
+ renderLoop();
94
+ }, 2000);
95
+ }
96
+
97
+ function debouncedRender() {
98
+ clearTimeout(resizeTimer);
99
+ resizeTimer = setTimeout(renderLoop, 50);
100
+ }
101
+
102
+ function cleanup() {
103
+ showCursor();
104
+ clearScreen();
105
+ process.exit(0);
106
+ }
107
+
108
+ // Raw mode AFTER prompts are done
109
+ process.stdin.setRawMode(true);
110
+ process.stdin.resume();
111
+ hideCursor();
112
+ renderLoop();
113
+
114
+ // Show controls help on startup for 4 seconds
115
+ showStatus = true;
116
+ statusTimer = setTimeout(() => {
117
+ showStatus = false;
118
+ renderLoop();
119
+ }, 4000);
120
+
121
+ process.stdout.on('resize', debouncedRender);
122
+
123
+ process.stdin.on('data', (key) => {
124
+ if (key[0] === 3 || key[0] === 113) cleanup(); // Ctrl+C or q
125
+
126
+ // Arrow up/down are 3-byte escape sequences
127
+ if (key[0] === 27 && key[1] === 91) {
128
+ if (key[2] === 65) { // Arrow Up — prev theme
129
+ themeIndex = (themeIndex - 1 + THEMES.length) % THEMES.length;
130
+ config.colors = THEMES[themeIndex].colors;
131
+ } else if (key[2] === 66) { // Arrow Down — next theme
132
+ themeIndex = (themeIndex + 1) % THEMES.length;
133
+ config.colors = THEMES[themeIndex].colors;
134
+ } else if (key[2] === 67) { // Arrow Right — next font
135
+ fontIndex = (fontIndex + 1) % FONTS.length;
136
+ config.font = FONTS[fontIndex];
137
+ } else if (key[2] === 68) { // Arrow Left — prev font
138
+ fontIndex = (fontIndex - 1 + FONTS.length) % FONTS.length;
139
+ config.font = FONTS[fontIndex];
140
+ } else {
141
+ return;
142
+ }
143
+ writeConfig(config);
144
+ flashStatus();
145
+ renderLoop();
146
+ }
147
+
148
+ });
149
+ process.on('SIGTERM', cleanup);
150
+ }
@@ -0,0 +1,24 @@
1
+ import figlet from 'figlet';
2
+ import gradient from 'gradient-string';
3
+ import { scaleArt } from './scaling.js';
4
+
5
+ function figletSync(text, font) {
6
+ try {
7
+ return figlet.textSync(text, { font });
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ export function render(config, columns, rows) {
14
+ const art = figletSync(config.text, config.font);
15
+ if (!art) {
16
+ const grad = gradient(config.colors);
17
+ return grad(config.text.slice(0, columns));
18
+ }
19
+
20
+ const trimmed = art.trimEnd();
21
+ const scaled = scaleArt(trimmed, columns, rows - 2);
22
+ const grad = gradient(config.colors);
23
+ return grad.multiline(scaled);
24
+ }
package/src/scaling.js ADDED
@@ -0,0 +1,39 @@
1
+ export function measureArt(art) {
2
+ const lines = art.split('\n');
3
+ const width = lines.reduce((max, line) => Math.max(max, line.length), 0);
4
+ return { width, height: lines.length };
5
+ }
6
+
7
+ export function scaleArt(art, targetWidth, targetHeight) {
8
+ const lines = art.split('\n');
9
+ const { width: artWidth, height: artHeight } = measureArt(art);
10
+
11
+ if (artWidth <= targetWidth && artHeight <= targetHeight) return art;
12
+
13
+ const scaleFactor = Math.min(
14
+ targetWidth / artWidth,
15
+ targetHeight / artHeight,
16
+ 1.0
17
+ );
18
+
19
+ const newWidth = Math.floor(artWidth * scaleFactor);
20
+ const newHeight = Math.floor(artHeight * scaleFactor);
21
+
22
+ const result = [];
23
+ for (let j = 0; j < newHeight; j++) {
24
+ const srcRow = Math.floor(j * artHeight / newHeight);
25
+ const line = lines[srcRow] || '';
26
+ if (line.length === 0) {
27
+ result.push('');
28
+ continue;
29
+ }
30
+ const row = [];
31
+ for (let i = 0; i < newWidth; i++) {
32
+ const srcCol = Math.floor(i * artWidth / newWidth);
33
+ row.push(srcCol < line.length ? line[srcCol] : ' ');
34
+ }
35
+ result.push(row.join(''));
36
+ }
37
+
38
+ return result.join('\n');
39
+ }
@@ -0,0 +1,42 @@
1
+ // Strip ANSI escape codes when calculating visual width
2
+ const ANSI_RE = /\x1B\[[0-9;]*m/g;
3
+
4
+ export function getTerminalSize() {
5
+ return {
6
+ columns: process.stdout.columns || 80,
7
+ rows: process.stdout.rows || 24,
8
+ };
9
+ }
10
+
11
+ export function clearScreen() {
12
+ process.stdout.write('\x1B[2J\x1B[H');
13
+ }
14
+
15
+ export function hideCursor() {
16
+ process.stdout.write('\x1B[?25l');
17
+ }
18
+
19
+ export function showCursor() {
20
+ process.stdout.write('\x1B[?25h');
21
+ }
22
+
23
+ export function centerContent(text, columns, rows) {
24
+ const lines = text.split('\n');
25
+
26
+ // Calculate the max visual width (strip ANSI codes)
27
+ const maxWidth = lines.reduce((max, line) => {
28
+ const visual = line.replace(ANSI_RE, '').length;
29
+ return visual > max ? visual : max;
30
+ }, 0);
31
+
32
+ // Horizontal padding for each line
33
+ const leftPad = Math.max(0, Math.floor((columns - maxWidth) / 2));
34
+ const padStr = ' '.repeat(leftPad);
35
+
36
+ const paddedLines = lines.map((line) => padStr + line);
37
+
38
+ const contentHeight = paddedLines.length;
39
+ const topPad = Math.max(0, Math.floor((rows - contentHeight) / 2));
40
+
41
+ return '\n'.repeat(topPad) + paddedLines.join('\n');
42
+ }
package/src/themes.js ADDED
@@ -0,0 +1,12 @@
1
+ export const FONTS = ['Standard', 'Big', 'ANSI Shadow', 'Slant', 'Small'];
2
+
3
+ export const THEMES = [
4
+ { name: 'Sunset', colors: ['#ff6b6b', '#ff9f43', '#feca57'] },
5
+ { name: 'Ocean', colors: ['#0abde3', '#48dbfb', '#54a0ff'] },
6
+ { name: 'Forest', colors: ['#1dd1a1', '#2ed573', '#26de81'] },
7
+ { name: 'Neon', colors: ['#c56cf0', '#fd79a8', '#ff6b6b'] },
8
+ { name: 'Twilight', colors: ['#5f27cd', '#c56cf0', '#48dbfb'] },
9
+ { name: 'Fire', colors: ['#ff4757', '#ff6348', '#ffdd59'] },
10
+ { name: 'Pastel', colors: ['#fd79a8', '#feca57', '#48dbfb'] },
11
+ { name: 'Monochrome', colors: ['#ffffff', '#54a0ff', '#5f27cd'] },
12
+ ];
package/src/ui.js ADDED
@@ -0,0 +1,116 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import { tryLoadConfig } from './config.js';
4
+
5
+ const COLOR_PALETTE = [
6
+ { value: '#ff6b6b', label: `${chalk.bgHex('#ff6b6b')(' ')} Coral` },
7
+ { value: '#ff4757', label: `${chalk.bgHex('#ff4757')(' ')} Vibrant Red` },
8
+ { value: '#ff6348', label: `${chalk.bgHex('#ff6348')(' ')} Tomato` },
9
+ { value: '#feca57', label: `${chalk.bgHex('#feca57')(' ')} Golden` },
10
+ { value: '#ff9f43', label: `${chalk.bgHex('#ff9f43')(' ')} Orange` },
11
+ { value: '#ffdd59', label: `${chalk.bgHex('#ffdd59')(' ')} Lemon` },
12
+ { value: '#1dd1a1', label: `${chalk.bgHex('#1dd1a1')(' ')} Mint` },
13
+ { value: '#2ed573', label: `${chalk.bgHex('#2ed573')(' ')} Neon Green` },
14
+ { value: '#26de81', label: `${chalk.bgHex('#26de81')(' ')} Emerald` },
15
+ { value: '#48dbfb', label: `${chalk.bgHex('#48dbfb')(' ')} Sky` },
16
+ { value: '#0abde3', label: `${chalk.bgHex('#0abde3')(' ')} Cerulean` },
17
+ { value: '#54a0ff', label: `${chalk.bgHex('#54a0ff')(' ')} Cornflower` },
18
+ { value: '#5f27cd', label: `${chalk.bgHex('#5f27cd')(' ')} Deep Purple` },
19
+ { value: '#c56cf0', label: `${chalk.bgHex('#c56cf0')(' ')} Lavender` },
20
+ { value: '#fd79a8', label: `${chalk.bgHex('#fd79a8')(' ')} Pink` },
21
+ { value: '#ffffff', label: `${chalk.bgHex('#ffffff')(' ')} White` },
22
+ ];
23
+
24
+ const FONT_OPTIONS = [
25
+ { value: 'Standard', label: 'Standard' },
26
+ { value: 'Big', label: 'Big' },
27
+ { value: 'ANSI Shadow', label: 'ANSI Shadow' },
28
+ { value: 'Slant', label: 'Slant' },
29
+ { value: 'Small', label: 'Small' },
30
+ ];
31
+
32
+ function handleCancel(value) {
33
+ if (p.isCancel(value)) {
34
+ p.cancel('Cancelled.');
35
+ process.exit(0);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ async function promptGenerate() {
41
+ const text = handleCancel(
42
+ await p.text({
43
+ message: 'What text should the logo display?',
44
+ placeholder: 'HELLO',
45
+ validate: (val) => {
46
+ if (!val || val.trim() === '') return 'Text is required.';
47
+ },
48
+ })
49
+ );
50
+
51
+ // Pick 3 random unique colors as defaults
52
+ const shuffled = [...COLOR_PALETTE].sort(() => Math.random() - 0.5);
53
+ const initialColors = shuffled.slice(0, 3).map(c => c.value);
54
+
55
+ let colors;
56
+ while (true) {
57
+ colors = handleCancel(
58
+ await p.multiselect({
59
+ message: 'Pick 2 or more colors for the gradient:',
60
+ options: COLOR_PALETTE,
61
+ initialValues: initialColors,
62
+ required: true,
63
+ })
64
+ );
65
+
66
+ if (colors.length >= 2) break;
67
+ p.log.warning('Please select at least 2 colors.');
68
+ }
69
+
70
+ const font = handleCancel(
71
+ await p.select({
72
+ message: 'Pick a font:',
73
+ options: FONT_OPTIONS,
74
+ })
75
+ );
76
+
77
+ return {
78
+ action: 'generate',
79
+ config: { text: text.trim(), colors, font },
80
+ };
81
+ }
82
+
83
+ export async function runInteractiveUI() {
84
+ p.intro(chalk.bold('terminal-logo'));
85
+
86
+ const hasConfig = !!tryLoadConfig();
87
+
88
+ const action = handleCancel(
89
+ await p.select({
90
+ message: 'What would you like to do?',
91
+ initialValue: hasConfig ? 'run' : 'generate',
92
+ options: [
93
+ { value: 'generate', label: 'Generate', hint: 'create .shell-logo.json' },
94
+ { value: 'run', label: 'Run', hint: 'display current logo' },
95
+ ],
96
+ })
97
+ );
98
+
99
+ if (action === 'generate') {
100
+ return promptGenerate();
101
+ }
102
+
103
+ // action === 'run'
104
+ const config = tryLoadConfig();
105
+ if (!config) {
106
+ p.log.error(
107
+ 'No valid .shell-logo.json found in the current directory.\n' +
108
+ ' Run again and choose "Generate" to create one.'
109
+ );
110
+ p.outro('Done');
111
+ process.exit(1);
112
+ }
113
+
114
+ p.outro('Launching logo...');
115
+ return { action: 'run', config };
116
+ }