@quatrain/cli 1.1.8 → 1.1.10

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,60 +1,103 @@
1
- # @quatrain/core-cli
1
+ # @quatrain/cli
2
2
 
3
- The official Command Line Interface (CLI) for the Quatrain ecosystem.
4
- This CLI provides tools to scaffold projects, generate normalized bootloader configurations, and create migration files.
3
+ The official Command Line Interface (CLI) and script utility library for the Quatrain ecosystem.
5
4
 
6
- ## Installation
5
+ This package serves two distinct purposes:
6
+ 1. **Programmatic Utilities (Library API):** Exported classes and prompt helpers to build interactive scripts and run system subprocesses (e.g. within agent skills).
7
+ 2. **Core Command-Line Executable (`core`):** A global terminal command runner to scaffold projects, generate configurations, and manage deployments.
7
8
 
8
- You can install the CLI globally via NPM or Yarn, or run it on the fly using `npx` or `bunx`.
9
+ ---
9
10
 
10
- ### Global Installation
11
+ ## 1. Programmatic Utilities (Library API)
11
12
 
12
- ```bash
13
- npm install -g @quatrain/core-cli
14
- # or
15
- yarn global add @quatrain/core-cli
16
- # or via Bun
17
- bun add -g @quatrain/core-cli
18
- ```
13
+ Import these utilities directly in your TypeScript/JavaScript scripts to interact with the user or run external processes.
19
14
 
20
- ### On-the-fly Execution
15
+ ### A. Fluent Command Executor (`Command`)
21
16
 
22
- ```bash
23
- npx @quatrain/core-cli <command>
24
- # or
25
- bunx @quatrain/core-cli <command>
17
+ The `Command` class provides a cross-platform, fluent builder-pattern interface to execute system subprocesses. It simplifies spawning commands, passing arguments, setting working directories, extending environment variables, and supports PowerShell routing.
18
+
19
+ ```typescript
20
+ import { Command } from '@quatrain/cli';
21
+
22
+ const result = await Command.create('kubectl')
23
+ .arg('apply')
24
+ .arg('-f')
25
+ .arg('deployment.yaml')
26
+ .cwd('/path/to/project')
27
+ .env({ KUBECONFIG: '/path/to/config' })
28
+ .execute();
29
+
30
+ if (result.success) {
31
+ console.log(`Success: ${result.stdout}`);
32
+ } else {
33
+ console.error(`Exit code: ${result.code}, Error: ${result.stderr}`);
34
+ }
26
35
  ```
27
36
 
28
- ## Commands
37
+ **Fluent Methods:**
38
+ - `Command.create(bin)` / `new Command(bin)`: Start building a command for the given binary.
39
+ - `.arg(value)` / `.args([values])`: Append command-line arguments.
40
+ - `.cwd(dir)`: Set the execution working directory.
41
+ - `.env({ KEY: VALUE })`: Set or extend environment variables.
42
+ - `.inherit()`: Direct stdout and stderr to the parent process terminal.
43
+ - `.usePowerShell(use, type)`: Force process execution through PowerShell (`powershell.exe` or `pwsh`) with safe quote escaping.
44
+ - `.execute()`: Run the process asynchronously and return `{ stdout, stderr, code, success }`.
29
45
 
30
- ### `core generate scaffold <project-name>`
31
- Quickly initializes a new Quatrain project.
32
- - Creates a base directory.
33
- - Sets up the `apps/`, `data/`, `config/`, `packages/`, and `migrations/` folders.
34
- - Generates a monorepo-ready `package.json` utilizing Yarn workspaces.
35
- - Generates a `tsconfig.json` pre-configured with the required path mappings.
46
+ ### B. Interactive Prompt Helpers
36
47
 
37
- ### `core generate config`
38
- Starts an interactive wizard to generate a `quatrain.json` configuration file.
39
- - Prompts for Backend, Auth, Queue, Storage, and Messaging adapters.
40
- - Generates a normalized JSON configuration.
41
- - The generated `env(...)` tokens will be resolved at runtime by the `AppBootloader`.
48
+ Helpers wrapping `inquirer` to prompt user inputs cleanly:
42
49
 
43
- ### `core generate migration <name>`
44
- Scaffolds a new migration file.
45
- - Creates a `migrations/` directory if it does not exist.
46
- - Generates a timestamped TypeScript file (e.g., `20260427184500_init.ts`).
47
- - Provides boilerplate `up()` and `down()` methods.
50
+ ```typescript
51
+ import { askConfirm, askInput, askChoice } from '@quatrain/cli';
48
52
 
49
- ## Language Guidelines
50
- > **Recommendation:** All text contents (such as console logs, commit messages, and comments) within the Quatrain ecosystem must be written in **International English**. This ensures accessibility and maintainability for developers worldwide.
53
+ // Yes/No Confirmations
54
+ const proceed = await askConfirm('Do you want to deploy now?');
55
+
56
+ // String Inputs
57
+ const name = await askInput('Enter your username:', 'default_user');
58
+
59
+ // Multi-choice select lists
60
+ const selected = await askChoice('Select action:', [
61
+ { name: 'Sync Google Calendar', value: 'sync' },
62
+ { name: 'Reset Database', value: 'reset' }
63
+ ]);
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 2. Core Command-Line Executable
51
69
 
52
- ## HOWTO / Usage Examples
70
+ A global CLI tool invoked via the `core` command (or `quatrain` depending on symlinks).
71
+
72
+ ### Installation
73
+
74
+ Install globally or run on-the-fly:
53
75
 
54
76
  ```bash
55
- # Example of scaffolding a new project
56
- yarn global add @quatrain/core-cli
57
- quatrain generate scaffold my-app
58
- cd my-app
59
- yarn install
77
+ # Global
78
+ bun add -g @quatrain/cli
79
+
80
+ # Run on the fly
81
+ bunx @quatrain/cli <command>
60
82
  ```
83
+
84
+ ### Commands Reference
85
+
86
+ #### `core deploy`
87
+ Manage Kubernetes deployments (create, list, modify, promote, delete namespaces and manifests).
88
+
89
+ #### `core generate scaffold <project-name>`
90
+ Initialize a new Quatrain project structure:
91
+ - Sets up directories: `apps/`, `data/`, `config/`, `packages/`, `migrations/`.
92
+ - Generates a monorepo-ready workspace `package.json` and a pre-configured `tsconfig.json`.
93
+
94
+ #### `core generate config`
95
+ Start an interactive wizard to generate the `quatrain.json` bootloader configuration file.
96
+
97
+ #### `core generate migration <name>`
98
+ Scaffold a timestamped TypeScript migration file (e.g., `migrations/20260427_name.ts`) with template `up()` and `down()` blocks.
99
+
100
+ ---
101
+
102
+ ## Language Guidelines
103
+ > **Recommendation:** All text contents (logs, console prints, commit messages, comments) within the Quatrain ecosystem must be written in **International English** to ensure global team maintainability.
@@ -0,0 +1,25 @@
1
+ export declare class Command {
2
+ private bin;
3
+ private argsList;
4
+ private workingDir?;
5
+ private envVars;
6
+ private stdoutBehavior;
7
+ private stderrBehavior;
8
+ private useShell;
9
+ private shellType?;
10
+ constructor(bin: string);
11
+ static create(bin: string): Command;
12
+ arg(value: string): this;
13
+ args(values: string[]): this;
14
+ cwd(dir: string): this;
15
+ env(vars: Record<string, string>): this;
16
+ inherit(): this;
17
+ usePowerShell(use?: boolean, type?: 'powershell' | 'pwsh'): this;
18
+ execute(): Promise<{
19
+ stdout: string;
20
+ stderr: string;
21
+ code: number | null;
22
+ success: boolean;
23
+ }>;
24
+ }
25
+ //# sourceMappingURL=Command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Command.d.ts","sourceRoot":"","sources":["../src/Command.ts"],"names":[],"mappings":"AAMA,qBAAa,OAAO;IAClB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,cAAc,CAAyC;IAC/D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAC,CAAwB;gBAM9B,GAAG,EAAE,MAAM;IAQvB,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAQnC,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IASxB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAS5B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAStB,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAQvC,OAAO,IAAI,IAAI;IAWf,aAAa,CAAC,GAAG,UAAO,EAAE,IAAI,GAAE,YAAY,GAAG,MAAqB,GAAG,IAAI;IAUrE,OAAO,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CA4DpG"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Command = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ class Command {
6
+ bin;
7
+ argsList = [];
8
+ workingDir;
9
+ envVars = {};
10
+ stdoutBehavior = 'pipe';
11
+ stderrBehavior = 'pipe';
12
+ useShell = false;
13
+ shellType;
14
+ constructor(bin) {
15
+ this.bin = bin;
16
+ }
17
+ static create(bin) {
18
+ return new Command(bin);
19
+ }
20
+ arg(value) {
21
+ this.argsList.push(value);
22
+ return this;
23
+ }
24
+ args(values) {
25
+ this.argsList.push(...values);
26
+ return this;
27
+ }
28
+ cwd(dir) {
29
+ this.workingDir = dir;
30
+ return this;
31
+ }
32
+ env(vars) {
33
+ Object.assign(this.envVars, vars);
34
+ return this;
35
+ }
36
+ inherit() {
37
+ this.stdoutBehavior = 'inherit';
38
+ this.stderrBehavior = 'inherit';
39
+ return this;
40
+ }
41
+ usePowerShell(use = true, type = 'powershell') {
42
+ this.useShell = use;
43
+ this.shellType = type;
44
+ return this;
45
+ }
46
+ async execute() {
47
+ return new Promise((resolve, reject) => {
48
+ let spawnBin = this.bin;
49
+ let spawnArgs = [...this.argsList];
50
+ const isWin = process.platform === 'win32';
51
+ if (this.useShell || (isWin && this.shellType)) {
52
+ const shell = this.shellType || (isWin ? 'powershell.exe' : 'pwsh');
53
+ const cmdString = [this.bin, ...this.argsList]
54
+ .map((arg) => {
55
+ if (arg.includes(' ') || arg.includes('"') || arg.includes("'")) {
56
+ return `"${arg.replace(/"/g, '`"')}"`;
57
+ }
58
+ return arg;
59
+ })
60
+ .join(' ');
61
+ spawnBin = shell;
62
+ spawnArgs = ['-NoProfile', '-NonInteractive', '-Command', cmdString];
63
+ }
64
+ const child = (0, node_child_process_1.spawn)(spawnBin, spawnArgs, {
65
+ cwd: this.workingDir,
66
+ env: { ...process.env, ...this.envVars },
67
+ stdio: ['inherit', this.stdoutBehavior, this.stderrBehavior],
68
+ });
69
+ let stdout = '';
70
+ let stderr = '';
71
+ if (child.stdout) {
72
+ child.stdout.on('data', (data) => {
73
+ stdout += data.toString();
74
+ });
75
+ }
76
+ if (child.stderr) {
77
+ child.stderr.on('data', (data) => {
78
+ stderr += data.toString();
79
+ });
80
+ }
81
+ child.on('close', (code) => {
82
+ resolve({
83
+ stdout,
84
+ stderr,
85
+ code,
86
+ success: code === 0,
87
+ });
88
+ });
89
+ child.on('error', (err) => {
90
+ reject(err);
91
+ });
92
+ });
93
+ }
94
+ }
95
+ exports.Command = Command;
96
+ //# sourceMappingURL=Command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Command.js","sourceRoot":"","sources":["../src/Command.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -9,4 +9,5 @@ export declare function askChoice<T = string>(message: string, choices: (string
9
9
  import { Command } from 'commander';
10
10
  export declare class CliCommand extends Command {
11
11
  }
12
+ export { Command } from './Command';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAE,CAAC;AAKpB,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAUrF;AAKD,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUpF;AAKD,wBAAsB,SAAS,CAAC,CAAC,GAAG,MAAM,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC,EAAE,GAC/C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,qBAAa,UAAW,SAAQ,OAAO;CAAG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAE,QAAQ,EAAE,CAAC;AAKpB,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAUrF;AAKD,wBAAsB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAUpF;AAKD,wBAAsB,SAAS,CAAC,CAAC,GAAG,MAAM,EACxC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC,EAAE,GAC/C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,qBAAa,UAAW,SAAQ,OAAO;CAAG;AAE1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CliCommand = exports.inquirer = void 0;
6
+ exports.Command = exports.CliCommand = exports.inquirer = void 0;
7
7
  exports.askConfirm = askConfirm;
8
8
  exports.askInput = askInput;
9
9
  exports.askChoice = askChoice;
@@ -46,4 +46,6 @@ const commander_1 = require("commander");
46
46
  class CliCommand extends commander_1.Command {
47
47
  }
48
48
  exports.CliCommand = CliCommand;
49
+ var Command_1 = require("./Command");
50
+ Object.defineProperty(exports, "Command", { enumerable: true, get: function () { return Command_1.Command; } });
49
51
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAOA,gCAUC;AAKD,4BAUC;AAKD,8BAaC;AAlDD,wDAAgC;AAEvB,mBAFF,kBAAQ,CAEE;AAKV,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,UAAU,GAAG,IAAI;IACjE,MAAM,MAAM,GAAG,MAAM,kBAAQ,CAAC,MAAM,CAAC;QACnC;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,OAAO;YACb,OAAO;YACP,OAAO,EAAE,UAAU;SACpB;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAKM,KAAK,UAAU,QAAQ,CAAC,OAAe,EAAE,UAAmB;IACjE,MAAM,MAAM,GAAG,MAAM,kBAAQ,CAAC,MAAM,CAAC;QACnC;YACE,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,OAAO;YACb,OAAO;YACP,OAAO,EAAE,UAAU;SACpB;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAKM,KAAK,UAAU,SAAS,CAC7B,OAAe,EACf,OAAgD;IAEhD,MAAM,MAAM,GAAG,MAAM,kBAAQ,CAAC,MAAM,CAAC;QACnC;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,OAAO;YACb,OAAO;YACP,OAAO;SACR;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAED,yCAAoC;AAKpC,MAAa,UAAW,SAAQ,mBAAO;CAAG;AAA1C,gCAA0C"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAOA,gCAUC;AAKD,4BAUC;AAKD,8BAaC;AAlDD,wDAAgC;AAEvB,mBAFF,kBAAQ,CAEE;AAKV,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,UAAU,GAAG,IAAI;IACjE,MAAM,MAAM,GAAG,MAAM,kBAAQ,CAAC,MAAM,CAAC;QACnC;YACE,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,OAAO;YACb,OAAO;YACP,OAAO,EAAE,UAAU;SACpB;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAKM,KAAK,UAAU,QAAQ,CAAC,OAAe,EAAE,UAAmB;IACjE,MAAM,MAAM,GAAG,MAAM,kBAAQ,CAAC,MAAM,CAAC;QACnC;YACE,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,OAAO;YACb,OAAO;YACP,OAAO,EAAE,UAAU;SACpB;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAKM,KAAK,UAAU,SAAS,CAC7B,OAAe,EACf,OAAgD;IAEhD,MAAM,MAAM,GAAG,MAAM,kBAAQ,CAAC,MAAM,CAAC;QACnC;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,OAAO;YACb,OAAO;YACP,OAAO;SACR;KACF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,KAAK,CAAC;AACtB,CAAC;AAED,yCAAoC;AAKpC,MAAa,UAAW,SAAQ,mBAAO;CAAG;AAA1C,gCAA0C;AAE1C,qCAAoC;AAA3B,kGAAA,OAAO,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatrain/cli",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "license": "AGPL-3.0-only",
5
5
  "description": "Quatrain Core CLI for generating configurations and migrations",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,62 @@
1
+ import { Command } from './Command';
2
+ import path from 'node:path';
3
+
4
+ describe('Command', () => {
5
+ test('should construct and return static instance', () => {
6
+ const cmd = Command.create('node');
7
+ expect(cmd).toBeInstanceOf(Command);
8
+ });
9
+
10
+ test('should execute system echo command and retrieve stdout', async () => {
11
+ const cmd = Command.create('echo')
12
+ .arg('hello world');
13
+
14
+ const result = await cmd.execute();
15
+ expect(result.success).toBe(true);
16
+ expect(result.code).toBe(0);
17
+ expect(result.stdout.trim()).toBe('hello world');
18
+ });
19
+
20
+ test('should pass environment variables correctly', async () => {
21
+ // Run a node script that prints the environment variable
22
+ const cmd = Command.create('node')
23
+ .args(['-e', 'console.log(process.env.TEST_VAR)'])
24
+ .env({ TEST_VAR: 'quatrain-cli' });
25
+
26
+ const result = await cmd.execute();
27
+ expect(result.success).toBe(true);
28
+ expect(result.stdout.trim()).toBe('quatrain-cli');
29
+ });
30
+
31
+ test('should execute within specified working directory', async () => {
32
+ const targetDir = path.resolve(__dirname);
33
+ const cmd = Command.create('node')
34
+ .args(['-e', 'console.log(process.cwd())'])
35
+ .cwd(targetDir);
36
+
37
+ const result = await cmd.execute();
38
+ expect(result.success).toBe(true);
39
+ // CWD returned from subprocess should match our targetDir
40
+ // Note: On Mac, /var/folders can resolve to /private/var/folders, so we resolve both paths
41
+ const resolvedPath = fsResolve(result.stdout.trim());
42
+ const expectedPath = fsResolve(targetDir);
43
+ expect(resolvedPath).toBe(expectedPath);
44
+ });
45
+
46
+ test('should report process failure on bad commands', async () => {
47
+ const cmd = Command.create('node')
48
+ .args(['-e', 'process.exit(5)']);
49
+
50
+ const result = await cmd.execute();
51
+ expect(result.success).toBe(false);
52
+ expect(result.code).toBe(5);
53
+ });
54
+ });
55
+
56
+ function fsResolve(p: string): string {
57
+ try {
58
+ return path.resolve(p);
59
+ } catch {
60
+ return p;
61
+ }
62
+ }
package/src/Command.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ /**
4
+ * Fluent builder for launching and managing system subprocesses.
5
+ * Supports cross-platform execution and shell redirection (e.g. PowerShell).
6
+ */
7
+ export class Command {
8
+ private bin: string;
9
+ private argsList: string[] = [];
10
+ private workingDir?: string;
11
+ private envVars: Record<string, string> = {};
12
+ private stdoutBehavior: 'pipe' | 'inherit' | 'ignore' = 'pipe';
13
+ private stderrBehavior: 'pipe' | 'inherit' | 'ignore' = 'pipe';
14
+ private useShell = false;
15
+ private shellType?: 'powershell' | 'pwsh';
16
+
17
+ /**
18
+ * Instantiate a new Command.
19
+ * @param bin Name or path of the binary/command.
20
+ */
21
+ constructor(bin: string) {
22
+ this.bin = bin;
23
+ }
24
+
25
+ /**
26
+ * Static factory to instantiate a new Command.
27
+ * @param bin Name or path of the binary/command.
28
+ */
29
+ static create(bin: string): Command {
30
+ return new Command(bin);
31
+ }
32
+
33
+ /**
34
+ * Add a single command-line argument.
35
+ * @param value Argument string.
36
+ */
37
+ arg(value: string): this {
38
+ this.argsList.push(value);
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Add multiple command-line arguments.
44
+ * @param values List of argument strings.
45
+ */
46
+ args(values: string[]): this {
47
+ this.argsList.push(...values);
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * Set the working directory for the subprocess.
53
+ * @param dir Absolute or relative directory path.
54
+ */
55
+ cwd(dir: string): this {
56
+ this.workingDir = dir;
57
+ return this;
58
+ }
59
+
60
+ /**
61
+ * Set or extend environment variables for the subprocess.
62
+ * @param vars Key-value dictionary of environment variables.
63
+ */
64
+ env(vars: Record<string, string>): this {
65
+ Object.assign(this.envVars, vars);
66
+ return this;
67
+ }
68
+
69
+ /**
70
+ * Configure the subprocess to inherit stdout and stderr from the parent process.
71
+ */
72
+ inherit(): this {
73
+ this.stdoutBehavior = 'inherit';
74
+ this.stderrBehavior = 'inherit';
75
+ return this;
76
+ }
77
+
78
+ /**
79
+ * Enable executing the command via PowerShell (powershell.exe or pwsh).
80
+ * @param use Whether to use PowerShell.
81
+ * @param type Shell binary choice ('powershell' or 'pwsh').
82
+ */
83
+ usePowerShell(use = true, type: 'powershell' | 'pwsh' = 'powershell'): this {
84
+ this.useShell = use;
85
+ this.shellType = type;
86
+ return this;
87
+ }
88
+
89
+ /**
90
+ * Execute the configured command and return a Promise resolving on process exit.
91
+ * @returns Promise resolving with standard output, error streams, and status.
92
+ */
93
+ async execute(): Promise<{ stdout: string; stderr: string; code: number | null; success: boolean }> {
94
+ return new Promise((resolve, reject) => {
95
+ let spawnBin = this.bin;
96
+ let spawnArgs = [...this.argsList];
97
+
98
+ const isWin = process.platform === 'win32';
99
+
100
+ // Route execution via PowerShell if configured or if on Windows with shellType set
101
+ if (this.useShell || (isWin && this.shellType)) {
102
+ const shell = this.shellType || (isWin ? 'powershell.exe' : 'pwsh');
103
+
104
+ // Assemble argument list safely escaping quotes for PowerShell Command parsing
105
+ const cmdString = [this.bin, ...this.argsList]
106
+ .map((arg) => {
107
+ if (arg.includes(' ') || arg.includes('"') || arg.includes("'")) {
108
+ return `"${arg.replace(/"/g, '`"')}"`;
109
+ }
110
+ return arg;
111
+ })
112
+ .join(' ');
113
+
114
+ spawnBin = shell;
115
+ spawnArgs = ['-NoProfile', '-NonInteractive', '-Command', cmdString];
116
+ }
117
+
118
+ const child = spawn(spawnBin, spawnArgs, {
119
+ cwd: this.workingDir,
120
+ env: { ...process.env, ...this.envVars },
121
+ stdio: ['inherit', this.stdoutBehavior, this.stderrBehavior],
122
+ });
123
+
124
+ let stdout = '';
125
+ let stderr = '';
126
+
127
+ if (child.stdout) {
128
+ child.stdout.on('data', (data) => {
129
+ stdout += data.toString();
130
+ });
131
+ }
132
+
133
+ if (child.stderr) {
134
+ child.stderr.on('data', (data) => {
135
+ stderr += data.toString();
136
+ });
137
+ }
138
+
139
+ child.on('close', (code) => {
140
+ resolve({
141
+ stdout,
142
+ stderr,
143
+ code,
144
+ success: code === 0,
145
+ });
146
+ });
147
+
148
+ child.on('error', (err) => {
149
+ reject(err);
150
+ });
151
+ });
152
+ }
153
+ }
package/src/index.ts CHANGED
@@ -57,3 +57,5 @@ import { Command } from 'commander';
57
57
  */
58
58
  export class CliCommand extends Command {}
59
59
 
60
+ export { Command } from './Command';
61
+