bunosh 0.1.4 → 0.2.2
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/COMPLETION.md +214 -0
- package/README.md +489 -73
- package/UPGRADE.md +200 -0
- package/bunosh.js +56 -0
- package/index.js +22 -11
- package/package.json +32 -24
- package/src/completion.js +341 -0
- package/src/font.js +258 -0
- package/src/formatters/base.js +17 -0
- package/src/formatters/console.js +81 -0
- package/src/formatters/factory.js +17 -0
- package/src/formatters/github-actions.js +43 -0
- package/src/init.js +13 -6
- package/src/io.js +20 -0
- package/src/printer.js +91 -0
- package/src/program.js +374 -154
- package/src/task.js +148 -0
- package/src/tasks/copyFile.js +21 -0
- package/src/tasks/exec.js +204 -0
- package/src/tasks/fetch.js +47 -0
- package/src/tasks/{writeToFile.jsx → writeToFile.js} +18 -16
- package/src/upgrade.js +255 -0
- package/types.d.ts +44 -0
- package/run.js +0 -31
- package/src/io.jsx +0 -47
- package/src/output.js +0 -37
- package/src/task.jsx +0 -209
- package/src/tasks/copyFile.jsx +0 -14
- package/src/tasks/exec.jsx +0 -104
- package/src/tasks/fetch.jsx +0 -74
- package/templates/banner.js +0 -8
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,31 +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 { say, yell } from './src/io';
|
|
7
|
-
|
|
8
|
-
const tasksFile = path.join(process.cwd(), BUNOSHFILE);
|
|
9
|
-
|
|
10
|
-
if (!existsSync(tasksFile)) {
|
|
11
|
-
console.log(banner);
|
|
12
|
-
|
|
13
|
-
if (process.argv.includes('init')) {
|
|
14
|
-
init();
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
console.log();
|
|
19
|
-
console.error(`Bunosh file not found: ${tasksFile}`);
|
|
20
|
-
console.log("Run `bunosh init` to create a new bunosh tasks file here")
|
|
21
|
-
console.log();
|
|
22
|
-
process.ar
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
global.say = say;
|
|
27
|
-
global.yell = yell;
|
|
28
|
-
|
|
29
|
-
import(tasksFile).then((tasks) => {
|
|
30
|
-
program(tasks, readFileSync(tasksFile, "utf-8"));
|
|
31
|
-
});
|
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
|
-
}
|
package/src/tasks/copyFile.jsx
DELETED