@gutenye/script.js 2.3.0 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gutenye/script.js",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Write shell scripts in JavaScript",
5
5
  "keywords": [
6
6
  "shell",
@@ -26,7 +26,6 @@
26
26
  ],
27
27
  "bin": {
28
28
  "script.js": "src/script.ts",
29
- "a": "src/ake.ts",
30
29
  "ake": "src/ake.ts",
31
30
  "akectl": "src/akectl.ts"
32
31
  },
@@ -39,6 +38,7 @@
39
38
  "access": "public"
40
39
  },
41
40
  "dependencies": {
41
+ "@gutenye/script.js": "^2.3.0",
42
42
  "yaml": "^2.6.0"
43
43
  },
44
44
  "peerDependencies": {
package/src/ake/README.md CHANGED
@@ -45,7 +45,80 @@ ake <Tab> # uses ake file's completion
45
45
  Create `~/bin.src/ake/template`
46
46
 
47
47
  ```sh
48
- akectl init local # create a ake file from template in currenct directory
49
- akectl init remote # create in ~/bin.src/ake/<dir>, doesn't touch original project files
50
- akectl edit # Opens a editor to edit the ake file
48
+ akectl init local # create an ake file from template in current directory
49
+ akectl init local foo # create an akefoo file
50
+ akectl init remote # create in ~/bin.src/ake/<dir>, doesn't touch original project files
51
+ akectl init remote foo # create akefoo in remote location
52
+ akectl edit # opens an editor to edit the ake file
53
+ akectl edit foo # opens an editor to edit the akefoo file
54
+ ```
55
+
56
+ ## Organize Multiple Files
57
+
58
+ For larger projects, split commands into separate files and import them.
59
+
60
+ ```
61
+ project/
62
+ ├── ake # entry point (executable)
63
+ ├── src/
64
+ │ ├── index.ts # imports all command files
65
+ │ ├── cmd1.ts
66
+ │ └── cmd2.ts
67
+ ```
68
+
69
+ `src/cmd1.ts`
70
+
71
+ ```ts
72
+ import { app } from "@gutenye/script.js";
73
+
74
+ app.cmd("greetings").add(() => {
75
+ $`echo greetings`;
76
+ });
77
+ ```
78
+
79
+ `src/index.ts`
80
+
81
+ ```ts
82
+ import "./cmd1";
83
+ import "./cmd2";
84
+ ```
85
+
86
+ `ake`
87
+
88
+ ```ts
89
+ #!/usr/bin/env bun
90
+
91
+ import { app } from "@gutenye/script.js";
92
+ import "./src";
93
+
94
+ await app.run();
95
+ ```
96
+
97
+ ## Multiple Ake Files
98
+
99
+ Any file named `ake<suffix>` or `ake<suffix>.ts` is supported.
100
+
101
+ 1. Create a variant ake file
102
+
103
+ ```sh
104
+ akectl init local foo # creates ./akefoo
105
+ ```
106
+
107
+ 2. Create a wrapper script so you can invoke it by name
108
+
109
+ ```sh
110
+ akectl install-bin ~/bin/ake foo
111
+ ```
112
+
113
+ 3. Enable shell completion in `ake.fish`
114
+
115
+ ```fish
116
+ _setup_ake_complete ake foo
117
+ ```
118
+
119
+ 4. Run it
120
+
121
+ ```sh
122
+ akefoo greetings
123
+ akefoo <Tab> # with autocompletion
51
124
  ```
package/src/ake/ake.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { exitWithError, findAkeFiles } from './shared'
3
+ import { exitWithError, findAkeFiles, getSuffix } from './shared'
4
+
5
+ const suffix = process.env.SUFFIX ?? getSuffix()
4
6
 
5
7
  async function main() {
6
8
  const akeFile = await findAkeFile()
@@ -8,7 +10,7 @@ async function main() {
8
10
  }
9
11
 
10
12
  async function findAkeFile() {
11
- const akeFiles = await findAkeFiles()
13
+ const akeFiles = await findAkeFiles(suffix)
12
14
 
13
15
  if (akeFiles.length >= 2) {
14
16
  exitWithError(
@@ -20,9 +22,10 @@ async function findAkeFile() {
20
22
  const akeFile = akeFiles[0]
21
23
 
22
24
  if (!akeFile) {
25
+ const name = `ake${suffix}`
23
26
  exitWithError(
24
- 'ake file not found',
25
- 'Use below commands to create one:\nakectl init local\nakectl init remote',
27
+ `${name} file not found`,
28
+ `Use below commands to create one:\nakectl init local${suffix ? ` ${suffix}` : ''}\nakectl init remote${suffix ? ` ${suffix}` : ''}`,
26
29
  )
27
30
  }
28
31
 
package/src/ake/akectl.ts CHANGED
@@ -3,12 +3,12 @@
3
3
  import { castArray } from 'lodash-es'
4
4
  import fs from '../utils/fs'
5
5
  import {
6
- AKE_FILENAMES,
7
- STORAGE_DIR,
8
- TEMPLATE_NAME,
9
6
  exitWithError,
10
7
  findAkeFiles,
8
+ getAkeFilenames,
11
9
  getRemoteDir,
10
+ STORAGE_DIR,
11
+ TEMPLATE_NAME,
12
12
  } from './shared'
13
13
 
14
14
  const NAME = 'akectl'
@@ -19,16 +19,19 @@ app.meta(NAME)
19
19
  app
20
20
  .cmd('init', 'Create ake file')
21
21
  .add('<place>', 'Place', ['local', 'remote'])
22
- .add(async (place: string) => {
23
- const akeFiles = await findAkeFiles()
22
+ .add('[suffix]', 'Ake file suffix (e.g. "foo" for akefoo)')
23
+ .add(async (place: string, suffix: string) => {
24
+ suffix = suffix ?? ''
25
+ const akeFiles = await findAkeFiles(suffix)
24
26
  if (akeFiles.length > 0) {
25
27
  exitWithError('Already have an ake file, cannot create a new one')
26
28
  }
27
- let target = AKE_FILENAMES[0]
29
+ const filenames = getAkeFilenames(suffix)
30
+ let target = filenames[0]
28
31
  if (place === 'remote') {
29
32
  const remoteDir = getRemoteDir()
30
33
  await fs.mkdirp(remoteDir)
31
- target = `${remoteDir}/${AKE_FILENAMES[0]}`
34
+ target = `${remoteDir}/${filenames[0]}`
32
35
  }
33
36
  const templateFile = `${STORAGE_DIR}/${TEMPLATE_NAME}`
34
37
  if (await fs.pathExists(templateFile)) {
@@ -40,14 +43,35 @@ app
40
43
  await openEditor(target)
41
44
  })
42
45
 
43
- app.cmd('edit', 'Edit ake file').add(async () => {
44
- const akeFiles = await findAkeFiles()
45
- const akeFile = akeFiles[0]
46
- if (!akeFile) {
47
- exitWithError('No ake file found')
48
- }
49
- await openEditor(akeFile)
50
- })
46
+ app
47
+ .cmd('install-bin', 'Create a wrapper script for ake with a suffix')
48
+ .add('<prefix>', 'Path prefix (e.g. ~/bin/ake, ~/bin/a)')
49
+ .add('<suffix>', 'Suffix to append (e.g. "foo" → ~/bin/akefoo)')
50
+ .add(async (prefix: string, suffix: string) => {
51
+ const target = `${prefix}${suffix}`
52
+ const content = `
53
+ #!/usr/bin/env bash
54
+
55
+ SUFFIX=${suffix} exec ${prefix} "$@"
56
+ `.trim()
57
+ await fs.writeFile(target, content)
58
+ await fs.chmod(target, 0o755)
59
+ console.log(`created ${target}`)
60
+ })
61
+
62
+ app
63
+ .cmd('edit', 'Edit ake file')
64
+ .add('[suffix]', 'Ake file suffix (e.g. "foo" for akefoo)')
65
+ .add(async (suffix: string) => {
66
+ suffix = suffix ?? ''
67
+ const akeFiles = await findAkeFiles(suffix)
68
+ const akeFile = akeFiles[0]
69
+ if (!akeFile) {
70
+ const name = `ake${suffix}`
71
+ exitWithError(`${name} file not found`)
72
+ }
73
+ await openEditor(akeFile)
74
+ })
51
75
 
52
76
  async function openEditor(inputPaths: string | string[]) {
53
77
  const paths = castArray(inputPaths)
@@ -1,8 +1,17 @@
1
- complete --erase a
2
- complete --command a --no-files --arguments '(_ake_complete)'
1
+ function _setup_ake_complete
2
+ set cmd $argv[1]
3
+ set suffix $argv[2]
4
+ complete --erase $cmd$suffix
5
+ complete --command $cmd$suffix --no-files --arguments "(_ake_complete $suffix)"
6
+ end
3
7
 
4
8
  function _ake_complete
5
- set unique_name (string replace --all '/' '_' (realpath $PWD))
6
- set name "ake.$unique_name"
9
+ set suffix $argv[1]
10
+ set unique_name (string replace --all '/' '_' $PWD)
11
+ set name "ake$suffix.$unique_name"
7
12
  _carapace_completer $name
8
13
  end
14
+
15
+ _setup_ake_complete ake
16
+ # _setup_ake_complete ake <suffix>
17
+
package/src/ake/shared.ts CHANGED
@@ -1,22 +1,38 @@
1
- import nodeFs from 'node:fs'
1
+ import fsSync from 'node:fs'
2
+ import path from 'node:path'
2
3
  import os from 'node:os'
3
4
  import fs from '../utils/fs'
4
5
 
5
6
  const HOME = os.homedir()
6
7
  const CWD = process.cwd()
7
8
 
8
- export const AKE_FILENAMES = ['ake', 'ake.ts']
9
9
  export const STORAGE_DIR = `${HOME}/bin.src/ake`
10
10
  export const TEMPLATE_NAME = 'template'
11
11
 
12
- export async function findAkeFiles(): Promise<string[]> {
12
+ export function getAkeFilenames(suffix = ''): string[] {
13
+ const name = `ake${suffix}`
14
+ return [name, `${name}.ts`]
15
+ }
16
+
17
+ export function getAkeSuffix(name: string): string | null {
18
+ const base = name.replace(/\.ts$/, '')
19
+ if (!base.startsWith('ake') || base === 'akectl') return null
20
+ return base.slice(3)
21
+ }
22
+
23
+ export function getSuffix(): string {
24
+ return getAkeSuffix(path.basename(process.argv[1])) ?? ''
25
+ }
26
+
27
+ export async function findAkeFiles(suffix = ''): Promise<string[]> {
28
+ const filenames = getAkeFilenames(suffix)
13
29
  const localDir = CWD
14
30
  const remoteDir = getRemoteDir()
15
31
  const dirsToCheck = [localDir, remoteDir]
16
32
 
17
33
  const akeFiles = await Promise.all(
18
34
  dirsToCheck.flatMap((dir) =>
19
- AKE_FILENAMES.map(async (name) => {
35
+ filenames.map(async (name) => {
20
36
  const akeFile = `${dir}/${name}`
21
37
  return (await fs.pathExists(akeFile)) ? akeFile : null
22
38
  }),
@@ -30,13 +46,13 @@ export function getRemoteDir() {
30
46
  return `${STORAGE_DIR}/${getUniqueName()}`
31
47
  }
32
48
 
33
- export function getCompletionName() {
34
- return `ake.${getUniqueName()}`
49
+ export function getCompletionName(suffix = '') {
50
+ return `ake${suffix}.${getUniqueName()}`
35
51
  }
36
52
 
37
53
  export function getUniqueName() {
38
54
  // use sync method to avoid using await in app.enableAkeCompletion()
39
- return nodeFs.realpathSync(CWD).replaceAll('/', '_')
55
+ return fsSync.realpathSync(CWD).replaceAll('/', '_')
40
56
  }
41
57
 
42
58
  export function exitWithError(message: string, help?: string): never {
package/src/completion.ts CHANGED
@@ -1,9 +1,9 @@
1
- import nodeFs from 'node:fs'
1
+ import fsSync from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import * as yaml from 'yaml'
5
5
  import type { Command } from './Command'
6
- import { AKE_FILENAMES, getCompletionName } from './ake/shared'
6
+ import { getAkeSuffix, getCompletionName } from './ake/shared'
7
7
 
8
8
  export type CompletionValue = string[] | (() => string[])
9
9
 
@@ -125,9 +125,9 @@ export async function installCompletion(
125
125
  ) {
126
126
  try {
127
127
  if (!command.name && options.scriptPath) {
128
- const basename = path.basename(options.scriptPath)
129
- if (AKE_FILENAMES.includes(basename)) {
130
- command.name = getCompletionName()
128
+ const suffix = getAkeSuffix(path.basename(options.scriptPath))
129
+ if (suffix !== null) {
130
+ command.name = getCompletionName(suffix)
131
131
  }
132
132
  }
133
133
 
@@ -139,13 +139,13 @@ export async function installCompletion(
139
139
 
140
140
  let existing: string | undefined
141
141
  try {
142
- existing = nodeFs.readFileSync(filePath, 'utf8')
142
+ existing = fsSync.readFileSync(filePath, 'utf8')
143
143
  } catch {}
144
144
 
145
145
  if (existing === result.text) return
146
146
 
147
- nodeFs.mkdirSync(specsDir, { recursive: true })
148
- nodeFs.writeFileSync(filePath, result.text)
147
+ fsSync.mkdirSync(specsDir, { recursive: true })
148
+ fsSync.writeFileSync(filePath, result.text)
149
149
  } catch {
150
150
  // completion is supplementary — silently ignore errors
151
151
  }