bunosh 0.1.5 → 0.2.3

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.
@@ -0,0 +1,341 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { BUNOSHFILE } from './program.js';
4
+
5
+ /**
6
+ * Generates shell completion scripts for bunosh commands
7
+ */
8
+ export class CompletionGenerator {
9
+ constructor(commands = []) {
10
+ this.commands = commands;
11
+ }
12
+
13
+ /**
14
+ * Generates bash completion script
15
+ */
16
+ generateBashCompletion() {
17
+ const commandList = this.commands.map(cmd => cmd.name).join(' ');
18
+
19
+ return `#!/bin/bash
20
+
21
+ # Bash completion for bunosh
22
+ _bunosh_completion() {
23
+ local cur prev opts
24
+ COMPREPLY=()
25
+ cur="\${COMP_WORDS[COMP_CWORD]}"
26
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
27
+
28
+ # Available commands
29
+ opts="${commandList} --help --version init completion"
30
+
31
+ # Special handling for specific commands
32
+ case "\${prev}" in
33
+ bunosh)
34
+ COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
35
+ return 0
36
+ ;;
37
+ completion)
38
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- \${cur}) )
39
+ return 0
40
+ ;;
41
+ init)
42
+ # No completion for init
43
+ return 0
44
+ ;;
45
+ *)
46
+ # Default completion for other commands
47
+ COMPREPLY=( $(compgen -W "\${opts}" -- \${cur}) )
48
+ return 0
49
+ ;;
50
+ esac
51
+ }
52
+
53
+ # Register the completion function
54
+ complete -F _bunosh_completion bunosh
55
+ `;
56
+ }
57
+
58
+ /**
59
+ * Generates zsh completion script
60
+ */
61
+ generateZshCompletion() {
62
+ const commandCompletions = this.commands.map(cmd => {
63
+ const args = cmd.args ? cmd.args.map(arg => `'${arg}'`).join(' ') : '';
64
+ const desc = cmd.description ? cmd.description.replace(/'/g, "\\'") : '';
65
+ return ` '${cmd.name}[${desc}]${args ? ':' + args : ''}'`;
66
+ }).join('\n');
67
+
68
+ return `#compdef bunosh
69
+
70
+ # Zsh completion for bunosh
71
+ _bunosh() {
72
+ local context state line
73
+ typeset -A opt_args
74
+
75
+ _arguments -C \\
76
+ '1: :_bunosh_commands' \\
77
+ '*::arg:->args'
78
+
79
+ case $state in
80
+ args)
81
+ case $line[1] in
82
+ completion)
83
+ _arguments \\
84
+ '1: :(bash zsh fish)'
85
+ ;;
86
+ init)
87
+ # No arguments for init
88
+ ;;
89
+ *)
90
+ # Default argument completion
91
+ _files
92
+ ;;
93
+ esac
94
+ ;;
95
+ esac
96
+ }
97
+
98
+ _bunosh_commands() {
99
+ local commands
100
+ commands=(
101
+ ${commandCompletions}
102
+ 'completion[Generate shell completion scripts]'
103
+ 'init[Create a new Bunoshfile.js]'
104
+ '--help[Show help information]'
105
+ '--version[Show version information]'
106
+ )
107
+ _describe 'commands' commands
108
+ }
109
+
110
+ _bunosh
111
+ `;
112
+ }
113
+
114
+ /**
115
+ * Generates fish completion script
116
+ */
117
+ generateFishCompletion() {
118
+ const commandCompletions = this.commands.map(cmd => {
119
+ const desc = cmd.description ? cmd.description : '';
120
+ return `complete -c bunosh -f -a "${cmd.name}" -d "${desc}"`;
121
+ }).join('\n');
122
+
123
+ return `# Fish completion for bunosh
124
+
125
+ # Basic commands
126
+ ${commandCompletions}
127
+ complete -c bunosh -f -a "completion" -d "Generate shell completion scripts"
128
+ complete -c bunosh -f -a "init" -d "Create a new Bunoshfile.js"
129
+ complete -c bunosh -f -l help -d "Show help information"
130
+ complete -c bunosh -f -l version -d "Show version information"
131
+
132
+ # Completion for completion command
133
+ complete -c bunosh -f -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
134
+ `;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Extracts commands from the current Bunoshfile for completion
140
+ */
141
+ export function getCompletionCommands() {
142
+ try {
143
+ if (!fs.existsSync(BUNOSHFILE)) {
144
+ return [];
145
+ }
146
+
147
+ const source = fs.readFileSync(BUNOSHFILE, 'utf8');
148
+
149
+ // Simple regex to extract export function names and comments
150
+ const functionRegex = /\/\*\*\s*\n\s*\*\s*(.+?)\s*\n[\s\S]*?\*\/\s*\n\s*export\s+(?:async\s+)?function\s+(\w+)/g;
151
+ const commands = [];
152
+ let match;
153
+
154
+ while ((match = functionRegex.exec(source)) !== null) {
155
+ const [, description, functionName] = match;
156
+ const commandName = prepareCommandName(functionName);
157
+
158
+ commands.push({
159
+ name: commandName,
160
+ description: description.trim(),
161
+ functionName
162
+ });
163
+ }
164
+
165
+ // Also check for simple exports without JSDoc
166
+ const simpleExportRegex = /export\s+(?:async\s+)?function\s+(\w+)/g;
167
+ while ((match = simpleExportRegex.exec(source)) !== null) {
168
+ const [, functionName] = match;
169
+ const commandName = prepareCommandName(functionName);
170
+
171
+ // Don't add duplicates
172
+ if (!commands.find(cmd => cmd.name === commandName)) {
173
+ commands.push({
174
+ name: commandName,
175
+ description: '',
176
+ functionName
177
+ });
178
+ }
179
+ }
180
+
181
+ return commands.sort((a, b) => a.name.localeCompare(b.name));
182
+ } catch (error) {
183
+ return [];
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Converts function name to command name (same logic as program.js)
189
+ */
190
+ function prepareCommandName(name) {
191
+ name = name
192
+ .split(/(?=[A-Z])/)
193
+ .join("-")
194
+ .toLowerCase();
195
+ return name.replace("-", ":");
196
+ }
197
+
198
+ /**
199
+ * Detects the current shell from environment
200
+ */
201
+ export function detectCurrentShell() {
202
+ // Check SHELL environment variable
203
+ const shellPath = process.env.SHELL;
204
+ if (shellPath) {
205
+ const shellName = path.basename(shellPath);
206
+ if (['bash', 'zsh', 'fish'].includes(shellName)) {
207
+ return shellName;
208
+ }
209
+ }
210
+
211
+ // Check parent process name (for cases where SHELL might not be set correctly)
212
+ try {
213
+ const { execSync } = require('child_process');
214
+ const parentProcess = execSync('ps -p $PPID -o comm=', { encoding: 'utf8' }).trim();
215
+ if (['bash', 'zsh', 'fish'].includes(parentProcess)) {
216
+ return parentProcess;
217
+ }
218
+ } catch (error) {
219
+ // Ignore errors, fall back to SHELL variable
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * Gets the appropriate paths for shell completion files
227
+ */
228
+ export function getCompletionPaths(shell) {
229
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
230
+
231
+ switch (shell) {
232
+ case 'bash':
233
+ return {
234
+ completionFile: path.join(homeDir, '.bunosh-completion.bash'),
235
+ configFiles: [
236
+ path.join(homeDir, '.bashrc'),
237
+ path.join(homeDir, '.bash_profile'),
238
+ path.join(homeDir, '.profile')
239
+ ],
240
+ sourceCommand: 'source ~/.bunosh-completion.bash'
241
+ };
242
+ case 'zsh':
243
+ return {
244
+ completionFile: path.join(homeDir, '.bunosh-completion.zsh'),
245
+ configFiles: [
246
+ path.join(homeDir, '.zshrc')
247
+ ],
248
+ sourceCommand: 'source ~/.bunosh-completion.zsh'
249
+ };
250
+ case 'fish':
251
+ const fishConfigDir = path.join(homeDir, '.config', 'fish');
252
+ return {
253
+ completionFile: path.join(fishConfigDir, 'completions', 'bunosh.fish'),
254
+ configFiles: [], // Fish doesn't need config file modification
255
+ sourceCommand: null // Fish loads completions automatically
256
+ };
257
+ default:
258
+ throw new Error(`Unsupported shell: ${shell}`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Installs completion for the specified shell
264
+ */
265
+ export function installCompletion(shell) {
266
+ const commands = getCompletionCommands();
267
+ const generator = new CompletionGenerator(commands);
268
+ const paths = getCompletionPaths(shell);
269
+
270
+ // Generate completion script
271
+ let completionScript;
272
+ switch (shell) {
273
+ case 'bash':
274
+ completionScript = generator.generateBashCompletion();
275
+ break;
276
+ case 'zsh':
277
+ completionScript = generator.generateZshCompletion();
278
+ break;
279
+ case 'fish':
280
+ completionScript = generator.generateFishCompletion();
281
+ break;
282
+ default:
283
+ throw new Error(`Unsupported shell: ${shell}`);
284
+ }
285
+
286
+ // Ensure directory exists
287
+ const completionDir = path.dirname(paths.completionFile);
288
+ if (!fs.existsSync(completionDir)) {
289
+ fs.mkdirSync(completionDir, { recursive: true });
290
+ }
291
+
292
+ // Write completion file
293
+ fs.writeFileSync(paths.completionFile, completionScript);
294
+
295
+ // For bash and zsh, add source command to config file if not present
296
+ if (paths.sourceCommand && paths.configFiles.length > 0) {
297
+ const sourceCommandWithComment = `
298
+ # Bunosh completion
299
+ if [ -f ${paths.completionFile} ]; then
300
+ ${paths.sourceCommand}
301
+ fi`;
302
+
303
+ // Find the first existing config file, or use the first option
304
+ let configFile = paths.configFiles.find(f => fs.existsSync(f));
305
+ if (!configFile) {
306
+ configFile = paths.configFiles[0];
307
+ // Create the file if it doesn't exist
308
+ fs.writeFileSync(configFile, '');
309
+ }
310
+
311
+ // Check if already configured
312
+ const configContent = fs.readFileSync(configFile, 'utf8');
313
+ if (!configContent.includes('bunosh-completion') && !configContent.includes(paths.sourceCommand)) {
314
+ fs.appendFileSync(configFile, sourceCommandWithComment);
315
+ return { configFile, added: true };
316
+ } else {
317
+ return { configFile, added: false };
318
+ }
319
+ }
320
+
321
+ return { configFile: null, added: false };
322
+ }
323
+
324
+ /**
325
+ * Main completion handler
326
+ */
327
+ export function handleCompletion(shell) {
328
+ const commands = getCompletionCommands();
329
+ const generator = new CompletionGenerator(commands);
330
+
331
+ switch (shell) {
332
+ case 'bash':
333
+ return generator.generateBashCompletion();
334
+ case 'zsh':
335
+ return generator.generateZshCompletion();
336
+ case 'fish':
337
+ return generator.generateFishCompletion();
338
+ default:
339
+ throw new Error(`Unsupported shell: ${shell}. Supported shells: bash, zsh, fish`);
340
+ }
341
+ }
package/src/font.js ADDED
@@ -0,0 +1,258 @@
1
+
2
+ // 5x5 ASCII "font". Use '#' for pixels and '.' for background.
3
+ // Lowercase letters are rendered via their uppercase equivalents.
4
+ const FONT5 = {
5
+ 'A': [
6
+ ".###.",
7
+ "#...#",
8
+ "#####",
9
+ "#...#",
10
+ "#...#",
11
+ ],
12
+ 'B': [
13
+ "####.",
14
+ "#...#",
15
+ "####.",
16
+ "#...#",
17
+ "####.",
18
+ ],
19
+ 'C': [
20
+ ".####",
21
+ "#....",
22
+ "#....",
23
+ "#....",
24
+ ".####",
25
+ ],
26
+ 'D': [
27
+ "####.",
28
+ "#...#",
29
+ "#...#",
30
+ "#...#",
31
+ "####.",
32
+ ],
33
+ 'E': [
34
+ "#####",
35
+ "#....",
36
+ "####.",
37
+ "#....",
38
+ "#####",
39
+ ],
40
+ 'F': [
41
+ "#####",
42
+ "#....",
43
+ "####.",
44
+ "#....",
45
+ "#....",
46
+ ],
47
+ 'G': [
48
+ ".####",
49
+ "#....",
50
+ "#.###",
51
+ "#...#",
52
+ ".####",
53
+ ],
54
+ 'H': [
55
+ "#...#",
56
+ "#...#",
57
+ "#####",
58
+ "#...#",
59
+ "#...#",
60
+ ],
61
+ 'I': [
62
+ "#####",
63
+ "..#..",
64
+ "..#..",
65
+ "..#..",
66
+ "#####",
67
+ ],
68
+ 'J': [
69
+ "#####",
70
+ "...#.",
71
+ "...#.",
72
+ "#..#.",
73
+ ".##..",
74
+ ],
75
+ 'K': [
76
+ "#...#",
77
+ "#..#.",
78
+ "###..",
79
+ "#..#.",
80
+ "#...#",
81
+ ],
82
+ 'L': [
83
+ "#....",
84
+ "#....",
85
+ "#....",
86
+ "#....",
87
+ "#####",
88
+ ],
89
+ 'M': [
90
+ "#...#",
91
+ "##.##",
92
+ "#.#.#",
93
+ "#...#",
94
+ "#...#",
95
+ ],
96
+ 'N': [
97
+ "#...#",
98
+ "##..#",
99
+ "#.#.#",
100
+ "#..##",
101
+ "#...#",
102
+ ],
103
+ 'O': [
104
+ ".###.",
105
+ "#...#",
106
+ "#...#",
107
+ "#...#",
108
+ ".###.",
109
+ ],
110
+ 'P': [
111
+ "####.",
112
+ "#...#",
113
+ "####.",
114
+ "#....",
115
+ "#....",
116
+ ],
117
+ 'Q': [
118
+ ".###.",
119
+ "#...#",
120
+ "#...#",
121
+ "#..##",
122
+ ".####",
123
+ ],
124
+ 'R': [
125
+ "####.",
126
+ "#...#",
127
+ "####.",
128
+ "#..#.",
129
+ "#...#",
130
+ ],
131
+ 'S': [
132
+ ".####",
133
+ "#....",
134
+ ".###.",
135
+ "....#",
136
+ "####.",
137
+ ],
138
+ 'T': [
139
+ "#####",
140
+ "..#..",
141
+ "..#..",
142
+ "..#..",
143
+ "..#..",
144
+ ],
145
+ 'U': [
146
+ "#...#",
147
+ "#...#",
148
+ "#...#",
149
+ "#...#",
150
+ ".###.",
151
+ ],
152
+ 'V': [
153
+ "#...#",
154
+ "#...#",
155
+ "#...#",
156
+ ".#.#.",
157
+ "..#..",
158
+ ],
159
+ 'W': [
160
+ "#...#",
161
+ "#...#",
162
+ "#.#.#",
163
+ "##.##",
164
+ "#...#",
165
+ ],
166
+ 'X': [
167
+ "#...#",
168
+ ".#.#.",
169
+ "..#..",
170
+ ".#.#.",
171
+ "#...#",
172
+ ],
173
+ 'Y': [
174
+ "#...#",
175
+ ".#.#.",
176
+ "..#..",
177
+ "..#..",
178
+ "..#..",
179
+ ],
180
+ 'Z': [
181
+ "#####",
182
+ "...#.",
183
+ "..#..",
184
+ ".#...",
185
+ "#####",
186
+ ],
187
+ ' ': [
188
+ ".....",
189
+ ".....",
190
+ ".....",
191
+ ".....",
192
+ ".....",
193
+ ],
194
+ '!': [
195
+ "..#..",
196
+ "..#..",
197
+ "..#..",
198
+ ".....",
199
+ "..#..",
200
+ ],
201
+ '.': [
202
+ ".....",
203
+ ".....",
204
+ ".....",
205
+ ".....",
206
+ "..#..",
207
+ ],
208
+ '?': [
209
+ ".###.",
210
+ "...#.",
211
+ "..#..",
212
+ ".....",
213
+ "..#..",
214
+ ],
215
+ '-': [
216
+ ".....",
217
+ ".....",
218
+ "#####",
219
+ ".....",
220
+ ".....",
221
+ ],
222
+ };
223
+
224
+ // Optional fallback for unknown characters:
225
+ const UNKNOWN5 = [
226
+ ".###.",
227
+ "#...#",
228
+ "..##.",
229
+ ".....",
230
+ "..#..",
231
+ ];
232
+
233
+ /**
234
+ * Render text into a 5-line ASCII banner.
235
+ * @param {string} text - The text to render.
236
+ * @param {object} [opts]
237
+ * @param {string} [opts.symbol='#'] - Pixel character to use instead of '#'.
238
+ * @param {number} [opts.letterSpacing=1] - Spaces between characters (0+).
239
+ * @returns {string} - Multiline banner string.
240
+ */
241
+ function cprint(text, { symbol = '#', letterSpacing = 1 } = {}) {
242
+ const rows = ["", "", "", "", ""];
243
+ const gap = " ".repeat(Math.max(0, letterSpacing));
244
+
245
+ for (const rawCh of text) {
246
+ const ch = /[a-z]/.test(rawCh) ? rawCh.toUpperCase() : rawCh;
247
+ const glyph = FONT5[ch] || UNKNOWN5;
248
+ for (let r = 0; r < 5; r++) {
249
+ // convert '.' -> space, '#' -> symbol
250
+ const line = glyph[r].replace(/\./g, ' ').replace(/#/g, symbol);
251
+ rows[r] += line + gap;
252
+ }
253
+ }
254
+ // Trim trailing spaces on each row and join
255
+ return rows.map(r => r.replace(/\s+$/g, '')).join('\n');
256
+ }
257
+
258
+ export default cprint;
@@ -0,0 +1,17 @@
1
+ export class BaseFormatter {
2
+ format(taskName, status, taskType, extra = {}) {
3
+ throw new Error('format method must be implemented by subclass');
4
+ }
5
+
6
+ formatOutput(line, isError = false) {
7
+ return line;
8
+ }
9
+
10
+ shouldDelayStart() {
11
+ return true;
12
+ }
13
+
14
+ getStartDelay() {
15
+ return 50;
16
+ }
17
+ }
@@ -0,0 +1,81 @@
1
+ import chalk from 'chalk';
2
+ import { BaseFormatter } from './base.js';
3
+
4
+ const STATUS_CONFIG = {
5
+ start: { icon: '▶', color: 'blue' },
6
+ finish: { icon: '✓', color: 'green' },
7
+ error: { icon: '✗', color: 'red' },
8
+ output: { icon: ' ', color: 'white' },
9
+ info: { icon: ' ', color: 'dim' }
10
+ };
11
+
12
+ export class ConsoleFormatter extends BaseFormatter {
13
+ format(taskName, status, taskType, extra = {}) {
14
+ const config = STATUS_CONFIG[status];
15
+ if (!config) {
16
+ throw new Error(`Unknown status: ${status}. Valid statuses: ${Object.keys(STATUS_CONFIG).join(', ')}`);
17
+ }
18
+
19
+ const icon = chalk[config.color](config.icon);
20
+ const taskTypeFormatted = taskType ? chalk.bold(taskType) + ' ' : '';
21
+ const taskNameFormatted = chalk.yellow(taskName);
22
+
23
+ const extraParts = [];
24
+ Object.entries(extra).forEach(([key, value]) => {
25
+ if (value !== null && value !== undefined) {
26
+ if (key === 'duration') {
27
+ extraParts.push(chalk.dim(`${value}ms`));
28
+ } else if (key === 'error') {
29
+ extraParts.push(chalk.dim(value));
30
+ } else if (key === 'status') {
31
+ extraParts.push(chalk.dim(value));
32
+ } else if (key === 'exitCode') {
33
+ extraParts.push(chalk.dim(`exit code: ${value}`));
34
+ } else {
35
+ extraParts.push(chalk.dim(`${key}: ${value}`));
36
+ }
37
+ }
38
+ });
39
+
40
+ const terminalWidth = process.stdout.columns || 100;
41
+ let leftContent = `${icon} ${taskTypeFormatted}${taskNameFormatted}`;
42
+ let rightContent = '';
43
+
44
+ if (extraParts.length > 0) {
45
+ rightContent = `(${extraParts.join(', ')})`;
46
+ }
47
+
48
+ const leftLength = this._stripAnsi(leftContent).length;
49
+ const rightLength = this._stripAnsi(rightContent).length;
50
+ const padding = ' '.repeat(Math.max(1, terminalWidth - leftLength - rightLength));
51
+
52
+ let line = leftContent + padding + rightContent;
53
+
54
+ if (icon.trim()) {
55
+ line = chalk.bgGray(line);
56
+ }
57
+
58
+ let result = line;
59
+ if (status === 'error' && extra.error) {
60
+ result += '\n' + chalk.red(' Error:') + ' ' + extra.error;
61
+ }
62
+ if (status === 'finish' || status === 'error') {
63
+ result += '\n';
64
+ }
65
+
66
+ return result;
67
+ }
68
+
69
+ formatOutput(line, isError = false) {
70
+ if (!line.trim()) return '';
71
+ return isError ? chalk.red(line) : line;
72
+ }
73
+
74
+ _stripAnsi(str) {
75
+ return str.replace(/\u001b\[[0-9;]*m/g, '');
76
+ }
77
+
78
+ static detect() {
79
+ return !process.env.CI;
80
+ }
81
+ }
@@ -0,0 +1,17 @@
1
+ import { GitHubActionsFormatter } from './github-actions.js';
2
+ import { ConsoleFormatter } from './console.js';
3
+
4
+ const FORMATTERS = [
5
+ GitHubActionsFormatter,
6
+ ConsoleFormatter
7
+ ];
8
+
9
+ export function createFormatter() {
10
+ for (const FormatterClass of FORMATTERS) {
11
+ if (FormatterClass.detect && FormatterClass.detect()) {
12
+ return new FormatterClass();
13
+ }
14
+ }
15
+
16
+ return new ConsoleFormatter();
17
+ }