bunosh 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,15 +1,125 @@
1
- # bunosh
1
+ # 🍲 bunosh
2
2
 
3
- To install dependencies:
3
+ > *Named after **banosh**, a traditional Ukrainian dish from cornmeal cooked with various ingredients such as mushrooms, cheese, sour cream*
4
+
5
+ <p align="center">
6
+ <img src="assets/logo.jpg" alt="Logo">
7
+ </p>
8
+
9
+ ## What it is?
10
+
11
+ Bunosh is a task runner powered by [Bun](https://bun.sh). Bunosh is aimed to help you to write common scripts in JavaScript with less effort. Combines awesome tools: Bun, Commander.js, Ink, Inquirer in a the most compact way.
12
+
13
+ What can you automate with Bunosh:
14
+
15
+ * shell scripts → use JavaScript to write them (you know it better, anyway!)
16
+ * boilerplate scripts → create new files from templates
17
+ * deploy scripts → combine JS and Shell commands to build and deploy
18
+ * parallel tasks → with native `Promise.all` execute multiple tasks at once
19
+ * ...every other task you have previously written a custom `js` or `shell` script
20
+
21
+ Bunosh will get your scripts cooked into a **single JavaScript file**.
22
+
23
+ Each function of this file:
24
+ ```js
25
+ /** Cleans up tmp & logs dir **/
26
+ export async function cleanTmp() {
27
+ await $`rm -rf tmp/*`;
28
+ await $`rm -rf logs/*`;
29
+ }
30
+
31
+ /** Builds Docker images for project **/
32
+ export async function build(opts = { push: false }) {
33
+ await Promise.all([
34
+ buildFrontend(),
35
+ buildBackend(),
36
+ ])
37
+
38
+ if (opts.push) {
39
+ await $`docker push frontend`;
40
+ await $`docker push backend`;
41
+ }
42
+ }
43
+
44
+ /** Deploys application **/
45
+ export async function deploy(env = 'staging') {
46
+ await build();
47
+ // ...
48
+ }
49
+
50
+ /** Adds value to config **/
51
+ export async function addToConfig(key, value) {}
52
+
53
+ //....
54
+ ```
55
+
56
+ Is turned into an executable command:
57
+ ```
58
+ Commands:
59
+ add:to-config Adds value to config
60
+ bunosh add:to-config [key] [value]
61
+ build Builds Docker images for project
62
+ bunosh build --push
63
+ clean:tmp Cleans up tmp & logs dir
64
+ deploy Deploys application
65
+ bunosh deploy [env=staging]
66
+
67
+
68
+ Special Commands:
69
+ 📝 Edit bunosh file: bunosh edit
70
+ 📥 Export scripts to package.json: bunosh export:scripts
71
+
72
+ ```
73
+
74
+ ### Command Rules:
75
+
76
+ * each exported function is a command:
77
+ * `function addUserModel` → `bunosh add:user-model`
78
+ * each function argument is a command argument:
79
+ * `addUser(name)` → `bunosh add:user <name>`
80
+ * `addUser(name='john')` → `bunosh add:user [name=john]`
81
+ * last argument passed as an object defines options:
82
+ * `clean(opts = {tmp: false, logs: false})` → `bunosh clean --tmp --logs`
83
+ * `addUsers(opts = {num: 1})` → `bunosh add:users --num 1`
84
+ * docblock or first-line comment of a function makes a command description
85
+ * functions can call make fetch requests, exec shell commands or call other functions
86
+
87
+
88
+ ### Installation
89
+
90
+ Install [Bun](https://bun.sh) (faster NodeJS alternative)
91
+
92
+ Install Bunosh globally:
4
93
 
5
94
  ```bash
6
- bun install
95
+ bun install -g bunosh
7
96
  ```
8
97
 
9
- To run:
98
+ Create a new tasks file in a project directory:
10
99
 
11
100
  ```bash
12
- bun run index.ts
101
+ bunosh init
13
102
  ```
14
103
 
15
- This project was created using `bun init` in bun v1.0.20. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
104
+ ### API
105
+
106
+ Commands can be written using classical NodeJS API using `fs` or `child_process` modules and print output using `console.log`. However, this doesn't unleash true Bunosh power.
107
+
108
+ Bunosh ships with a built-in functions to simplify writing scripts:
109
+
110
+ ### Exec
111
+
112
+ `exec` or `$` is a wrapper around [Bun Shell](https://bun.sh/docs/runtime/shell). It is cross-platform bash-like shell with seamless JavaScript interop.
113
+
114
+ ```js
115
+ import { exec } from `bunosh`
116
+
117
+ export async function build()
118
+ {
119
+ await exec`docker ps`
120
+ await exec`docker build -t api .`
121
+ }
122
+ ```
123
+
124
+ Bunosh wraps `$` to make a fancy realtime output with Ink:
125
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "module": "index.js",
5
5
  "bin": {
6
6
  "bunosh": "./run.js"
@@ -24,10 +24,18 @@
24
24
  "ink-spinner": "^5.0.0",
25
25
  "inquirer": "^9.2.13",
26
26
  "open-editor": "^4.1.1",
27
- "picocolors": "^1.0.0",
28
27
  "react": "^18.2.0",
29
28
  "timer-node": "^5.0.7"
30
29
  },
30
+ "license": "ISC",
31
+ "files": [
32
+ "index.js",
33
+ "run.js",
34
+ "io.js",
35
+ "files.js",
36
+ "src",
37
+ "templates"
38
+ ],
31
39
  "lint-staged": {
32
40
  "*.{js,css,md}": "prettier --write"
33
41
  },
package/run.js CHANGED
@@ -1,10 +1,11 @@
1
-
1
+ #!/usr/bin/env bun
2
2
  import program, { BUNOSHFILE, banner } from "./src/program";
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import init from "./src/init";
5
+ import path from "path";
5
6
  import { say, yell } from './io';
6
7
 
7
- const tasksFile = `./${BUNOSHFILE}`;
8
+ const tasksFile = path.join(process.cwd(), BUNOSHFILE);
8
9
 
9
10
  if (!existsSync(tasksFile)) {
10
11
  console.log(banner);
package/src/init.js CHANGED
@@ -1,11 +1,40 @@
1
1
  import { BUNOSHFILE } from "./program";
2
2
  import color from "picocolors";
3
- import fs from "fs";
4
- import templateFile from "../templates/init";
3
+ import fs from 'fs';
4
+
5
+ const template = `
6
+ import { exec, fetch, ignoreFail } from 'bunosh';
7
+ import { say, yell } from 'bunosh/io';
8
+ import { writeToFile } from 'bunosh/files';
9
+
10
+ /**
11
+ * 🎉 Hello world command
12
+ */
13
+ export async function helloWorld() {
14
+ // use say() to print to the console
15
+ // say('Hello World!');
16
+
17
+ // use exec\`\` to execute shell scripts:
18
+ // await exec\`git status\`
19
+
20
+ // use fetch() to make HTTP requests
21
+ // await fetch('https://reqres.in/api/users')
22
+
23
+ // add arguments and options to this function if needed
24
+ // export async function helloWorld(userName, opts = { force: false })
25
+ //
26
+ // bunosh hello:world 'bob' --force
27
+
28
+ // use ignoreFail(true) to prevent the command from stopping on error
29
+
30
+ yell('Heloo Bunosh!');
31
+ say('Edit me with bunosh edit');
32
+ }
33
+ `;
5
34
 
6
35
  export default function init() {
7
36
  if (!fs.existsSync(BUNOSHFILE)) {
8
- fs.writeFileSync(BUNOSHFILE, templateFile());
37
+ Bun.write(BUNOSHFILE, template);
9
38
  console.log(color.bold(`🎉 Bunosh ${BUNOSHFILE} file created`));
10
39
  console.log(' Edit it with "bunosh edit" command');
11
40
  console.log(' Or open it in your favorite editor');
package/src/program.js CHANGED
@@ -16,7 +16,7 @@ export default function bunosh(commands, source) {
16
16
  const internalCommands = [];
17
17
 
18
18
  program.configureHelp({
19
- commandDescription: _cmd => `${banner}\n Commands are loaded from ${color.bold(BUNOSHFILE)} as JS functions`,
19
+ commandDescription: _cmd => `${banner}\n Commands are loaded from exported functions in ${color.bold(BUNOSHFILE)}`,
20
20
  commandUsage: usg => 'bunosh <command> <args> [options]',
21
21
  showGlobalOptions: false,
22
22
  // visibleArguments: cmd => cmd.registeredArguments,
@@ -49,30 +49,50 @@ export default function bunosh(commands, source) {
49
49
  const fnBody = commands[fnName].toString();
50
50
 
51
51
  const ast = fetchFnAst();
52
-
53
52
  const args = parseArgs();
54
53
  const opts = parseOpts();
54
+
55
55
  const comment = comments[fnName];
56
56
 
57
- const command = program.command(prepareCommandName(fnName));
58
- command.hook('preAction', (thisCommand) => {
57
+ const commandName = prepareCommandName(fnName);
58
+
59
+ const command = program.command(commandName);
60
+ command.hook('preAction', (_thisCommand) => {
59
61
  process.env.BUNOSH_COMMAND_STARTED = true;
60
62
  })
61
63
 
62
- args.filter(a => !!a).forEach((arg) => {
63
- command.argument(`<${arg}>`);
64
+ let argsAndOptsDescription = [];
65
+
66
+ Object.entries(args).forEach(([arg, value]) => {
67
+ if (value === undefined) {
68
+ argsAndOptsDescription.push(`<${arg}>`);
69
+ return command.argument(`<${arg}>`);
70
+ }
71
+
72
+ if (value === null) {
73
+ argsAndOptsDescription.push(`[${arg}]`);
74
+ return command.argument(`[${arg}]`, '', null);
75
+ }
76
+
77
+ argsAndOptsDescription.push(`[${arg}=${value}]`);
78
+ command.argument(`[${arg}]`, ``, value);
64
79
  });
80
+
65
81
  Object.entries(opts).forEach(([opt, value]) => {
66
- if (value === false || value === null) command.option(`--${opt}`);
82
+ if (value === false || value === null) {
83
+ argsAndOptsDescription.push(`--${opt}`);
84
+ return command.option(`--${opt}`);
85
+ }
86
+
87
+ argsAndOptsDescription.push(`--${opt}=${value}`);
67
88
  command.option(`--${opt} [${opt}]`, "", value);
89
+
68
90
  });
69
91
 
70
- let description = comment || '';
71
-
72
- let argsAndOpts = '';
73
- if (args.length > 0) argsAndOpts += `${args.map(a => color.blueBright(`<${a}>`)).join(' ')}`
74
- if (Object.keys(opts).length > 0) argsAndOpts += ` ${Object.keys(opts).map(opt => color.blueBright(`--${opt}`)).join(' ')}`
75
- if (comment && argsAndOpts) description += `\n ${argsAndOpts.trim()}\n`;
92
+ let description = comment?.split('\n')[0] || '';
93
+
94
+
95
+ if (comment && argsAndOptsDescription.length) description += `\n ${color.gray(`bunosh ${commandName}`)} ${color.blue(argsAndOptsDescription.join(' ').trim())}`;
76
96
 
77
97
  command.description(description);
78
98
  command.action(commands[fnName].bind(commands));
@@ -97,7 +117,7 @@ export default function bunosh(commands, source) {
97
117
 
98
118
  // We parse command args from function args
99
119
  function parseArgs() {
100
- const functionArguments = [];
120
+ const functionArguments = {};
101
121
 
102
122
  traverse(ast, {
103
123
  FunctionDeclaration(path) {
@@ -107,13 +127,20 @@ export default function bunosh(commands, source) {
107
127
  .filter((node) => {
108
128
  return node?.right?.type !== "ObjectExpression";
109
129
  })
110
- .map((param) => param.name);
130
+ .forEach((param) => {
131
+ if (param.type === "AssignmentPattern") {
132
+ functionArguments[param.left.name] = param.right.value;
133
+ return;
134
+ }
135
+ if (!param.name) return;
136
+
137
+ return functionArguments[param.name] = null;
138
+ });
111
139
 
112
- functionArguments.push(params);
113
140
  },
114
141
  });
115
142
 
116
- return functionArguments.flat();
143
+ return functionArguments;
117
144
  }
118
145
 
119
146
  // We parse command options from the object of last function args
@@ -132,7 +159,8 @@ export default function bunosh(commands, source) {
132
159
  )
133
160
  return;
134
161
 
135
- node.right.properties.forEach((p) => {
162
+
163
+ node?.right?.properties?.forEach((p) => {
136
164
  if (
137
165
  ["NumericLiteral", "StringLiteral", "BooleanLiteral"].includes(
138
166
  p.value.type,
@@ -183,13 +211,10 @@ export default function bunosh(commands, source) {
183
211
  internalCommands.push(exoprtCmd);
184
212
 
185
213
  program.addHelpText('after', `
186
- Example call:
187
- bunosh ${program.commands.find(c => !internalCommands.includes(c)).name()} ${color.gray('<args>')} ${color.gray('[options]')}
188
214
 
189
215
  Special Commands:
190
216
  📝 Edit bunosh file: ${color.bold('bunosh edit')}
191
217
  📥 Export scripts to package.json: ${color.bold('bunosh export:scripts')}
192
-
193
218
  `);
194
219
 
195
220
 
@@ -220,6 +245,7 @@ Special Commands:
220
245
  if (matches && matches[1]) {
221
246
  comments[functionName] = matches[1]
222
247
  .replace(/^\s*\*\s*/gm, "") // remove * chars
248
+ .replace(/\s*\*\*\s*$/gm, "") // remove * chars
223
249
  .trim()
224
250
  .replace(/^@.*$/gm, "") // remove params from description
225
251
  .trim();
@@ -19,32 +19,49 @@ export default function exec(strings, ...values) {
19
19
  });
20
20
 
21
21
  let envs = null
22
- cmdPromise.env = (newEnvs) => envs = newEnvs;
22
+ cmdPromise.env = (newEnvs) => {
23
+ envs = newEnvs;
24
+ return cmdPromise;
25
+ }
23
26
 
24
27
  let cwd = null;
25
- cmdPromise.cwd = (newCwd) => cwd = newCwd;
26
-
27
- const ExecOutput = () => {
28
- let shell = $(strings, ...values);
29
- if (cwd) {
30
- shell = shell.cwd(cwd);
31
- }
32
- if (envs) {
33
- shell = shell.env(envs);
34
- }
28
+ cmdPromise.cwd = (newCwd) => {
29
+ cwd = newCwd;
30
+ return cmdPromise;
31
+ }
35
32
 
33
+ const ExecOutput = () => {
36
34
  const stackOutput = [];
37
-
35
+
38
36
  const [printedLines, setPrintedLines] = useState([]);
39
37
  const [exitCode, setExitCode] = useState(null);
40
-
38
+
41
39
  const readNextChunk = async () => {
40
+ let shell = $(strings, ...values);
41
+
42
+ if (cwd) {
43
+ shell = shell.cwd(cwd);
44
+ }
45
+ if (envs) {
46
+ shell = shell.env(envs);
47
+ }
48
+
49
+ // there should be API to read stderr line by line...
42
50
  for await (let line of shell.lines()) {
43
51
  debugTask(cmd, line);
44
52
  stackOutput.push(line);
45
53
  setPrintedLines(stackOutput.slice(-DEFAULT_OUTPUT_SIZE));
46
54
  }
47
- const code = parseInt((await shell).exitCode, 10);
55
+
56
+ const { exitCode, stderr } = await shell;
57
+
58
+ if (stderr) {
59
+ debugTask(cmd, stderr.toString());
60
+ stackOutput.push(stderr.toString())
61
+ setPrintedLines(stackOutput.slice(-DEFAULT_OUTPUT_SIZE));
62
+ }
63
+
64
+ const code = parseInt(exitCode, 10);
48
65
  setExitCode(code);
49
66
  if (code == 0) resolve(TaskResult.success(stackOutput.join('\n')));
50
67
  if (code !== 0) resolve(TaskResult.fail(stackOutput.join('\n')));
@@ -1,12 +1,8 @@
1
- import color from "picocolors";
1
+ import color from "chalk";
2
+ import cfonts from "cfonts";
2
3
 
3
4
  export default `
4
- ─╔══╗──╔╗─╔╗─╔═╗─╔╗─╔═══╗─╔═══╗─╔╗─╔╗
5
- ─║╔╗║──║║─║║─║║╚╗║║─║╔═╗║─║╔═╗║─║║─║║
6
- ─║╚╝╚╗─║║─║║─║╔╗╚╝║─║║─║║─║╚══╗─║╚═╝║
7
- ─║╔═╗║─║║─║║─║║╚╗║║─║║─║║─╚══╗║─║╔═╗║
8
- ─║╚═╝║─║╚═╝║─║║─║║║─║╚═╝║─║╚═╝║─║║─║║
9
- ─╚═══╝─╚═══╝─╚╝─╚═╝─╚═══╝─╚═══╝─╚╝─╚╝
5
+ ${cfonts.render('Bunosh', { font: 'pallet', gradient: ['blue','yellow'], colors: ['system'], space: false}).string}
10
6
 
11
- 🍲 ${color.bold(color.white('Bunosh'))} - your ${color.bold('exceptional')} task runner powered by Bun
7
+ 🍲 ${color.bold(color.dim.white('Bunosh'))} - your ${color.bold('exceptional')} task runner powered by Bun
12
8
  `.trim();
@@ -1,14 +1,14 @@
1
1
 
2
- export default () => `import { exec, fetch, ignoreFail } from "bunosh";
3
- import { say, yell } from "bunosh/io";
4
- import { writeToFile } from "bunosh/files";
2
+ import { exec, fetch, ignoreFail } from 'bunosh';
3
+ import { say, yell } from 'bunosh/io';
4
+ import { writeToFile } from 'bunosh/files';
5
5
 
6
6
  /**
7
7
  * 🎉 Hello world command
8
8
  */
9
9
  export async function helloWorld() {
10
10
  // use say() to print to the console
11
- // say("Hello World!");
11
+ // say('Hello World!');
12
12
 
13
13
  // use exec`` to execute shell scripts:
14
14
  // await exec\`git status\`
@@ -23,7 +23,6 @@ export async function helloWorld() {
23
23
 
24
24
  // use ignoreFail(true) to prevent the command from stopping on error
25
25
 
26
- yell("Heloo Bunosh!");
27
- say('Edit me with "bunosh edit"');
26
+ yell('Heloo Bunosh!');
27
+ say('Edit me with bunosh edit');
28
28
  }
29
- `;
package/bunosh DELETED
Binary file
package/bunosh.tasks.js DELETED
@@ -1,83 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { exec, fetch, task, ignoreFail } from "./index";
3
- import { ask, say, yell } from "./io";
4
- import fs from "fs";
5
- import { writeToFile } from "./files";
6
-
7
- /**
8
- * Builds binary file for Bunosh
9
- */
10
- export async function buildBinary() {
11
- await exec`bun build ./run.js --compile --outfile bunosh`;
12
- }
13
-
14
- /**
15
- * 🎉 Hello world
16
- * @param {*} arg1
17
- * @param {*} opts
18
- */
19
- export async function helloWorld(
20
- arg1,
21
- opts = { user: null, val: "ok", flag: false },
22
- ) {
23
- // console.log("Hello World!", arg1);
24
- yell("I need all git status");
25
- const pack = await task('read file', () => {
26
- return fs.readFileSync("package.json").toString();
27
- });
28
- say(pack);
29
- writeToFile('test.txt', l => {
30
- l`lock file`
31
- l('Hello world');
32
- l.fromFile('package.json');
33
- });
34
- yell('done');
35
- yell('ok');
36
- }
37
-
38
- /**
39
- * Hello other
40
- */
41
- export async function helloOther(opts = {
42
- user: null,
43
- val: "ok",
44
- flag: false
45
- }) {
46
-
47
- yell('running everything!')
48
-
49
- await fetch('https://reqres.in/api/users')
50
-
51
- await Promise.all([
52
- fetch('https://reqres.in/api/users/1'),
53
- exec`ps aux | grep redis`,
54
- fetch('https://reqres.in/api/users/2')
55
- ])
56
-
57
- ignoreFail(true);
58
-
59
- await exec`ps aux | grep node`,
60
- await exec`git status`.cwd('/home/davert/projects/codeceptjs');
61
- }
62
-
63
- /**
64
- * Deploys code to staging
65
- */
66
- export async function updateToProduction() {
67
-
68
- await Promise.all([
69
- exec`ps aux | grep redis`,
70
- exec`ps aux | grep clickhouse`,
71
- fetch('https://reqres.in/api/users/2'),
72
- ])
73
- }
74
-
75
- /**
76
- * Deploys code to production
77
- */
78
- export async function updateToStaging() {
79
- // this is not ok
80
- const env = await ask('Which environment we are in?');
81
- say(env);
82
- }
83
-
package/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 './src/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/test.txt DELETED
@@ -1,44 +0,0 @@
1
- lock file
2
- Hello world
3
- {
4
- "name": "bunosh",
5
- "module": "index.js",
6
- "bin": {
7
- "bunosh": "./run.js"
8
- },
9
- "devDependencies": {
10
- "@types/bun": "latest",
11
- "prettier": "^3.2.4",
12
- "lint-staged": "^15.2.0"
13
- },
14
- "peerDependencies": {
15
- "typescript": "^5.0.0"
16
- },
17
- "dependencies": {
18
- "@babel/parser": "^7.23.6",
19
- "@babel/traverse": "^7.23.7",
20
- "cfonts": "^3.2.0",
21
- "chalk": "^5.3.0",
22
- "commander": "^11.1.0",
23
- "debug": "^4.3.4",
24
- "fs-extra": "^11.2.0",
25
- "ink": "^4.4.1",
26
- "ink-big-text": "^2.0.0",
27
- "ink-gradient": "^3.0.0",
28
- "ink-spinner": "^5.0.0",
29
- "inquirer": "^9.2.13",
30
- "open-editor": "^4.1.1",
31
- "picocolors": "^1.0.0",
32
- "react": "^18.2.0",
33
- "timer-node": "^5.0.7"
34
- },
35
- "lint-staged": {
36
- "*.{js,css,md}": "prettier --write"
37
- },
38
- "scripts": {
39
- "hello:other": "bunosh hello:other",
40
- "hello:world": "bunosh hello:world",
41
- "update:to-production": "bunosh update:to-production",
42
- "update:to-staging": "bunosh update:to-staging"
43
- }
44
- }