@gunshi/plugin-completion 0.31.0 → 0.32.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.
Files changed (3) hide show
  1. package/README.md +3 -9
  2. package/lib/index.js +122 -15
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -13,7 +13,7 @@ This completion plugin is powered by [`@bomb.sh/tab`](https://github.com/bombshe
13
13
  <!-- eslint-disable markdown/no-missing-label-refs -->
14
14
 
15
15
  > [!WARNING]
16
- > This package support Node.js runtime only. Deno and Bun support are coming soon.
16
+ > This package supports Node.js and Bun runtimes only. Deno support is not available yet.
17
17
 
18
18
  <!-- eslint-enable markdown/no-missing-label-refs -->
19
19
 
@@ -122,14 +122,8 @@ This section provides detailed instructions for setting up shell completions in
122
122
 
123
123
  ### Prerequisites
124
124
 
125
- Shell completion requires Node.js runtime. Ensure your CLI is running with Node.js (not Deno or Bun).
126
-
127
- <!-- eslint-disable markdown/no-missing-label-refs -->
128
-
129
- > [!WARNING]
130
- > This package support Node.js runtime only. Deno and Bun support are coming soon.
131
-
132
- <!-- eslint-enable markdown/no-missing-label-refs -->
125
+ Shell completion requires your CLI command to be executable from the generated completion script.
126
+ The plugin currently supports completion script generation for Node.js, Bun source execution, and Bun single-file executables.
133
127
 
134
128
  ### Setup by Shell
135
129
 
package/lib/index.js CHANGED
@@ -266,14 +266,126 @@ const pluginId = namespacedId("completion");
266
266
  * @author kazuya kawaguchi (a.k.a. kazupon)
267
267
  * @license MIT
268
268
  */
269
+ function getRuntimeGlobals() {
270
+ return globalThis;
271
+ }
272
+ /**
273
+ * Detect the active JavaScript runtime.
274
+ *
275
+ * @returns The detected runtime name
276
+ */
269
277
  function detectRuntime() {
270
- if (globalThis.process !== void 0 && globalThis.process.release?.name === "node") return "node";
271
- if (globalThis.Deno !== void 0) return "deno";
272
- if (globalThis.Bun !== void 0) return "bun";
278
+ return detectRuntimeFromGlobals(getRuntimeGlobals());
279
+ }
280
+ /**
281
+ * Detect the active JavaScript runtime from globals.
282
+ *
283
+ * @param globals - Runtime globals to inspect
284
+ * @returns The detected runtime name
285
+ */
286
+ function detectRuntimeFromGlobals(globals) {
287
+ if (globals.Bun !== void 0) return "bun";
288
+ if (globals.Deno !== void 0) return "deno";
289
+ if (globals.process !== void 0 && globals.process.release?.name === "node") return "node";
273
290
  return "unknown";
274
291
  }
275
- function quoteIfNeeded(path) {
276
- return path.includes(" ") ? `'${path}'` : path;
292
+ /**
293
+ * Quote a shell command part when needed.
294
+ *
295
+ * This protects the command part when the generated completion script later
296
+ * evaluates the assembled command string. Callers that embed the quoted result
297
+ * into another shell string still need to escape that outer string.
298
+ *
299
+ * @param value - The command part to quote
300
+ * @returns The shell-safe command part
301
+ */
302
+ function shellQuote(value) {
303
+ if (value.length === 0) return "''";
304
+ if (/^[\w%+,./:=@-]+$/.test(value)) return value;
305
+ return `'${value.replaceAll(`'`, `'\\''`)}'`;
306
+ }
307
+ function escapeDoubleQuotedShellString(value) {
308
+ return value.replaceAll(/[$`"\\]/g, "\\$&");
309
+ }
310
+ function basename(path) {
311
+ const slash = path.lastIndexOf("/");
312
+ const backslash = path.lastIndexOf("\\");
313
+ return path.slice(Math.max(slash, backslash) + 1);
314
+ }
315
+ /**
316
+ * Check if the entry is a Bun virtual entry.
317
+ *
318
+ * @param entry - The entry to check
319
+ * @returns Whether the entry is a Bun virtual entry
320
+ *
321
+ * Bun compiled executables may surface internal virtual paths in `process.argv`
322
+ * instead of the original source entry. These markers are intentionally private
323
+ * to keep the single-binary check heuristic rather than a public contract.
324
+ *
325
+ * @see https://github.com/oven-sh/bun/blob/main/src/standalone_graph/StandaloneModuleGraph.rs#L44-L70
326
+ * @see https://github.com/oven-sh/bun/blob/main/test/regression/issue/22157.test.ts#L47-L55
327
+ */
328
+ function isBunVirtualEntry(entry) {
329
+ return entry?.startsWith("/$bunfs/") === true || entry?.startsWith("B:\\~BUN\\") === true || entry?.startsWith("B:/~BUN/") === true;
330
+ }
331
+ /**
332
+ * Check if the executable is a likely Bun single binary.
333
+ *
334
+ * @param execPath - The executable path
335
+ * @param entry - The entry to check
336
+ * @returns Whether the executable is a likely Bun single binary
337
+ *
338
+ * Bun documents `bun build --compile` as producing a single-file executable,
339
+ * but it does not expose a stable "compiled executable" runtime flag.
340
+ * Prefer calling the current executable when `process.execPath` is no longer
341
+ * the Bun launcher, because the shell completion script needs to re-enter the
342
+ * compiled CLI rather than the original source file.
343
+ *
344
+ * @see https://bun.com/docs/bundler/executables
345
+ * @see https://github.com/oven-sh/bun/issues/14676
346
+ */
347
+ function isLikelyBunSingleBinary(execPath, entry) {
348
+ const name = basename(execPath).toLowerCase();
349
+ if (name !== "bun" && name !== "bun.exe") return true;
350
+ return isBunVirtualEntry(entry);
351
+ }
352
+ /**
353
+ * Resolve executable command parts for Node.js source execution.
354
+ *
355
+ * @param process - Node-compatible process global
356
+ * @returns Executable command parts
357
+ */
358
+ function resolveNodeExecParts(process) {
359
+ const entry = process.argv[1];
360
+ return [
361
+ process.execPath,
362
+ ...process.execArgv ?? [],
363
+ ...entry ? [entry] : []
364
+ ];
365
+ }
366
+ /**
367
+ * Resolve executable command parts for Bun source and compiled execution.
368
+ *
369
+ * @param process - Bun process global
370
+ * @returns Executable command parts
371
+ */
372
+ function resolveBunExecParts(process) {
373
+ const entry = process.argv[1];
374
+ if (isLikelyBunSingleBinary(process.execPath, entry)) return [process.execPath];
375
+ return [
376
+ process.execPath,
377
+ ...process.execArgv ?? [],
378
+ ...entry ? [entry] : []
379
+ ];
380
+ }
381
+ /**
382
+ * Join executable command parts into a shell-safe command string.
383
+ *
384
+ * @param parts - Executable command parts
385
+ * @returns The executable command string
386
+ */
387
+ function joinExecParts(parts) {
388
+ return escapeDoubleQuotedShellString(parts.map(shellQuote).join(" "));
277
389
  }
278
390
  /**
279
391
  * Quote the exec command for the current runtime.
@@ -281,17 +393,12 @@ function quoteIfNeeded(path) {
281
393
  * @returns The quoted exec command string
282
394
  */
283
395
  function quoteExec() {
396
+ const globals = getRuntimeGlobals();
284
397
  switch (detectRuntime()) {
285
- case "node": {
286
- const execPath = globalThis.process.execPath;
287
- const processArgs = globalThis.process.argv.slice(1);
288
- const quotedExecPath = quoteIfNeeded(execPath);
289
- const quotedProcessArgs = processArgs.map(quoteIfNeeded);
290
- return `${quotedExecPath} ${globalThis.process.execArgv.map(quoteIfNeeded).join(" ")} ${quotedProcessArgs[0]}`;
291
- }
292
- case "deno": throw new Error("deno not implemented yet, welcome contributions :)");
293
- case "bun": throw new Error("bun not implemented yet, welcome contributions :)");
294
- default: throw new Error("Unsupported your javascript runtime for completion script generation.");
398
+ case "node": return joinExecParts(resolveNodeExecParts(globals.process));
399
+ case "deno": throw new Error("Deno runtime is not supported for completion script generation yet.");
400
+ case "bun": return joinExecParts(resolveBunExecParts(globals.process));
401
+ default: throw new Error("Unsupported JavaScript runtime for completion script generation.");
295
402
  }
296
403
  }
297
404
  //#endregion
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gunshi/plugin-completion",
3
3
  "description": "completion plugin for gunshi",
4
- "version": "0.31.0",
4
+ "version": "0.32.0",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -53,10 +53,10 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@bomb.sh/tab": "^0.0.15",
56
- "@gunshi/plugin": "0.31.0"
56
+ "@gunshi/plugin": "0.32.0"
57
57
  },
58
58
  "peerDependencies": {
59
- "@gunshi/plugin-i18n": "0.31.0"
59
+ "@gunshi/plugin-i18n": "0.32.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^25.6.2",
@@ -68,8 +68,8 @@
68
68
  "tsdown": "0.21.0",
69
69
  "typedoc": "^0.28.19",
70
70
  "typedoc-plugin-markdown": "^4.11.0",
71
- "@gunshi/shared": "0.31.0",
72
- "@gunshi/plugin-i18n": "0.31.0"
71
+ "@gunshi/plugin-i18n": "0.32.0",
72
+ "@gunshi/shared": "0.32.0"
73
73
  },
74
74
  "scripts": {
75
75
  "build": "tsdown",