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.
package/src/upgrade.js ADDED
@@ -0,0 +1,255 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { execSync } from 'child_process';
5
+
6
+ /**
7
+ * Detects if bunosh is running as a single executable or npm package
8
+ */
9
+ export function isExecutable() {
10
+ try {
11
+ // Check if we're running from a compiled executable
12
+ // In Bun compiled binaries, process.execPath points to the executable
13
+ const execPath = process.execPath;
14
+ const isCompiledBinary = !execPath.includes('node_modules') &&
15
+ !execPath.includes('.bun') &&
16
+ (execPath.includes('bunosh') || path.basename(execPath).startsWith('bunosh'));
17
+
18
+ return isCompiledBinary;
19
+ } catch (error) {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Gets the current executable path
26
+ */
27
+ export function getExecutablePath() {
28
+ if (!isExecutable()) {
29
+ throw new Error('Not running as executable');
30
+ }
31
+ return process.execPath;
32
+ }
33
+
34
+ /**
35
+ * Detects the platform and architecture for download
36
+ */
37
+ export function getPlatformInfo() {
38
+ const platform = process.platform;
39
+ const arch = process.arch;
40
+
41
+ // Map to GitHub release asset names
42
+ switch (platform) {
43
+ case 'linux':
44
+ if (arch === 'x64' || arch === 'x86_64') {
45
+ return { platform: 'linux', arch: 'x64', asset: 'bunosh-linux-x64.tar.gz' };
46
+ }
47
+ break;
48
+ case 'darwin':
49
+ if (arch === 'arm64') {
50
+ return { platform: 'darwin', arch: 'arm64', asset: 'bunosh-darwin-arm64.tar.gz' };
51
+ }
52
+ if (arch === 'x64' || arch === 'x86_64') {
53
+ return { platform: 'darwin', arch: 'x64', asset: 'bunosh-darwin-x64.tar.gz' };
54
+ }
55
+ break;
56
+ case 'win32':
57
+ if (arch === 'x64' || arch === 'x86_64') {
58
+ return { platform: 'windows', arch: 'x64', asset: 'bunosh-windows-x64.exe.zip' };
59
+ }
60
+ break;
61
+ }
62
+
63
+ throw new Error(`Unsupported platform: ${platform} ${arch}`);
64
+ }
65
+
66
+ /**
67
+ * Fetches the latest release info from GitHub
68
+ */
69
+ export async function getLatestRelease() {
70
+ try {
71
+ const response = await fetch('https://api.github.com/repos/davertmik/bunosh/releases/latest');
72
+ if (!response.ok) {
73
+ throw new Error(`GitHub API error: ${response.status}`);
74
+ }
75
+ return await response.json();
76
+ } catch (error) {
77
+ throw new Error(`Failed to fetch release info: ${error.message}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Gets current version from package.json or executable
83
+ */
84
+ export function getCurrentVersion() {
85
+ try {
86
+ // Try to read from package.json first
87
+ const packagePath = path.join(process.cwd(), 'package.json');
88
+ if (fs.existsSync(packagePath)) {
89
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
90
+ if (pkg.name === 'bunosh') {
91
+ return pkg.version;
92
+ }
93
+ }
94
+
95
+ // For executable, version might be embedded or we can try --version
96
+ try {
97
+ const version = execSync('bunosh --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
98
+ return version;
99
+ } catch (error) {
100
+ // Fallback to unknown
101
+ return 'unknown';
102
+ }
103
+ } catch (error) {
104
+ return 'unknown';
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Compares version strings (simple semantic version comparison)
110
+ */
111
+ export function isNewerVersion(latest, current) {
112
+ if (current === 'unknown') return true;
113
+
114
+ // Remove 'v' prefix if present
115
+ const latestClean = latest.replace(/^v/, '');
116
+ const currentClean = current.replace(/^v/, '');
117
+
118
+ const latestParts = latestClean.split('.').map(n => parseInt(n) || 0);
119
+ const currentParts = currentClean.split('.').map(n => parseInt(n) || 0);
120
+
121
+ // Pad arrays to same length
122
+ const maxLength = Math.max(latestParts.length, currentParts.length);
123
+ while (latestParts.length < maxLength) latestParts.push(0);
124
+ while (currentParts.length < maxLength) currentParts.push(0);
125
+
126
+ // Compare each part
127
+ for (let i = 0; i < maxLength; i++) {
128
+ if (latestParts[i] > currentParts[i]) return true;
129
+ if (latestParts[i] < currentParts[i]) return false;
130
+ }
131
+
132
+ return false; // Versions are equal
133
+ }
134
+
135
+ /**
136
+ * Downloads and extracts the new binary
137
+ */
138
+ export async function downloadAndInstall(release, platformInfo, executablePath, onProgress) {
139
+ const asset = release.assets.find(a => a.name === platformInfo.asset);
140
+ if (!asset) {
141
+ throw new Error(`No asset found for platform: ${platformInfo.asset}`);
142
+ }
143
+
144
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bunosh-upgrade-'));
145
+ const downloadPath = path.join(tempDir, asset.name);
146
+
147
+ try {
148
+ // Download with progress
149
+ onProgress?.('Downloading latest release...');
150
+ const response = await fetch(asset.browser_download_url);
151
+ if (!response.ok) {
152
+ throw new Error(`Download failed: ${response.status}`);
153
+ }
154
+
155
+ const buffer = await response.arrayBuffer();
156
+ fs.writeFileSync(downloadPath, Buffer.from(buffer));
157
+ onProgress?.('Download complete');
158
+
159
+ // Extract and replace
160
+ onProgress?.('Extracting and installing...');
161
+
162
+ // Create backup of current executable
163
+ const backupPath = executablePath + '.backup';
164
+ fs.copyFileSync(executablePath, backupPath);
165
+
166
+ if (platformInfo.platform === 'windows') {
167
+ // Handle ZIP extraction for Windows
168
+ execSync(`powershell -command "Expand-Archive -Path '${downloadPath}' -DestinationPath '${tempDir}' -Force"`);
169
+ const extractedExe = path.join(tempDir, 'bunosh-windows-x64.exe');
170
+ fs.copyFileSync(extractedExe, executablePath);
171
+ } else {
172
+ // Handle tar.gz extraction for Unix
173
+ const extractDir = path.join(tempDir, 'extracted');
174
+ fs.mkdirSync(extractDir);
175
+
176
+ execSync(`tar -xzf "${downloadPath}" -C "${extractDir}"`);
177
+
178
+ // Find the executable in extracted files
179
+ const extractedFiles = fs.readdirSync(extractDir);
180
+ const executableName = extractedFiles.find(f => f.startsWith('bunosh-'));
181
+
182
+ if (!executableName) {
183
+ throw new Error('Could not find executable in downloaded archive');
184
+ }
185
+
186
+ const extractedPath = path.join(extractDir, executableName);
187
+ fs.copyFileSync(extractedPath, executablePath);
188
+ fs.chmodSync(executablePath, 0o755);
189
+ }
190
+
191
+ onProgress?.('Installation complete');
192
+
193
+ // Clean up
194
+ fs.rmSync(tempDir, { recursive: true, force: true });
195
+ fs.unlinkSync(backupPath);
196
+
197
+ return true;
198
+ } catch (error) {
199
+ // Restore backup if something went wrong
200
+ const backupPath = executablePath + '.backup';
201
+ if (fs.existsSync(backupPath)) {
202
+ try {
203
+ fs.copyFileSync(backupPath, executablePath);
204
+ fs.unlinkSync(backupPath);
205
+ } catch (restoreError) {
206
+ console.error('Failed to restore backup:', restoreError.message);
207
+ }
208
+ }
209
+
210
+ // Clean up temp directory
211
+ if (fs.existsSync(tempDir)) {
212
+ fs.rmSync(tempDir, { recursive: true, force: true });
213
+ }
214
+
215
+ throw error;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Main upgrade function
221
+ */
222
+ export async function upgradeExecutable(options = {}) {
223
+ const { force = false, onProgress } = options;
224
+
225
+ if (!isExecutable()) {
226
+ throw new Error('Upgrade is only available for single executable installations. Use "npm update -g bunosh" for npm installations.');
227
+ }
228
+
229
+ onProgress?.('Checking for updates...');
230
+
231
+ const currentVersion = getCurrentVersion();
232
+ const release = await getLatestRelease();
233
+ const latestVersion = release.tag_name;
234
+
235
+ if (!force && !isNewerVersion(latestVersion, currentVersion)) {
236
+ return {
237
+ updated: false,
238
+ currentVersion,
239
+ latestVersion,
240
+ message: `Already on latest version: ${currentVersion}`
241
+ };
242
+ }
243
+
244
+ const platformInfo = getPlatformInfo();
245
+ const executablePath = getExecutablePath();
246
+
247
+ await downloadAndInstall(release, platformInfo, executablePath, onProgress);
248
+
249
+ return {
250
+ updated: true,
251
+ currentVersion,
252
+ latestVersion,
253
+ message: `Successfully upgraded from ${currentVersion} to ${latestVersion}`
254
+ };
255
+ }
package/types.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ interface FileLine {
2
+ (strings: TemplateStringsArray | string, ...values: any[]): void;
3
+ fromFile(file: string): void;
4
+ currentFile(): void;
5
+ }
6
+
7
+ type TaskStatus = {
8
+ RUNNING: string;
9
+ FAIL: string;
10
+ SUCCESS: string;
11
+ };
12
+
13
+ declare function exec(cmd: TemplateStringsArray, ...values: any[]): {
14
+ env: (newEnvs: Record<string, string>) => Promise<void>;
15
+ cwd: (newCwd: string) => Promise<void>;
16
+ };
17
+
18
+ declare global {
19
+ namespace NodeJS {
20
+ interface Global {
21
+ bunosh: {
22
+ io: {
23
+ say(...args: any[]): void;
24
+ ask(question: string, opts?: Record<string, any>): Promise<any>;
25
+ yell(text: string): void;
26
+ }
27
+ fetch: typeof import('node-fetch');
28
+ exec: typeof exec;
29
+ $: typeof exec;
30
+ writeToFile(
31
+ fileName: string,
32
+ lineBuilderFn: ((fileLine: FileLine) => void) | string
33
+ ): void;
34
+ copyFile(src: string, dst: string): void;
35
+ stopOnFail(enable?: boolean): void;
36
+ ignoreFail(enable?: boolean): void;
37
+ buildCmd(cmd: string): (args: string) => Promise<any>;
38
+ task(name: string | Function, fn?: Function): Promise<any>;
39
+ };
40
+ }
41
+ }
42
+ }
43
+
44
+ export {};
package/run.js DELETED
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env bun
2
- import program, { BUNOSHFILE, banner } from "./src/program";
3
- import { existsSync, readFileSync } from "fs";
4
- import init from "./src/init";
5
- import path from "path";
6
- import io from './src/io';
7
- import fetch from './src/tasks/fetch';
8
- import writeToFile from "./src/tasks/writeToFile";
9
- import copyFile from "./src/tasks/copyFile";
10
- import exec from "./src/tasks/exec";
11
-
12
- const tasksFile = path.join(process.cwd(), BUNOSHFILE);
13
-
14
- if (!existsSync(tasksFile)) {
15
- console.log(banner);
16
-
17
- if (process.argv.includes('init')) {
18
- init();
19
- process.exit(0);
20
- }
21
-
22
- console.log();
23
- console.error(`Bunosh file not found: ${tasksFile}`);
24
- console.log("Run `bunosh init` to create a new bunosh tasks file here")
25
- console.log();
26
- process.exit(1);
27
- }
28
-
29
- global.bunosh = {
30
- ...io,
31
- fetch,
32
- exec,
33
- writeToFile,
34
- copyFile,
35
- }
36
-
37
- import(tasksFile).then((tasks) => {
38
- program(tasks, readFileSync(tasksFile, "utf-8"));
39
- }).catch((e) => {
40
- console.error(`Error loading: ${tasksFile}`);
41
- console.error(e);
42
- });
package/src/io.jsx DELETED
@@ -1,47 +0,0 @@
1
- import React, {useState, useEffect} from 'react';
2
- import { Box, Text } from 'ink';
3
- import { renderOnce, isStaticOutput} from './output';
4
- import Gradient from 'ink-gradient';
5
- import BigText from 'ink-big-text';
6
- import inquirer from 'inquirer';
7
-
8
- export function say(...args) {
9
- if (isStaticOutput) {
10
- console.log(...args);
11
- return;
12
- };
13
-
14
- const colors = ['yellow', 'magenta', 'cyan', 'blue', 'blueBright', 'magentaBright', 'cyanBright', 'whiteBright'];
15
-
16
- renderOnce(
17
- <Box gap={1} height={20} overflow='hidden' >
18
- <Text color='white'>!</Text>
19
- {args.map((arg, i) => <Text color={colors[i]} key={i}>{arg}</Text>)}
20
- </Box>
21
- );
22
- }
23
-
24
- export async function ask(question, opts = {}) {
25
-
26
- const answers = await inquirer.prompt({ name: question, message: question, ...opts })
27
-
28
- return Object.values(answers)[0];
29
- }
30
-
31
- export function yell(text) {
32
- if (isStaticOutput) {
33
- console.log();
34
- console.log(text.toUpperCase());
35
- console.log();
36
- return;
37
- };
38
-
39
- renderOnce(
40
- <Box gap={1} marginLeft={1}>
41
- <Gradient name="teen">
42
- <BigText text={text}/>
43
- </Gradient>
44
-
45
- </Box>
46
- );
47
- }
package/src/output.js DELETED
@@ -1,37 +0,0 @@
1
- import { render as inkRender } from 'ink';
2
- import debug from 'debug';
3
-
4
- export const isStaticOutput = process.env.CI || process.env.DEBUG || !process.stdout.isTTY;
5
-
6
- if (isStaticOutput) debug.enable('bunosh:*');
7
-
8
- let renderer = null;
9
-
10
- export function render(comp = <></>) {
11
-
12
- if (!renderer) {
13
- renderer = inkRender(comp);
14
- return renderer;
15
- }
16
-
17
- renderer.rerender(comp);
18
- return renderer;
19
- }
20
-
21
- export async function renderOnce(comp) {
22
- if (renderer) await renderer.waitUntilExit();
23
- render(comp);
24
- clearRenderer();
25
- }
26
-
27
- export function clearRenderer() {
28
- if (!renderer) return;
29
- renderer.unmount();
30
- renderer = null;
31
- }
32
-
33
-
34
- export function debugTask(task, line) {
35
- const ns = `bunosh:${task}`;
36
- debug(ns)(line);
37
- }
package/src/task.jsx DELETED
@@ -1,209 +0,0 @@
1
- import React, {useState, useEffect} from 'react';
2
- import { Text, Box } from 'ink';
3
- import Spinner from 'ink-spinner';
4
- import { Timer } from 'timer-node';
5
- import { render, clearRenderer, renderOnce } from './output';
6
-
7
- export const TaskStatus = {
8
- RUNNING: 'running',
9
- FAIL: 'fail',
10
- SUCCESS: 'success'
11
- };
12
-
13
- export const tasksExecuted = [];
14
-
15
-
16
- let activeComponents = [];
17
- let activeTasks = [];
18
-
19
- let stopFailToggle = true;
20
-
21
- export function stopOnFail(enable = true) {
22
- stopFailToggle = enable;
23
- }
24
-
25
- export function ignoreFail(enable = true) {
26
- stopFailToggle = !enable;
27
- }
28
-
29
- const globalTimer = new Timer({ label: 'global', precision: 'ms'});
30
- globalTimer.start();
31
- process.on('exit', (code) => {
32
- // we don't need this banner if no tasks were executed
33
- if (!process.env.BUNOSH_COMMAND_STARTED) return;
34
-
35
- globalTimer.stop();
36
- const success = code === 0;
37
- const tasksFailed = tasksExecuted.filter(ti => ti.result?.status === TaskStatus.FAIL).length;
38
- renderOnce(<Box flexDirection='row' gap={2}>
39
- <Text bold backgroundColor={!success && 'red'}>🍲
40
- {success ? '' : 'FAIL '}
41
- </Text>
42
- <Text dimColor >Exit Code: <Text bold color={code === 0 ? 'green' : 'red'}>{code}</Text></Text>
43
- <Text dimColor>Tasks executed: <Text bold>{tasksExecuted.length}</Text></Text>
44
- {!!tasksFailed && <Text dimColor>Tasks failed: <Text bold color="red">{tasksFailed}</Text></Text>}
45
- <Text dimColor>Time: <Text bold>{globalTimer.ms()}</Text>ms</Text>
46
- </Box>);
47
- });
48
-
49
- export async function task(name, fn) {
50
- let fnResult = null;
51
-
52
- if (!fn) {
53
- fn = name;
54
- name = fn.toString().slice(0, 50).replace(/\s+/g, ' ').trim();
55
- }
56
-
57
- const promise = Promise.resolve(fn()).then((ret) => {
58
- fnResult = ret;
59
- return TaskResult.success(ret);
60
- }).catch((err) => {
61
- return TaskResult.fail(err);
62
- });
63
- const taskInfo = new TaskInfo({ promise, kind: 'task', text: name });
64
-
65
- const TaskOutput = () => {
66
- const [output, setOutput] = useState(null);
67
- useEffect(() => {
68
- if (!fnResult?.toString) return;
69
- promise.then(_ => setOutput(fnResult.toString()));
70
- });
71
-
72
- return <Box overflow='hidden' height={10} borderStyle="round" >
73
- <Text dimColor={true}>{output}</Text>
74
- </Box>
75
- }
76
- renderTask(taskInfo, <TaskOutput />);
77
-
78
- await promise;
79
- return fnResult;
80
- }
81
-
82
- function addToRender(comp) {
83
- activeComponents.push(comp);
84
- activeTasks.push(comp.props.taskInfo);
85
-
86
- if (activeComponents.length < 2) {
87
- render(comp);
88
- return;
89
- }
90
- render(<Box flexDirection='row' gap={1}>
91
- {activeComponents}
92
- </Box>);
93
- }
94
-
95
- function removeFromRender(taskInfo) {
96
- activeTasks = activeTasks.filter((ti) => ti?.id !== taskInfo?.id);
97
-
98
- if (activeTasks.length === 0) {
99
- clearRenderer();
100
- activeComponents = [];
101
- return;
102
- }
103
- }
104
-
105
- export const renderTask = async (taskInfo, children) => {
106
- if (tasksExecuted.map(t => t.id).includes(taskInfo.id)) return; // alraday executed
107
-
108
- tasksExecuted.push(taskInfo);
109
- addToRender(<Task key={`${tasksExecuted.length}_${taskInfo}`} taskInfo={taskInfo}>{children}</Task>);
110
- };
111
-
112
-
113
- function Status({ taskStatus }) {
114
- if (!taskStatus) return <Text dimColor>-</Text>;
115
- if (taskStatus == TaskStatus.SUCCESS) return <Text color='green' bold>✓</Text>;
116
- if (taskStatus == TaskStatus.FAIL) return <Text color='red' bold>×</Text>;
117
- }
118
-
119
- export const Task = ({ taskInfo, children }) => {
120
- const timer = new Timer({ label: taskInfo.text, precision: 'ms'});
121
-
122
- const [time, setTime] = useState(null);
123
- const [status, setStatus] = useState(null);
124
-
125
- const { promise } = taskInfo;
126
- timer.start();
127
-
128
- function updateTaskInfo(result) {
129
- timer.stop();
130
- taskInfo.result = result;
131
- taskInfo.time = timer.ms();
132
- setStatus(result.status);
133
- setTime(timer.ms());
134
- removeFromRender(taskInfo);
135
-
136
- // hard exit, task has failed
137
- if (result.status === TaskStatus.FAIL && stopFailToggle) {
138
- process.exit(1);
139
-
140
- }
141
- }
142
-
143
- useEffect(() => {
144
- promise.then((result) => {
145
- updateTaskInfo(result)
146
- }).catch((err) => {
147
- updateTaskInfo(TaskResult.fail(err.toString()));
148
- });
149
- }, []);
150
-
151
- return (<Box flexGrow={1} flexBasis="50%" flexDirection='column'>
152
- <Box gap={1} flexDirection='row' alignItems='flex-start' justifyContent="flex-start">
153
- <Status taskStatus={status} />
154
-
155
- {taskInfo.titleComponent}
156
-
157
- {taskInfo.extraText && <Text color='cyan' dimColor>{taskInfo.extraText}</Text>}
158
-
159
- {time === null && <Spinner />}
160
- {time !== null && <Text dimColor={true}>{time}ms</Text>}
161
-
162
- </Box>
163
-
164
- {children}
165
- </Box>
166
- );
167
- };
168
-
169
- export class TaskInfo {
170
- constructor({ promise, kind, text, extraText }) {
171
- if (!kind) throw new Error('TaskInfo: kind is required');
172
- if (!text) throw new Error('TaskInfo: text is required');
173
-
174
- this.id = `${kind}-${text.slice(0,30).replace(/\s/g, '-')}-${Math.random().toString(36).substring(7)}`;
175
- this.kind = kind;
176
- this.text = text;
177
- this.extraText = extraText;
178
- this.promise = promise;
179
- this.result = null;
180
- this.time = null;
181
- }
182
-
183
- get titleComponent() {
184
- return <>
185
- <Text bold>{this.kind}</Text>
186
- <Text color='yellow'>{this.text}</Text>
187
- </>;
188
- }
189
-
190
- toString() {
191
- return `${this.kind} ${this.text}`;
192
- }
193
- }
194
-
195
- export class TaskResult {
196
- constructor({ status, output }) {
197
- this.status = status;
198
- this.output = output;
199
- }
200
-
201
- static fail(output = null) {
202
- return new TaskResult({ status: TaskStatus.FAIL, output });
203
- }
204
-
205
- static success(output = null) {
206
- return new TaskResult({ status: TaskStatus.SUCCESS, output });
207
-
208
- }
209
- }
@@ -1,14 +0,0 @@
1
- const { copySync } = require('fs-extra');
2
- import { task } from '../task.jsx';
3
-
4
- export default function copyFile(src, dst) {
5
-
6
- task(`copy ${src} ⇒ ${dst}`, () => {
7
- copySync(src, dst);
8
- });
9
-
10
- }
11
-
12
-
13
-
14
-