@gutenye/script.js 1.0.0

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 ADDED
@@ -0,0 +1,35 @@
1
+ # 🌟 Script.js 🌟
2
+
3
+ [![Stars](https://img.shields.io/github/stars/gutenye/script.js?style=social)](https://github.com/gutenye/script.js) [![NPM Version](https://img.shields.io/npm/v/@gutenye/script.js)](https://www.npmjs.com/package/@gutenye/script.js) [![License](https://img.shields.io/github/license/gutenye/script.js?color=blue)](https://github.com/gutenye/script.js/blob/main/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-blue)](https://github.com/gutenye/script.js#-contribute)
4
+
5
+ Write shell scripts in JavaScript
6
+
7
+ **Show your ❤️ and support by starring this project and following the author, [Guten Ye](https://github.com/gutenye)!**
8
+
9
+ ## 🌟 Features
10
+
11
+ - **Javascript**: ..
12
+ - **Autocomplete**: ..
13
+ - **Fast**: ..
14
+
15
+ ## 📖 Documentation
16
+
17
+ - [Getting Started](./docs/Getting%20Started.md)
18
+ - [Completion](./docs/Completion.md)
19
+
20
+ ## 🤝 Contribute
21
+
22
+ We welcome contributions from the community! Whether it’s reporting bugs, suggesting features, or submitting pull requests, your help is appreciated.
23
+
24
+ 1. Fork the Repository
25
+ 2. Open a Pull Request on Github
26
+
27
+ ---
28
+
29
+ Thank you for using Script.js! 🔐 ✨ If you found it helpful, please ⭐️ star the project ️️⭐ on GitHub. If you have any questions or encounter issues, please refer to the documentation or report an issue on GitHub.
30
+
31
+ **Thanks to all the people who contribute:**
32
+
33
+ [![](https://contrib.rocks/image?repo=gutenye/script.js)](https://github.com/gutenye/script.js/graphs/contributors)
34
+
35
+ [⬆ Back to top ⬆](#readme)
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@gutenye/script.js",
3
+ "version": "1.0.0",
4
+ "description": "Write shell scripts in JavaScript",
5
+ "keywords": [
6
+ "shell",
7
+ "script",
8
+ "bash",
9
+ "bin",
10
+ "binary",
11
+ "child",
12
+ "process",
13
+ "exec",
14
+ "execute",
15
+ "invoke",
16
+ "call",
17
+ "spawn",
18
+ "zx",
19
+ "bunshell"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": "github:gutenye/script.js",
23
+ "type": "module",
24
+ "files": [
25
+ "src",
26
+ "tsconfig.json",
27
+ "build",
28
+ "!**/__tests__"
29
+ ],
30
+ "bin": {
31
+ "gutenye-script.js": "src/script.ts"
32
+ },
33
+ "scripts": {
34
+ "test": "bun test",
35
+ "lint": "biome check --fix",
36
+ "lint:ci": "biome ci --reporter=github",
37
+ "build": "rm -rf build; tsc --project tsconfig.build.json; tsc-alias --project tsconfig.build.json"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@gutenye/commander-completion-carapace": "^1.0.3",
44
+ "chalk": "^5.3.0",
45
+ "commander": "^12.1.0",
46
+ "csv-parse": "^5.5.6",
47
+ "lodash-es": "^4.17.21",
48
+ "table": "^6.8.2",
49
+ "tiny-invariant": "^1.3.3",
50
+ "yaml": "^2.6.0",
51
+ "zx": "^8.2.1"
52
+ },
53
+ "peerDependencies": {
54
+ "typescript": "^5.7.2"
55
+ },
56
+ "devDependencies": {
57
+ "@biomejs/biome": "1.9.4",
58
+ "@semantic-release/changelog": "^6.0.3",
59
+ "@semantic-release/git": "^10.0.1",
60
+ "@types/bun": "latest",
61
+ "@types/lodash-es": "^4.17.12",
62
+ "conventional-changelog-conventionalcommits": "^8.0.0",
63
+ "tsc-alias": "^1.8.10"
64
+ }
65
+ }
package/src/app.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Argument, Option, program } from './command'
2
+
3
+ const [bunPath, scriptJsPath, scriptPath, ...args] = process.argv
4
+
5
+ export const app = program
6
+
7
+ app.Argument = Argument
8
+ app.Option = Option
9
+
10
+ export async function start() {
11
+ if (!scriptPath) {
12
+ echo('Error: missing script path, usage: gutenye-script.js <script>')
13
+ process.exit(1)
14
+ }
15
+
16
+ await import(scriptPath)
17
+
18
+ const promise = program.installCompletion()
19
+ if (args.length === 0) {
20
+ // wait for file write operation completed before app print hellp and exit in parse()
21
+ await promise
22
+ }
23
+
24
+ program.parse(args, { from: 'user' })
25
+ }
package/src/command.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { Command as BaseCommand } from '@gutenye/commander-completion-carapace'
2
+
3
+ export * from '@gutenye/commander-completion-carapace'
4
+
5
+ export class Command extends BaseCommand {
6
+ createCommand(name) {
7
+ return new Command(name)
8
+ }
9
+
10
+ async invoke(text, ...args) {
11
+ if (args.length === 0) {
12
+ const args = text.split(/ +/)
13
+ return program.parseAsync(args, { from: 'user' })
14
+ } else {
15
+ return program._findCommand(text)._actionHandler(...args)
16
+ }
17
+ }
18
+
19
+ actionLinux(fn) {
20
+ this._actionLinux = fn
21
+ this._actionOS()
22
+ return this
23
+ }
24
+
25
+ actionMac(fn) {
26
+ this._actionMac = fn
27
+ this._actionOS()
28
+ return this
29
+ }
30
+
31
+ actionWin(fn) {
32
+ this._actionWin = fn
33
+ this._actionOS()
34
+ return this
35
+ }
36
+
37
+ _actionOS(fn) {
38
+ this.action((...args) => {
39
+ switch (process.platform) {
40
+ case 'linux':
41
+ return this._actionLinux?.(...args)
42
+ case 'darwin':
43
+ return this._actionMac?.(...args)
44
+ case 'win32':
45
+ return this._actionWin?.(...args)
46
+ }
47
+ })
48
+ }
49
+ }
50
+
51
+ export const program = new Command()
package/src/csv.ts ADDED
@@ -0,0 +1,12 @@
1
+ import * as csv from 'csv-parse/sync'
2
+
3
+ // csv.parse(text, { delimiter: ','*, columns: true* })
4
+ // columns: true -> [{column: value}]
5
+ // columns: false -> [[column], [value]]
6
+ export function parse(text, options = {}) {
7
+ const newOptions = {
8
+ columns: true,
9
+ ...options,
10
+ }
11
+ return csv.parse(text, newOptions)
12
+ }
package/src/exit.ts ADDED
@@ -0,0 +1,10 @@
1
+ // exitWithError(message, [more])
2
+ export function exitWithError(message, more) {
3
+ // for comletion <Tab> display error message: 1) no prefix: '\n, it uses first line 2) use console.error
4
+ let text = colors.red.bold(`Error: ${message}`)
5
+ if (more) {
6
+ text += `\n${more}`
7
+ }
8
+ console.error(text)
9
+ process.exit(1)
10
+ }
@@ -0,0 +1,17 @@
1
+ import { fs } from '#/utils'
2
+
3
+ export { fs }
4
+
5
+ export const cp = fs.copy
6
+
7
+ export const mv = fs.move
8
+
9
+ export const rm = fs.remove
10
+
11
+ export const mkdir = fs.mkdirp
12
+
13
+ export const ls = glob
14
+
15
+ export const ln = fs.ln
16
+
17
+ export const lns = fs.symlink
package/src/mixins.ts ADDED
@@ -0,0 +1,19 @@
1
+ export async function mixins(...names) {
2
+ const errorNames = []
3
+ for (const name of names) {
4
+ try {
5
+ // TODO: use env to config
6
+ await import(`${process.env.HOME}/bin.src/mixins/${name}`)
7
+ } catch (error) {
8
+ if (error.message.match(/Cannot find module/)) {
9
+ errorNames.push(name)
10
+ } else {
11
+ throw error
12
+ }
13
+ }
14
+ }
15
+ if (errorNames.length > 0) {
16
+ console.error(`Error: [mixins] not found: ${errorNames.join(', ')}`)
17
+ process.exit(1)
18
+ }
19
+ }
package/src/script.ts ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import 'zx/globals'
4
+
5
+ // Variables
6
+ globalThis.HOME = os.homedir()
7
+ globalThis.PWD = process.cwd()
8
+ globalThis.ENV = process.env
9
+
10
+ // App
11
+ import { app, start } from './app'
12
+ globalThis.app = app
13
+
14
+ // Mixins
15
+ import { mixins } from './mixins'
16
+ globalThis.mixins = mixins
17
+
18
+ // Spawn
19
+ import { $, $l, $t } from './spawn'
20
+ globalThis.$ = $
21
+ globalThis.$t = $t
22
+ globalThis.$l = $l
23
+
24
+ // Error
25
+ import { exitWithError } from './exit'
26
+ globalThis.exitWithError = exitWithError
27
+
28
+ globalThis.mixins = mixins
29
+
30
+ // Filesystem
31
+ import { fs, cp, ls, mkdir, mv, rm } from './fileSystem'
32
+ globalThis.fs = fs
33
+ globalThis.cp = cp
34
+ globalThis.mv = mv
35
+ globalThis.rm = rm
36
+ globalThis.mkdir = mkdir
37
+ globalThis.ls = ls
38
+
39
+ // Lodash
40
+ import _ from 'lodash-es'
41
+ globalThis._ = _
42
+
43
+ // UI
44
+ import * as ui from './ui'
45
+ globalThis.ui = ui
46
+ import colors from 'chalk'
47
+ globalThis.colors = colors
48
+
49
+ // Csv
50
+ import * as csv from './csv'
51
+ globalThis.csv = csv
52
+
53
+ start()
package/src/spawn.ts ADDED
@@ -0,0 +1,23 @@
1
+ import * as zx from 'zx'
2
+
3
+ // wait for release fix: handle nullable stdout/stderr https://github.com/google/zx/commits/main/
4
+ // export const $ = zx.$.sync({ stdio: 'inherit' })
5
+ export const $ = zx.$({ stdio: 'inherit' })
6
+
7
+ // Returns text
8
+ export function $t(...args) {
9
+ const result = zx.$.sync(...args).text()
10
+ return result.trim()
11
+ }
12
+
13
+ // Returns lines
14
+ export function $l(...args) {
15
+ const lines = zx.$.sync(...args).lines()
16
+ // fix [''] issue
17
+ if (lines.length === 1 && lines[0] === '') {
18
+ return []
19
+ } else {
20
+ // fix ' a\n b\n' with space issue
21
+ return lines.map((v) => v.trim())
22
+ }
23
+ }
@@ -0,0 +1,8 @@
1
+ import 'zx/globals'
2
+ import type { Command } from '@gutenye/commander-completion-carapace'
3
+
4
+ declare global {
5
+ interface GlobalThis {
6
+ app: Command
7
+ }
8
+ }
@@ -0,0 +1 @@
1
+ export * from './table'
@@ -0,0 +1,35 @@
1
+ import { getBorderCharacters, table as rawTable } from 'table'
2
+ import invariant from 'tiny-invariant'
3
+
4
+ // Display table
5
+ // data: [{a: '1'}] | [['a'], ['1'], ..]
6
+ export function table(data) {
7
+ invariant(_.isArray(data), 'data should be array')
8
+ let columns = []
9
+ let rows = []
10
+ if (_.isPlainObject(data[0])) {
11
+ columns = Object.keys(data[0])
12
+ rows = data.map((v) => Object.values(v))
13
+ } else if (_.isArray(data[0])) {
14
+ columns = data[0]
15
+ rows = data.slice(1)
16
+ } else {
17
+ throw new Error('data should be array of object or array')
18
+ }
19
+ const newData = [data[0], ...data.slice(1)]
20
+ const tableText = rawTable(
21
+ [columns.map((v) => colors.green.bold(v)), ...rows],
22
+ {
23
+ drawHorizontalLine: (lineIndex, rowCount) =>
24
+ [0, 1, rowCount].includes(lineIndex),
25
+ border: {
26
+ ...getBorderCharacters('norc'),
27
+ topLeft: '╭',
28
+ topRight: '╮',
29
+ bottomLeft: '╰',
30
+ bottomRight: '╯',
31
+ },
32
+ },
33
+ )
34
+ echo(tableText)
35
+ }
@@ -0,0 +1,226 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import nodePath from 'node:path'
4
+
5
+ /**
6
+ * Check path exists
7
+ */
8
+ async function pathExists(path: string) {
9
+ try {
10
+ await fs.access(cleanPath(path))
11
+ return true
12
+ } catch (error) {
13
+ if (isNodeError(error) && error.code === 'ENOENT') {
14
+ return false
15
+ }
16
+ throw error
17
+ }
18
+ }
19
+
20
+ /**
21
+ * - file not exists: returns undefined
22
+ */
23
+ export async function inputFile(
24
+ path: ReadFileArgs[0],
25
+ options?: ReadFileArgs[1],
26
+ ) {
27
+ try {
28
+ return await fs.readFile(cleanPath(path), options)
29
+ } catch (error) {
30
+ if (isNodeError(error) && error.code === 'ENOENT') {
31
+ return
32
+ }
33
+ throw error
34
+ }
35
+ }
36
+
37
+ /**
38
+ * - Auto create missing dirs
39
+ */
40
+ async function outputFile(
41
+ rawPath: WriteFileArgs[0],
42
+ data: WriteFileArgs[1],
43
+ options?: WriteFileArgs[2],
44
+ ) {
45
+ const path = cleanPath(rawPath)
46
+ if (typeof path === 'string') {
47
+ const dir = nodePath.dirname(path)
48
+ await fs.mkdir(dir, { recursive: true })
49
+ }
50
+ return fs.writeFile(path, data, options)
51
+ }
52
+
53
+ // TODO
54
+ // emptyDir: readdirSync(dir).forEach(v => fs.rmSync(`${dir}/${v}`, { recursive: true })
55
+
56
+ /**
57
+ * Walk dir
58
+ */
59
+
60
+ async function* walk(rawDir: string): AsyncGenerator<string> {
61
+ const dir = cleanPath(rawDir)
62
+ for await (const d of await fs.opendir(dir)) {
63
+ const entry = nodePath.join(dir, d.name)
64
+ if (d.isDirectory()) yield* walk(entry)
65
+ else if (d.isFile()) yield entry
66
+ }
67
+ }
68
+
69
+ /**
70
+ * - uses inputFile
71
+ */
72
+ async function inputJson(input: ReadFileArgs[0], options?: ReadFileArgs[1]) {
73
+ const text = await inputFile(cleanPath(input), options)
74
+ if (!text) {
75
+ return
76
+ }
77
+ try {
78
+ return JSON.parse(text)
79
+ } catch (error) {
80
+ if (error instanceof Error) {
81
+ throw new Error(`[inputJson] ${error.message} from '${input}'`)
82
+ }
83
+ throw error
84
+ }
85
+ }
86
+
87
+ /*
88
+ * - uses readFile
89
+ */
90
+ async function readJson(input: ReadFileArgs[0], options?: ReadFileArgs[1]) {
91
+ const text = await fs.readFile(cleanPath(input), options)
92
+ try {
93
+ return JSON.parse(text)
94
+ } catch (error) {
95
+ if (error instanceof Error) {
96
+ throw new Error(`[readJson] ${error.message} from '${input}'`)
97
+ }
98
+ throw error
99
+ }
100
+ }
101
+
102
+ /**
103
+ * isSymlink
104
+ */
105
+ async function isSymlink(input: PathLike) {
106
+ const stat = await lstatSafe(input)
107
+ return stat ? stat.isSymbolicLink() : false
108
+ }
109
+
110
+ /**
111
+ * isFile
112
+ */
113
+ async function isFile(input: PathLike) {
114
+ const stat = await lstatSafe(input)
115
+ return stat ? stat.isFile() : false
116
+ }
117
+
118
+ /**
119
+ * isDir
120
+ */
121
+ async function isDir(input: PathLike) {
122
+ const stat = await lstatSafe(input)
123
+ return stat ? stat.isDirectory() : false
124
+ }
125
+
126
+ /**
127
+ * TS check if error is a Node Error
128
+ */
129
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
130
+ return error instanceof Error && 'code' in error
131
+ }
132
+
133
+ // ignore ENOENT
134
+ async function lstatSafe(input: PathLike) {
135
+ try {
136
+ return await fs.lstat(cleanPath(input))
137
+ } catch (error) {
138
+ if (isNodeError(error) && error.code === 'ENOENT') {
139
+ return
140
+ }
141
+ throw error
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Expands a file path that starts with '~' to the absolute path of the home directory.
147
+ * @param {string} path - The file path to expand.
148
+ * @returns {string} - The expanded absolute file path.
149
+ */
150
+ export function expand(path: any) {
151
+ if (!path || typeof path !== 'string') {
152
+ return path
153
+ }
154
+ const home = os.homedir()
155
+ if (path === '~') {
156
+ return home
157
+ }
158
+ if (path.startsWith('~/') || path.startsWith('~\\')) {
159
+ return nodePath.join(os.homedir(), path.slice(2))
160
+ }
161
+ return path
162
+ }
163
+
164
+ export function removeTrailingSlash(path: any) {
165
+ if (!path || typeof path !== 'string') {
166
+ return path
167
+ }
168
+ return path.replace(/[\\/]+$/, '')
169
+ }
170
+
171
+ export function cleanPath(path: any) {
172
+ return removeTrailingSlash(expand(path))
173
+ }
174
+
175
+ async function remove(path: PathLike) {
176
+ return fs.rm(cleanPath(path), { recursive: true, force: true })
177
+ }
178
+
179
+ async function copy(source: CpArgs[0], destination: CpArgs[1]) {
180
+ return fs.cp(source, destination, { recursive: true })
181
+ }
182
+
183
+ async function move(rawSrc: PathLike, rawDest: PathLike) {
184
+ const src = cleanPath(rawSrc)
185
+ const dest = cleanPath(rawDest)
186
+ await makeMissingDirs(dest)
187
+ return fs.rename(src, dest)
188
+ }
189
+
190
+ async function makeMissingDirs(rawPath: PathLike) {
191
+ if (typeof rawPath !== 'string') {
192
+ return
193
+ }
194
+ const path = cleanPath(rawPath)
195
+ const parent = nodePath.dirname(path)
196
+ return mkdirp(parent)
197
+ }
198
+
199
+ async function mkdirp(path: PathLike) {
200
+ return fs.mkdir(path, { recursive: true })
201
+ }
202
+
203
+ type WriteFileArgs = Parameters<typeof fs.writeFile>
204
+ type ReadFileArgs = Parameters<typeof fs.readFile>
205
+ type PathLike = Parameters<typeof fs.lstat>[0]
206
+ type CpArgs = Parameters<typeof fs.cp>
207
+
208
+ export default {
209
+ ...fs,
210
+ pathExists,
211
+ expand,
212
+ cleanPath,
213
+ inputFile,
214
+ outputFile,
215
+ isNodeError,
216
+ readJson,
217
+ inputJson,
218
+ walk,
219
+ isFile,
220
+ isDir,
221
+ isSymlink,
222
+ remove,
223
+ copy,
224
+ move,
225
+ mkdirp,
226
+ }
@@ -0,0 +1 @@
1
+ export { default as fs } from './fs'
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Import path alias
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "#/*": ["src/*"],
7
+ "#root/*": ["./*"]
8
+ },
9
+
10
+ // Enable latest features
11
+ "lib": ["ESNext", "DOM"],
12
+ "target": "ESNext",
13
+ "module": "ESNext",
14
+ "moduleDetection": "force",
15
+ "jsx": "react-jsx",
16
+ "allowJs": true,
17
+
18
+ // Bundler mode
19
+ "moduleResolution": "bundler",
20
+ "allowImportingTsExtensions": true,
21
+ "verbatimModuleSyntax": true,
22
+ "noEmit": true,
23
+
24
+ // Best practices
25
+ "strict": true,
26
+ "skipLibCheck": true,
27
+ "noFallthroughCasesInSwitch": true,
28
+
29
+ // Some stricter flags (disabled by default)
30
+ "noUnusedLocals": false,
31
+ "noUnusedParameters": false,
32
+ "noPropertyAccessFromIndexSignature": false
33
+ }
34
+ }