bunosh 0.5.0 → 0.5.6

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.
@@ -1,66 +1,34 @@
1
- import { TaskResult, createTaskInfo, finishTaskInfo, getCurrentTaskId, runningTasks } from "../task.js";
2
- import Printer from "../printer.js";
1
+ import { TaskResult, createTaskInfo, finishTaskInfo, getCurrentTaskId, runningTasks } from '../task.js';
2
+ import Printer from '../printer.js';
3
3
 
4
4
  const isBun = typeof Bun !== 'undefined' && typeof Bun.spawn === 'function';
5
5
 
6
6
  export default function shell(strings, ...values) {
7
- let envs = null;
8
- let cwd = null;
9
-
10
7
  // Check if called as regular function instead of template literal
11
8
  if (!Array.isArray(strings)) {
12
- // If first argument is a string, treat it as the command
13
9
  if (typeof strings === 'string') {
14
- // For Bun shell, we need to create a template literal-like call
15
- // But since we can't, fall back to exec
16
- console.log('Note: shell() with string argument falls back to exec()');
17
- const cmdPromise = (async () => {
18
- const { default: exec } = await import("./exec.js");
19
- let execPromise = exec(strings);
20
- if (envs) execPromise = execPromise.env(envs);
21
- if (cwd) execPromise = execPromise.cwd(cwd);
22
- return execPromise;
23
- })();
24
-
25
- // Add .env and .cwd methods
26
- cmdPromise.env = (newEnvs) => {
27
- envs = newEnvs;
28
- return cmdPromise;
29
- };
30
-
31
- cmdPromise.cwd = (newCwd) => {
32
- cwd = newCwd;
33
- return cmdPromise;
34
- };
35
-
36
- return cmdPromise;
10
+ strings = [strings];
11
+ values = [];
37
12
  } else {
38
- throw new Error('shell() must be called as a template literal: shell`command`');
13
+ throw new Error('shell() must be called as a template literal: shell`command` or shell("command")');
39
14
  }
40
15
  }
41
-
16
+
42
17
  const cmd = strings.reduce((accumulator, str, i) => {
43
- return accumulator + str + (values[i] || "");
44
- }, "");
18
+ return accumulator + str + (values[i] || '');
19
+ }, '');
45
20
 
46
- const cmdPromise = new Promise(async (resolve, reject) => {
47
- const extraInfo = {};
48
- if (cwd) extraInfo.cwd = cwd;
49
- if (envs) extraInfo.env = envs;
21
+ let envs = null;
22
+ let cwd = null;
50
23
 
51
- if (!isBun) {
52
- const { default: exec } = await import("./exec.js");
53
- let execPromise = exec([cmd]);
54
- if (envs) execPromise = execPromise.env(envs);
55
- if (cwd) execPromise = execPromise.cwd(cwd);
56
- const result = await execPromise;
57
- resolve(result);
58
- return;
59
- }
24
+ const cmdPromise = new Promise(async (resolve) => {
25
+ // Wait for the next event loop tick to ensure .env() and .cwd() have been called
26
+ await new Promise(resolve => setTimeout(resolve, 0));
27
+
28
+ const currentTaskId = getCurrentTaskId();
60
29
 
61
30
  // Check if parent task is silent
62
31
  let isParentSilent = false;
63
- const currentTaskId = getCurrentTaskId();
64
32
  if (currentTaskId) {
65
33
  const parentTask = runningTasks.get(currentTaskId);
66
34
  if (parentTask && parentTask.isSilent) {
@@ -68,91 +36,144 @@ export default function shell(strings, ...values) {
68
36
  }
69
37
  }
70
38
 
71
- const taskInfo = createTaskInfo(cmd, null, isParentSilent);
72
- const printer = new Printer("shell", taskInfo.id);
39
+ const extraInfo = {};
40
+ if (cwd) extraInfo.cwd = cwd;
41
+ if (envs) extraInfo.env = envs;
42
+
43
+ const taskInfo = createTaskInfo(cmd, currentTaskId, isParentSilent);
44
+ const printer = new Printer('shell', taskInfo.id);
73
45
  printer.start(cmd, extraInfo);
74
46
 
75
47
  try {
76
- const { $ } = await import("bun");
77
-
78
- let shell = $;
79
-
80
- if (cwd) {
81
- shell = shell.cwd(cwd);
82
- }
83
-
84
- if (envs) {
85
- shell = shell.env(envs);
48
+ if (global.disableBunForTesting || !isBun) {
49
+ const result = await nodeShell(cmd, extraInfo, printer);
50
+ if (result.status === 'success') {
51
+ finishTaskInfo(taskInfo, true, null, result.output);
52
+ resolve(result);
53
+ } else {
54
+ finishTaskInfo(taskInfo, false, new Error(result.output), result.output);
55
+ resolve(result);
56
+ }
57
+ return;
86
58
  }
87
59
 
88
- let result;
89
- try {
90
- result = await shell(strings, ...values);
91
-
92
- const output = await result.text();
93
-
94
- const metadata = {
95
- taskType: 'shell',
96
- exitCode: 0,
97
- stdout: output.trim(),
98
- stderr: ''
99
- };
100
-
101
- printer.finish(cmd);
102
- finishTaskInfo(taskInfo, true, null, output.trim());
103
- resolve(TaskResult.success(output.trim(), metadata));
104
- return;
105
-
106
- } catch (shellError) {
107
- const isCommandNotFound = shellError.stderr &&
108
- (shellError.stderr.includes('command not found') ||
109
- shellError.stderr.includes('bun: command not found'));
110
-
111
- if (isCommandNotFound) {
112
- printer.finish(cmd);
113
- finishTaskInfo(taskInfo, true, null, "fallback to exec");
114
-
115
- const { default: exec } = await import("./exec.js");
116
- let execPromise = exec([cmd]);
117
- if (envs) execPromise = execPromise.env(envs);
118
- if (cwd) execPromise = execPromise.cwd(cwd);
119
- const result = await execPromise;
120
- resolve(result);
121
- return;
60
+ // Bun implementation with real-time streaming
61
+ const needsShell = cmd.includes('|') || cmd.includes('>') || cmd.includes('<') || cmd.includes('&&') || cmd.includes('||') || cmd.includes("'") || cmd.includes('"') || cmd.includes(';') || cmd.includes('$') || cmd.includes('`') || cmd.includes('\n');
62
+
63
+ const { spawn } = Bun;
64
+ const proc = spawn({
65
+ cmd: needsShell ? ['/bin/sh', '-c', cmd] : cmd.trim().split(/\s+/),
66
+ cwd: cwd || process.cwd(),
67
+ env: {
68
+ ...(envs ? { ...process.env, ...envs } : process.env),
69
+ },
70
+ stdout: "pipe",
71
+ stderr: "pipe",
72
+ stdin: "ignore"
73
+ });
74
+
75
+ const decoder = new TextDecoder();
76
+ let output = '';
77
+ let stdout = '';
78
+ let stderr = '';
79
+ let finished = false;
80
+
81
+ // Process stdout
82
+ const readStdout = async () => {
83
+ const reader = proc.stdout.getReader();
84
+ let buffer = '';
85
+
86
+ try {
87
+ while (!finished) {
88
+ const { done, value } = await reader.read();
89
+ if (done) break;
90
+
91
+ const text = decoder.decode(value, { stream: true });
92
+ buffer += text;
93
+
94
+ const lines = buffer.split('\n');
95
+ buffer = lines.pop();
96
+
97
+ for (const line of lines) {
98
+ if (line.trim()) {
99
+ printer.output(line);
100
+ output += line + '\n';
101
+ stdout += line + '\n';
102
+ }
103
+ }
104
+ }
105
+
106
+ if (buffer.trim()) {
107
+ printer.output(buffer);
108
+ output += buffer + '\n';
109
+ stdout += buffer + '\n';
110
+ }
111
+ } finally {
112
+ reader.releaseLock();
122
113
  }
123
-
124
- if (shellError.exitCode !== undefined) {
125
- const stderr = shellError.stderr ? Buffer.isBuffer(shellError.stderr) ? shellError.stderr.toString() : shellError.stderr : "";
126
- const stdout = shellError.stdout ? Buffer.isBuffer(shellError.stdout) ? shellError.stdout.toString() : shellError.stdout : "";
127
- const errorOutput = (stderr + stdout).trim() || `Command failed with exit code ${shellError.exitCode}`;
128
-
129
- if (errorOutput) {
130
- const lines = errorOutput.split('\n');
114
+ };
115
+
116
+ // Process stderr
117
+ const readStderr = async () => {
118
+ const reader = proc.stderr.getReader();
119
+ let buffer = '';
120
+
121
+ try {
122
+ while (!finished) {
123
+ const { done, value } = await reader.read();
124
+ if (done) break;
125
+
126
+ const text = decoder.decode(value, { stream: true });
127
+ buffer += text;
128
+
129
+ const lines = buffer.split('\n');
130
+ buffer = lines.pop();
131
+
131
132
  for (const line of lines) {
132
133
  if (line.trim()) {
133
134
  printer.output(line, true);
135
+ output += line + '\n';
136
+ stderr += line + '\n';
134
137
  }
135
138
  }
136
139
  }
137
-
138
- const metadata = {
139
- taskType: 'shell',
140
- exitCode: shellError.exitCode,
141
- stdout: stdout.trim(),
142
- stderr: stderr.trim()
143
- };
144
-
145
- const error = new Error(`Exit code: ${shellError.exitCode}`);
146
- printer.error(cmd, null, { exitCode: shellError.exitCode });
147
- finishTaskInfo(taskInfo, false, error, errorOutput);
148
- resolve(TaskResult.fail(errorOutput, metadata));
149
- return;
150
- } else {
151
- const errorMessage = shellError.message || shellError.toString();
152
- printer.error(cmd, shellError);
153
- finishTaskInfo(taskInfo, false, shellError, errorMessage);
154
- resolve(TaskResult.fail(errorMessage, { taskType: 'shell' }));
140
+
141
+ if (buffer.trim()) {
142
+ printer.output(buffer, true);
143
+ output += buffer + '\n';
144
+ stderr += buffer + '\n';
145
+ }
146
+ } finally {
147
+ reader.releaseLock();
155
148
  }
149
+ };
150
+
151
+ // Start reading both streams
152
+ const [, , exitResult] = await Promise.all([
153
+ readStdout(),
154
+ readStderr(),
155
+ proc.exited
156
+ ]);
157
+
158
+ finished = true;
159
+ const exitCode = parseInt(exitResult, 10);
160
+
161
+ const metadata = {
162
+ taskType: 'shell',
163
+ exitCode,
164
+ stdout: stdout.trim(),
165
+ stderr: stderr.trim()
166
+ };
167
+
168
+ if (exitCode === 0) {
169
+ printer.finish(cmd);
170
+ finishTaskInfo(taskInfo, true, null, output.trim());
171
+ resolve(TaskResult.success(output.trim(), metadata));
172
+ } else {
173
+ const error = new Error(`Exit code: ${exitCode}`);
174
+ printer.error(cmd, null, { exitCode });
175
+ finishTaskInfo(taskInfo, false, error, output.trim());
176
+ resolve(TaskResult.fail(output.trim(), metadata));
156
177
  }
157
178
  } catch (error) {
158
179
  printer.error(cmd, error);
@@ -173,3 +194,57 @@ export default function shell(strings, ...values) {
173
194
 
174
195
  return cmdPromise;
175
196
  }
197
+
198
+ async function nodeShell(cmd, extraInfo, printer) {
199
+ // Node.js fallback - simple execution without real-time output
200
+ const { spawn } = await import('child_process');
201
+
202
+ return new Promise((resolve) => {
203
+ const proc = spawn('sh', ['-c', cmd], {
204
+ cwd: extraInfo.cwd || process.cwd(),
205
+ env: extraInfo.env ? { ...process.env, ...extraInfo.env } : process.env,
206
+ stdio: ['ignore', 'pipe', 'pipe']
207
+ });
208
+
209
+ let output = '';
210
+ let stdout = '';
211
+ let stderr = '';
212
+
213
+ proc.stdout.on('data', (data) => {
214
+ const text = data.toString();
215
+ printer.output(text.trim());
216
+ output += text;
217
+ stdout += text;
218
+ });
219
+
220
+ proc.stderr.on('data', (data) => {
221
+ const text = data.toString();
222
+ printer.output(text.trim(), true);
223
+ output += text;
224
+ stderr += text;
225
+ });
226
+
227
+ proc.on('close', (code) => {
228
+ const combinedOutput = output.trim();
229
+ const metadata = {
230
+ taskType: 'shell',
231
+ exitCode: code,
232
+ stdout: stdout.trim(),
233
+ stderr: stderr.trim()
234
+ };
235
+
236
+ if (code === 0) {
237
+ printer.finish(cmd);
238
+ resolve(TaskResult.success(combinedOutput, metadata));
239
+ } else {
240
+ printer.error(cmd, new Error(`Exit code: ${code}`));
241
+ resolve(TaskResult.fail(combinedOutput, metadata));
242
+ }
243
+ });
244
+
245
+ proc.on('error', (error) => {
246
+ printer.error(cmd, error);
247
+ resolve(TaskResult.fail(error.message, { taskType: 'shell' }));
248
+ });
249
+ });
250
+ }
package/src/upgrade.js CHANGED
@@ -1,4 +1,4 @@
1
- import { exec, execSync } from 'child_process';
1
+ import { exec, execSync, spawn } from 'child_process';
2
2
  import { readFileSync, writeFileSync, existsSync } from 'fs';
3
3
  import { platform } from 'os';
4
4
  import { homedir } from 'os';
@@ -13,18 +13,44 @@ export async function upgradeCommand(options = {}) {
13
13
  const installMethod = detectInstallMethod();
14
14
  console.log(`Bunosh is installed via: ${color.bold(installMethod)}`);
15
15
 
16
- if (installMethod === 'bun') {
17
- if (!check) {
16
+ if (installMethod === 'bun' || installMethod === 'npm') {
17
+ const currentVersion = getCurrentVersion();
18
+ let latestVersion = null;
19
+ try {
20
+ latestVersion = await getLatestNpmVersion();
21
+ } catch (e) {
22
+ }
23
+
24
+ console.log(`Current version: ${color.bold(currentVersion)}`);
25
+ if (latestVersion) {
26
+ console.log(`Latest version: ${color.bold(latestVersion)}`);
27
+ }
28
+
29
+ if (latestVersion && !force && !isNewerVersion(latestVersion, currentVersion)) {
30
+ console.log(color.green('You are already on the latest version!'));
31
+ if (!force) {
32
+ console.log(` Use ${color.bold('--force')} to reinstall the current version.`);
33
+ }
34
+ return;
35
+ }
36
+
37
+ const upgradeCmd = installMethod === 'bun'
38
+ ? 'bun add -g bunosh@latest --force'
39
+ : 'npm install -g bunosh@latest';
40
+
41
+ if (check) {
42
+ console.log('Will upgrade with: ' + color.bold(upgradeCmd));
43
+ return;
44
+ }
45
+
46
+ if (installMethod === 'bun') {
18
47
  await upgradeWithBun();
19
48
  } else {
20
- console.log('Will upgrade with: ' + color.bold('bun add -g bunosh'));
21
- }
22
- } else if (installMethod === 'npm') {
23
- if (!check) {
24
49
  await upgradeWithNpm();
25
- } else {
26
- console.log('Will upgrade with: ' + color.bold('npm update -g bunosh'));
27
50
  }
51
+
52
+ console.log();
53
+ console.log(`Run ${color.bold('bunosh --version')} to verify the new version.`);
28
54
  } else if (installMethod === 'executable') {
29
55
  await upgradeExecutable({ force, check });
30
56
  } else {
@@ -101,36 +127,115 @@ function detectInstallMethod() {
101
127
  return 'unknown';
102
128
  }
103
129
 
104
- async function upgradeWithBun() {
105
- console.log('Upgrading with Bun...');
106
-
130
+ function runStreaming(command, args) {
107
131
  return new Promise((resolve, reject) => {
108
- exec('bun add -g bunosh', (error, stdout, stderr) => {
109
- if (error) {
110
- reject(new Error(`Bun upgrade failed: ${stderr || error.message}`));
132
+ const child = spawn(command, args, { stdio: 'inherit' });
133
+ child.on('error', (error) => reject(new Error(`${command} not available: ${error.message}`)));
134
+ child.on('close', (code) => {
135
+ if (code === 0) {
136
+ resolve();
111
137
  return;
112
138
  }
113
- console.log(stdout);
114
- console.log(color.green('Upgrade successful!'));
115
- resolve();
139
+ reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
116
140
  });
117
141
  });
118
142
  }
119
143
 
144
+ async function upgradeWithBun() {
145
+ console.log('Upgrading with Bun...');
146
+
147
+ try {
148
+ await runStreaming('bun', ['add', '-g', 'bunosh@latest', '--force']);
149
+ console.log(color.green('Upgrade successful!'));
150
+ return;
151
+ } catch (error) {
152
+ console.warn(color.yellow(`Bun upgrade failed: ${error.message}`));
153
+ console.warn(color.yellow('Falling back to npm (a known Bun global-install bug)...'));
154
+ }
155
+
156
+ await upgradeWithNpm();
157
+ }
158
+
120
159
  async function upgradeWithNpm() {
121
160
  console.log('Upgrading with npm...');
122
-
123
- return new Promise((resolve, reject) => {
124
- exec('npm update -g bunosh', (error, stdout, stderr) => {
125
- if (error) {
126
- reject(new Error(`npm upgrade failed: ${stderr || error.message}`));
127
- return;
128
- }
129
- console.log(stdout);
130
- console.log(color.green('Upgrade successful!'));
131
- resolve();
161
+
162
+ await runStreaming('npm', ['install', '-g', 'bunosh@latest']);
163
+ console.log(color.green('Upgrade successful!'));
164
+ }
165
+
166
+ export async function getLatestNpmVersion(timeoutMs = 3000) {
167
+ const controller = new AbortController();
168
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
169
+
170
+ try {
171
+ const response = await fetch('https://registry.npmjs.org/bunosh/latest', {
172
+ headers: { 'User-Agent': 'bunosh' },
173
+ signal: controller.signal,
132
174
  });
133
- });
175
+
176
+ if (!response.ok) {
177
+ throw new Error(`npm registry error: ${response.status}`);
178
+ }
179
+
180
+ const data = await response.json();
181
+ return data.version;
182
+ } finally {
183
+ clearTimeout(timer);
184
+ }
185
+ }
186
+
187
+ async function getCachedLatestNpmVersion() {
188
+ const cacheDir = join(homedir(), '.bunosh');
189
+ const cacheFile = join(cacheDir, 'version-check.json');
190
+ const TTL = 12 * 60 * 60 * 1000;
191
+
192
+ try {
193
+ if (existsSync(cacheFile)) {
194
+ const cache = JSON.parse(readFileSync(cacheFile, 'utf8'));
195
+ if (cache.latest && cache.checkedAt && Date.now() - cache.checkedAt < TTL) {
196
+ return cache.latest;
197
+ }
198
+ }
199
+ } catch (e) {
200
+ }
201
+
202
+ let latest = null;
203
+ try {
204
+ latest = await getLatestNpmVersion(2000);
205
+ } catch (e) {
206
+ return null;
207
+ }
208
+
209
+ try {
210
+ await mkdir(cacheDir, { recursive: true });
211
+ writeFileSync(cacheFile, JSON.stringify({ checkedAt: Date.now(), latest }));
212
+ } catch (e) {
213
+ }
214
+
215
+ return latest;
216
+ }
217
+
218
+ export async function printUpgradeNoticeIfAvailable() {
219
+ if (process.env.NODE_ENV === 'test' || process.env.VITEST_WORKER_ID !== undefined) {
220
+ return;
221
+ }
222
+
223
+ try {
224
+ const currentVersion = getCurrentVersion();
225
+ if (!currentVersion || currentVersion === 'unknown') {
226
+ return;
227
+ }
228
+
229
+ const latestVersion = await getCachedLatestNpmVersion();
230
+ if (!latestVersion || !isNewerVersion(latestVersion, currentVersion)) {
231
+ return;
232
+ }
233
+
234
+ console.log();
235
+ console.log(color.yellow(`🦾 A new version of Bunosh is available: ${color.bold(currentVersion)} → ${color.bold(latestVersion)}`));
236
+ console.log(color.dim(` Run ${color.bold('bunosh upgrade')} to update.`));
237
+ } catch (e) {
238
+ }
134
239
  }
135
240
 
136
241
  export async function upgradeExecutable(options = {}) {
@@ -199,7 +304,7 @@ function getCurrentVersion() {
199
304
  }
200
305
 
201
306
  async function getLatestRelease() {
202
- const response = await fetch('https://api.github.com/repos/davertmik/bunosh/releases/latest', {
307
+ const response = await fetch('https://api.github.com/repos/DavertMik/bunosh/releases/latest', {
203
308
  headers: {
204
309
  'User-Agent': 'bunosh'
205
310
  }