@sandro-sikic/maker 1.0.11 → 1.1.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 +14 -8
- package/docs/API.md +6 -2
- package/docs/USAGE.md +7 -3
- package/example.ts +24 -0
- package/index.d.ts +20 -2
- package/index.js +195 -5
- package/package.json +1 -1
- package/test/index.test.js +400 -73
- package/example.js +0 -13
package/README.md
CHANGED
|
@@ -47,13 +47,13 @@ loading.succeed('Project created! 🎉');
|
|
|
47
47
|
|
|
48
48
|
### Core Functions
|
|
49
49
|
|
|
50
|
-
| Function | Description
|
|
51
|
-
| -------------------- |
|
|
52
|
-
| `init()`
|
|
53
|
-
| `run(command, opts)` | Executes shell commands with streaming output
|
|
54
|
-
| `onExit(callback)` | Registers cleanup function for graceful shutdown
|
|
55
|
-
| `prompt.*` | Interactive prompts (input, select, confirm, etc.)
|
|
56
|
-
| `spinner(text)` | Creates terminal loading indicators
|
|
50
|
+
| Function | Description |
|
|
51
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `init(opts?)` | Validates interactive terminal environment; accepts optional options object (`{ configPath?: string }`) |
|
|
53
|
+
| `run(command, opts)` | Executes shell commands with streaming output |
|
|
54
|
+
| `onExit(callback)` | Registers cleanup function for graceful shutdown |
|
|
55
|
+
| `prompt.*` | Interactive prompts (input, select, confirm, etc.) |
|
|
56
|
+
| `spinner(text)` | Creates terminal loading indicators |
|
|
57
57
|
|
|
58
58
|
### Example: Simple Build Tool
|
|
59
59
|
|
|
@@ -100,12 +100,18 @@ build();
|
|
|
100
100
|
|
|
101
101
|
### `init()`
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Initializes CLI environment and validates your process is running in an interactive terminal. Call first in your CLI app. Accepts an optional options object: `{ configPath?: string }`.
|
|
104
104
|
|
|
105
105
|
```javascript
|
|
106
|
+
// default
|
|
106
107
|
maker.init();
|
|
108
|
+
|
|
109
|
+
// override config file location
|
|
110
|
+
maker.init({ configPath: '/path/to/config.cfg' });
|
|
107
111
|
```
|
|
108
112
|
|
|
113
|
+
Note: passing a plain string to `init()` is not supported — use an options object instead.
|
|
114
|
+
|
|
109
115
|
### `run(command, opts)`
|
|
110
116
|
|
|
111
117
|
Execute shell commands with real-time output streaming.
|
package/docs/API.md
CHANGED
|
@@ -6,13 +6,17 @@ Quick reference guide for `@sandro-sikic/maker` functions.
|
|
|
6
6
|
|
|
7
7
|
## `init()`
|
|
8
8
|
|
|
9
|
-
Validates interactive terminal environment. **Call first** in your CLI app.
|
|
9
|
+
Validates interactive terminal environment. **Call first** in your CLI app. Accepts an optional options object `{ configPath?: string }` to override the location of `config.cfg`.
|
|
10
10
|
|
|
11
11
|
```javascript
|
|
12
|
+
// default
|
|
12
13
|
maker.init();
|
|
14
|
+
|
|
15
|
+
// with options
|
|
16
|
+
maker.init({ configPath: '/custom/path/config.cfg' });
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
Exits with code 1 if not running in an interactive terminal (TTY).
|
|
19
|
+
Exits with code 1 if not running in an interactive terminal (TTY). Passing a plain string is not supported.
|
|
16
20
|
|
|
17
21
|
---
|
|
18
22
|
|
package/docs/USAGE.md
CHANGED
|
@@ -28,18 +28,22 @@ loading.succeed('Installation complete!');
|
|
|
28
28
|
|
|
29
29
|
### `init()`
|
|
30
30
|
|
|
31
|
-
Initializes the CLI environment and validates that the application is running in an interactive terminal (TTY). This should be called at the start of your CLI application.
|
|
31
|
+
Initializes the CLI environment and validates that the application is running in an interactive terminal (TTY). This should be called at the start of your CLI application. You may pass an optional options object `{ configPath?: string }` to change where `config.cfg` is read/written.
|
|
32
32
|
|
|
33
33
|
**Returns:** `void`
|
|
34
34
|
|
|
35
35
|
**Throws:** Exits with code 1 if not running in an interactive terminal
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**Examples:**
|
|
38
38
|
|
|
39
39
|
```javascript
|
|
40
40
|
import { init } from '@sandro-sikic/maker';
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// default
|
|
43
|
+
init();
|
|
44
|
+
|
|
45
|
+
// override config file
|
|
46
|
+
init({ configPath: '/custom/path/config.cfg' });
|
|
43
47
|
```
|
|
44
48
|
|
|
45
49
|
**Why use it?**
|
package/example.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as maker from './index.js';
|
|
2
|
+
|
|
3
|
+
(async () => {
|
|
4
|
+
maker.init();
|
|
5
|
+
|
|
6
|
+
maker.save('test key', 'test.txt');
|
|
7
|
+
maker.save('key test', 'test.txt');
|
|
8
|
+
|
|
9
|
+
const answer = await maker.prompt.confirm({
|
|
10
|
+
message: 'Do you want to continue?',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const processing = await maker.spinner('Processing...').start();
|
|
14
|
+
console.log('Answer:', answer);
|
|
15
|
+
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
17
|
+
|
|
18
|
+
await maker.run('echo "Hello, World!"');
|
|
19
|
+
|
|
20
|
+
processing.succeed();
|
|
21
|
+
|
|
22
|
+
console.log(maker.load('key test'));
|
|
23
|
+
console.log(maker.load('test key'));
|
|
24
|
+
})();
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference path="./storage.generated.d.ts" />
|
|
2
|
+
|
|
3
|
+
export function init(opts?: { configPath?: string }): void;
|
|
2
4
|
|
|
3
5
|
export type RunResult = {
|
|
4
6
|
output: string;
|
|
@@ -16,8 +18,24 @@ export function run(
|
|
|
16
18
|
|
|
17
19
|
export function onExit(cb: () => void | Promise<void>): () => void;
|
|
18
20
|
|
|
21
|
+
export function save<K extends keyof StorageSchema>(
|
|
22
|
+
key: K,
|
|
23
|
+
value: StorageSchema[K],
|
|
24
|
+
): void;
|
|
25
|
+
|
|
26
|
+
export function save(key: string, value: unknown): void;
|
|
27
|
+
|
|
28
|
+
export function load<K extends keyof StorageSchema>(key: K): StorageSchema[K];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Overload for unknown string keys.
|
|
32
|
+
* @deprecated Key not found in `StorageSchema`. The call is allowed and returns `unknown`.
|
|
33
|
+
*/
|
|
34
|
+
export function load(key: string): unknown;
|
|
35
|
+
|
|
19
36
|
export const prompt: typeof import('@inquirer/prompts');
|
|
20
|
-
|
|
37
|
+
|
|
38
|
+
export const spinner: typeof import('ora').default;
|
|
21
39
|
|
|
22
40
|
export type { Ora } from 'ora';
|
|
23
41
|
export * from '@inquirer/prompts';
|
package/index.js
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
1
|
import * as inquirer from '@inquirer/prompts';
|
|
2
2
|
import * as ora from 'ora';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
// filename for runtime-generated storage types
|
|
8
|
+
const GENERATED_TYPES_FILENAME = 'storage.generated.d.ts';
|
|
9
|
+
|
|
10
|
+
let _overrideConfigPath = null;
|
|
11
|
+
|
|
12
|
+
function init(opts) {
|
|
13
|
+
if (opts !== undefined) {
|
|
14
|
+
if (!opts || typeof opts !== 'object' || Array.isArray(opts)) {
|
|
15
|
+
throw new TypeError(
|
|
16
|
+
'init(opts) expects an options object (e.g. { configPath?: string })',
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Object.prototype.hasOwnProperty.call(opts, 'configPath')) {
|
|
21
|
+
if (typeof opts.configPath !== 'string') {
|
|
22
|
+
throw new TypeError('init(opts).configPath must be a string');
|
|
23
|
+
}
|
|
24
|
+
_overrideConfigPath = opts.configPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Clear the auto-generated storage types file so previously-generated keys are removed
|
|
29
|
+
try {
|
|
30
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const gen = path.join(dir, GENERATED_TYPES_FILENAME);
|
|
32
|
+
try {
|
|
33
|
+
fs.unlinkSync(gen);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// ignore if file does not exist or cannot be removed
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
/* non-fatal; don't prevent init from continuing */
|
|
39
|
+
try {
|
|
40
|
+
console.warn(
|
|
41
|
+
'maker: failed to clear generated storage types:',
|
|
42
|
+
err && err.message ? err.message : err,
|
|
43
|
+
);
|
|
44
|
+
} catch (__) {}
|
|
45
|
+
}
|
|
3
46
|
|
|
4
|
-
function init() {
|
|
5
|
-
// Ensure we're running in an interactive terminal
|
|
6
47
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
7
48
|
console.error(
|
|
8
49
|
'\n⚠ This TUI requires an interactive terminal. Run this in a terminal (not the Node REPL or a non-interactive/debug console).\n',
|
|
@@ -145,7 +186,156 @@ function onExit(cb) {
|
|
|
145
186
|
};
|
|
146
187
|
}
|
|
147
188
|
|
|
148
|
-
|
|
149
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Resolve path to the `config.cfg` file placed next to this module.
|
|
191
|
+
* If an override was provided to `init(opts)` return that path instead.
|
|
192
|
+
* @returns {string}
|
|
193
|
+
*/
|
|
194
|
+
function _configPath() {
|
|
195
|
+
if (_overrideConfigPath) return _overrideConfigPath;
|
|
196
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
197
|
+
return path.join(dir, 'config.json');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Save a value under `key` in `config.cfg` (creates file if missing).
|
|
202
|
+
* - Overwrites existing keys
|
|
203
|
+
* - Accepts objects and primitives (stored as JSON)
|
|
204
|
+
* - Additionally generates `storage.generated.d.ts` (next-time autocomplete)
|
|
205
|
+
*
|
|
206
|
+
* @param {string} key
|
|
207
|
+
* @param {*} value
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
function save(key, value) {
|
|
211
|
+
if (typeof key !== 'string' || !key) {
|
|
212
|
+
throw new TypeError('save(key, value) requires a non-empty string key');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const file = _configPath();
|
|
216
|
+
let cfg = {};
|
|
217
|
+
try {
|
|
218
|
+
const txt = fs.readFileSync(file, 'utf8');
|
|
219
|
+
cfg = txt.trim() ? JSON.parse(txt) : {};
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err && err.code !== 'ENOENT') throw err;
|
|
222
|
+
cfg = {};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cfg[key] = value;
|
|
226
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2), 'utf8');
|
|
227
|
+
|
|
228
|
+
// Generate (or update) `storage.generated.d.ts` so consumers get autocomplete
|
|
229
|
+
try {
|
|
230
|
+
_writeGeneratedStorageTypes(file);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// non-fatal: don't break save() if typegen fails
|
|
233
|
+
/* istanbul ignore next */
|
|
234
|
+
try {
|
|
235
|
+
console.warn(
|
|
236
|
+
'maker: failed to generate storage types:',
|
|
237
|
+
err && err.message ? err.message : err,
|
|
238
|
+
);
|
|
239
|
+
} catch (__) {}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Generate `storage.generated.d.ts` from the current config file.
|
|
245
|
+
* This writes a declaration that augments `StorageSchema` with all keys
|
|
246
|
+
* currently present in the config file so that TypeScript autocompletes
|
|
247
|
+
* on subsequent runs.
|
|
248
|
+
*/
|
|
249
|
+
function _writeGeneratedStorageTypes(configFile) {
|
|
250
|
+
const outDir = path.dirname(fileURLToPath(import.meta.url));
|
|
251
|
+
const outPath = path.join(outDir, GENERATED_TYPES_FILENAME);
|
|
252
|
+
let txt = '';
|
|
253
|
+
try {
|
|
254
|
+
txt = fs.readFileSync(configFile, 'utf8');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err && err.code === 'ENOENT') {
|
|
257
|
+
// nothing saved yet — remove generated file if it exists
|
|
258
|
+
try {
|
|
259
|
+
fs.unlinkSync(outPath);
|
|
260
|
+
} catch (__) {}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const cfg = txt.trim() ? JSON.parse(txt) : {};
|
|
267
|
+
const keys = Object.keys(cfg).sort();
|
|
268
|
+
|
|
269
|
+
function tsTypeForValue(v) {
|
|
270
|
+
if (v === null) return 'null';
|
|
271
|
+
const t = typeof v;
|
|
272
|
+
if (t === 'string') return 'string';
|
|
273
|
+
if (t === 'number') return 'number';
|
|
274
|
+
if (t === 'boolean') return 'boolean';
|
|
275
|
+
if (Array.isArray(v)) {
|
|
276
|
+
if (v.length === 0) return 'unknown[]';
|
|
277
|
+
const types = Array.from(new Set(v.map(tsTypeForValue)));
|
|
278
|
+
if (types.length === 1) return `${types[0]}[]`;
|
|
279
|
+
return `(${types.join(' | ')})[]`;
|
|
280
|
+
}
|
|
281
|
+
if (t === 'object') {
|
|
282
|
+
// shallow object -> prefer a loose index signature to avoid fragile typings
|
|
283
|
+
const props = Object.keys(v);
|
|
284
|
+
if (props.length === 0) return 'Record<string, unknown>';
|
|
285
|
+
// construct a shallow literal type for better DX when shape is simple
|
|
286
|
+
const parts = props.map((p) => {
|
|
287
|
+
const pn = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(p) ? p : JSON.stringify(p);
|
|
288
|
+
return `${pn}: ${tsTypeForValue(v[p])}`;
|
|
289
|
+
});
|
|
290
|
+
return `{ ${parts.join('; ')} }`;
|
|
291
|
+
}
|
|
292
|
+
return 'unknown';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const lines = keys.map((k) => {
|
|
296
|
+
const v = cfg[k];
|
|
297
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
|
|
298
|
+
? k
|
|
299
|
+
: JSON.stringify(k);
|
|
300
|
+
return ` ${safeKey}: ${tsTypeForValue(v)};`;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const content =
|
|
304
|
+
`/* Auto-generated by maker — do not edit. */\n` +
|
|
305
|
+
`interface StorageSchema {\n` +
|
|
306
|
+
(lines.length ? lines.join('\n') + '\n' : '') +
|
|
307
|
+
`}\n`;
|
|
308
|
+
|
|
309
|
+
fs.writeFileSync(outPath, content, 'utf8');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Load a value by `key` from `config.cfg` located next to this module.
|
|
314
|
+
* Returns undefined when the file or key does not exist.
|
|
315
|
+
*
|
|
316
|
+
* @param {string} key
|
|
317
|
+
* @returns {*|undefined}
|
|
318
|
+
*/
|
|
319
|
+
function load(key) {
|
|
320
|
+
if (typeof key !== 'string' || !key) {
|
|
321
|
+
throw new TypeError('load(key) requires a non-empty string key');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const file = _configPath();
|
|
325
|
+
try {
|
|
326
|
+
const txt = fs.readFileSync(file, 'utf8');
|
|
327
|
+
if (!txt.trim()) return undefined;
|
|
328
|
+
const cfg = JSON.parse(txt);
|
|
329
|
+
return Object.prototype.hasOwnProperty.call(cfg, key)
|
|
330
|
+
? cfg[key]
|
|
331
|
+
: undefined;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
if (err && err.code === 'ENOENT') return undefined;
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const prompt = inquirer;
|
|
339
|
+
const spinner = ora.default;
|
|
150
340
|
|
|
151
|
-
export { init, run, onExit, prompt, spinner };
|
|
341
|
+
export { init, run, onExit, prompt, spinner, save, load };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandro-sikic/maker",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A lightweight library for building interactive command-line tools with prompts, command execution, spinners, and graceful shutdown handling",
|
|
6
6
|
"main": "index.js",
|
package/test/index.test.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
3
|
vi.mock('ora', () => ({
|
|
4
4
|
default: () => ({ start: () => ({ stop: () => {} }) }),
|
|
5
5
|
}));
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
import { run, onExit, init, prompt, spinner, save, load } from '../index.js';
|
|
8
12
|
|
|
9
13
|
describe('run()', () => {
|
|
14
|
+
it('throws TypeError when command is not a string', async () => {
|
|
15
|
+
await expect(run(null)).rejects.toThrow(TypeError);
|
|
16
|
+
await expect(run(undefined)).rejects.toThrow(TypeError);
|
|
17
|
+
await expect(run(123)).rejects.toThrow(TypeError);
|
|
18
|
+
await expect(run({})).rejects.toThrow(TypeError);
|
|
19
|
+
await expect(run([])).rejects.toThrow(TypeError);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('throws TypeError when command is an empty string', async () => {
|
|
23
|
+
await expect(run('')).rejects.toThrow(TypeError);
|
|
24
|
+
await expect(run('')).rejects.toThrow(/non-empty string/);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('throws TypeError when command is only whitespace', async () => {
|
|
28
|
+
await expect(run(' ')).rejects.toThrow(TypeError);
|
|
29
|
+
await expect(run('\t\n')).rejects.toThrow(TypeError);
|
|
30
|
+
});
|
|
31
|
+
|
|
10
32
|
it('captures stdout for successful commands (mocked)', async () => {
|
|
11
33
|
vi.resetModules();
|
|
12
34
|
vi.doMock('child_process', () => ({
|
|
@@ -285,6 +307,25 @@ describe('init()', () => {
|
|
|
285
307
|
process.stdout.isTTY = origStdoutTTY;
|
|
286
308
|
});
|
|
287
309
|
|
|
310
|
+
it('throws when passed a non-object argument', () => {
|
|
311
|
+
expect(() => init('some/path/config.cfg')).toThrow(TypeError);
|
|
312
|
+
expect(() => init(123)).toThrow(TypeError);
|
|
313
|
+
expect(() => init(true)).toThrow(TypeError);
|
|
314
|
+
expect(() => init(null)).toThrow(TypeError);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('throws when passed an array instead of object', () => {
|
|
318
|
+
expect(() => init([])).toThrow(TypeError);
|
|
319
|
+
expect(() => init(['path'])).toThrow(TypeError);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('throws TypeError when configPath is not a string', () => {
|
|
323
|
+
expect(() => init({ configPath: 123 })).toThrow(TypeError);
|
|
324
|
+
expect(() => init({ configPath: {} })).toThrow(TypeError);
|
|
325
|
+
expect(() => init({ configPath: null })).toThrow(TypeError);
|
|
326
|
+
expect(() => init({ configPath: true })).toThrow(TypeError);
|
|
327
|
+
});
|
|
328
|
+
|
|
288
329
|
it('exits with code 1 and logs error when not in a TTY', () => {
|
|
289
330
|
const origStdinTTY = process.stdin.isTTY;
|
|
290
331
|
const origStdoutTTY = process.stdout.isTTY;
|
|
@@ -302,6 +343,38 @@ describe('init()', () => {
|
|
|
302
343
|
process.stdin.isTTY = origStdinTTY;
|
|
303
344
|
process.stdout.isTTY = origStdoutTTY;
|
|
304
345
|
});
|
|
346
|
+
|
|
347
|
+
it('clears generated storage types file when present', async () => {
|
|
348
|
+
const genPath = path.join(
|
|
349
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
350
|
+
'storage.generated.d.ts',
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// create a dummy generated file
|
|
354
|
+
await fs.writeFile(genPath, '/* generated */', 'utf8');
|
|
355
|
+
|
|
356
|
+
const origStdinTTY = process.stdin.isTTY;
|
|
357
|
+
const origStdoutTTY = process.stdout.isTTY;
|
|
358
|
+
process.stdin.isTTY = true;
|
|
359
|
+
process.stdout.isTTY = true;
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
init();
|
|
363
|
+
let exists = true;
|
|
364
|
+
try {
|
|
365
|
+
await fs.access(genPath);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
exists = false;
|
|
368
|
+
}
|
|
369
|
+
expect(exists).toBe(false);
|
|
370
|
+
} finally {
|
|
371
|
+
process.stdin.isTTY = origStdinTTY;
|
|
372
|
+
process.stdout.isTTY = origStdoutTTY;
|
|
373
|
+
try {
|
|
374
|
+
await fs.rm(genPath);
|
|
375
|
+
} catch (e) {}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
305
378
|
});
|
|
306
379
|
|
|
307
380
|
describe('exports', () => {
|
|
@@ -314,6 +387,311 @@ describe('exports', () => {
|
|
|
314
387
|
});
|
|
315
388
|
});
|
|
316
389
|
|
|
390
|
+
describe('config: save/load', () => {
|
|
391
|
+
const cfgPath = path.join(
|
|
392
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
393
|
+
'config.json',
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
afterEach(async () => {
|
|
397
|
+
try {
|
|
398
|
+
await fs.rm(cfgPath);
|
|
399
|
+
} catch (e) {
|
|
400
|
+
// ignore
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('creates file and saves/loads primitive value', async () => {
|
|
405
|
+
save('foo', 'bar');
|
|
406
|
+
expect(load('foo')).toBe('bar');
|
|
407
|
+
const txt = await fs.readFile(cfgPath, 'utf8');
|
|
408
|
+
expect(JSON.parse(txt)).toEqual({ foo: 'bar' });
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('throws TypeError when save() key is not a string', () => {
|
|
412
|
+
expect(() => save(null, 'value')).toThrow(TypeError);
|
|
413
|
+
expect(() => save(undefined, 'value')).toThrow(TypeError);
|
|
414
|
+
expect(() => save(123, 'value')).toThrow(TypeError);
|
|
415
|
+
expect(() => save({}, 'value')).toThrow(TypeError);
|
|
416
|
+
expect(() => save([], 'value')).toThrow(TypeError);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('throws TypeError when save() key is an empty string', () => {
|
|
420
|
+
expect(() => save('', 'value')).toThrow(TypeError);
|
|
421
|
+
expect(() => save('', 'value')).toThrow(/non-empty string key/);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('throws TypeError when load() key is not a string', () => {
|
|
425
|
+
expect(() => load(null)).toThrow(TypeError);
|
|
426
|
+
expect(() => load(undefined)).toThrow(TypeError);
|
|
427
|
+
expect(() => load(123)).toThrow(TypeError);
|
|
428
|
+
expect(() => load({})).toThrow(TypeError);
|
|
429
|
+
expect(() => load([])).toThrow(TypeError);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('throws TypeError when load() key is an empty string', () => {
|
|
433
|
+
expect(() => load('')).toThrow(TypeError);
|
|
434
|
+
expect(() => load('')).toThrow(/non-empty string key/);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('saves and loads null values', () => {
|
|
438
|
+
save('nullKey', null);
|
|
439
|
+
expect(load('nullKey')).toBeNull();
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('saves and loads boolean values', () => {
|
|
443
|
+
save('trueKey', true);
|
|
444
|
+
save('falseKey', false);
|
|
445
|
+
expect(load('trueKey')).toBe(true);
|
|
446
|
+
expect(load('falseKey')).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('saves and loads number values including zero and negative', () => {
|
|
450
|
+
save('zero', 0);
|
|
451
|
+
save('negative', -42);
|
|
452
|
+
save('float', 3.14);
|
|
453
|
+
expect(load('zero')).toBe(0);
|
|
454
|
+
expect(load('negative')).toBe(-42);
|
|
455
|
+
expect(load('float')).toBe(3.14);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('saves and loads deeply nested objects', () => {
|
|
459
|
+
const nested = {
|
|
460
|
+
level1: {
|
|
461
|
+
level2: {
|
|
462
|
+
level3: {
|
|
463
|
+
value: 'deep',
|
|
464
|
+
array: [1, 2, { nested: true }],
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
save('nested', nested);
|
|
470
|
+
expect(load('nested')).toEqual(nested);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('saves and loads arrays with mixed types', () => {
|
|
474
|
+
const mixed = [1, 'two', true, null, { key: 'value' }, [1, 2]];
|
|
475
|
+
save('mixedArray', mixed);
|
|
476
|
+
expect(load('mixedArray')).toEqual(mixed);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('saves and loads empty arrays and objects', () => {
|
|
480
|
+
save('emptyArray', []);
|
|
481
|
+
save('emptyObject', {});
|
|
482
|
+
expect(load('emptyArray')).toEqual([]);
|
|
483
|
+
expect(load('emptyObject')).toEqual({});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('handles keys with special characters', () => {
|
|
487
|
+
save('key-with-dashes', 'value1');
|
|
488
|
+
save('key.with.dots', 'value2');
|
|
489
|
+
save('key_with_underscores', 'value3');
|
|
490
|
+
save('key with spaces', 'value4');
|
|
491
|
+
expect(load('key-with-dashes')).toBe('value1');
|
|
492
|
+
expect(load('key.with.dots')).toBe('value2');
|
|
493
|
+
expect(load('key_with_underscores')).toBe('value3');
|
|
494
|
+
expect(load('key with spaces')).toBe('value4');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('handles corrupted JSON file gracefully in load()', async () => {
|
|
498
|
+
const cfgPath = path.join(
|
|
499
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
500
|
+
'config.json',
|
|
501
|
+
);
|
|
502
|
+
await fs.writeFile(cfgPath, '{invalid json', 'utf8');
|
|
503
|
+
expect(() => load('anyKey')).toThrow(SyntaxError);
|
|
504
|
+
await fs.rm(cfgPath);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('preserves all keys when saving multiple values', () => {
|
|
508
|
+
save('key1', 'value1');
|
|
509
|
+
save('key2', 'value2');
|
|
510
|
+
save('key3', 'value3');
|
|
511
|
+
expect(load('key1')).toBe('value1');
|
|
512
|
+
expect(load('key2')).toBe('value2');
|
|
513
|
+
expect(load('key3')).toBe('value3');
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('accepts and returns object values', async () => {
|
|
517
|
+
save('obj', { a: 1, b: 'x' });
|
|
518
|
+
expect(load('obj')).toEqual({ a: 1, b: 'x' });
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('overwrites existing key', async () => {
|
|
522
|
+
save('k', 'v1');
|
|
523
|
+
save('k', 'v2');
|
|
524
|
+
expect(load('k')).toBe('v2');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('returns undefined for missing key or missing file', async () => {
|
|
528
|
+
try {
|
|
529
|
+
await fs.rm(cfgPath);
|
|
530
|
+
} catch (e) {}
|
|
531
|
+
expect(load('nope')).toBeUndefined();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it('respects config path passed to init(opts)', async () => {
|
|
535
|
+
const origStdinTTY = process.stdin.isTTY;
|
|
536
|
+
const origStdoutTTY = process.stdout.isTTY;
|
|
537
|
+
process.stdin.isTTY = true;
|
|
538
|
+
process.stdout.isTTY = true;
|
|
539
|
+
|
|
540
|
+
vi.resetModules();
|
|
541
|
+
const { init: freshInit, save: freshSave } = await import('../index.js');
|
|
542
|
+
const customPath = path.join(
|
|
543
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
544
|
+
'custom-config.cfg',
|
|
545
|
+
);
|
|
546
|
+
try {
|
|
547
|
+
await fs.rm(customPath);
|
|
548
|
+
} catch (e) {}
|
|
549
|
+
freshInit({ configPath: customPath });
|
|
550
|
+
freshSave('customKey', 'value');
|
|
551
|
+
const txt = await fs.readFile(customPath, 'utf8');
|
|
552
|
+
expect(JSON.parse(txt)).toEqual({ customKey: 'value' });
|
|
553
|
+
await fs.rm(customPath);
|
|
554
|
+
|
|
555
|
+
process.stdin.isTTY = origStdinTTY;
|
|
556
|
+
process.stdout.isTTY = origStdoutTTY;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('handles empty config file in load()', async () => {
|
|
560
|
+
const cfgPath = path.join(
|
|
561
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
562
|
+
'config.json',
|
|
563
|
+
);
|
|
564
|
+
await fs.writeFile(cfgPath, '', 'utf8');
|
|
565
|
+
expect(load('anyKey')).toBeUndefined();
|
|
566
|
+
await fs.rm(cfgPath);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('handles whitespace-only config file in load()', async () => {
|
|
570
|
+
const cfgPath = path.join(
|
|
571
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
572
|
+
'config.json',
|
|
573
|
+
);
|
|
574
|
+
await fs.writeFile(cfgPath, ' \n\t ', 'utf8');
|
|
575
|
+
expect(load('anyKey')).toBeUndefined();
|
|
576
|
+
await fs.rm(cfgPath);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
describe('type generation (storage.generated.d.ts)', () => {
|
|
581
|
+
const cfgPath = path.join(
|
|
582
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
583
|
+
'config.json',
|
|
584
|
+
);
|
|
585
|
+
const genPath = path.join(
|
|
586
|
+
path.dirname(fileURLToPath(new URL('../index.js', import.meta.url).href)),
|
|
587
|
+
'storage.generated.d.ts',
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
afterEach(async () => {
|
|
591
|
+
try {
|
|
592
|
+
await fs.rm(cfgPath);
|
|
593
|
+
} catch (e) {}
|
|
594
|
+
try {
|
|
595
|
+
await fs.rm(genPath);
|
|
596
|
+
} catch (e) {}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('generates TypeScript types for string values', async () => {
|
|
600
|
+
save('name', 'John');
|
|
601
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
602
|
+
expect(content).toContain('interface StorageSchema');
|
|
603
|
+
expect(content).toContain('name: string');
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('generates TypeScript types for number values', async () => {
|
|
607
|
+
save('age', 42);
|
|
608
|
+
save('price', 19.99);
|
|
609
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
610
|
+
expect(content).toContain('age: number');
|
|
611
|
+
expect(content).toContain('price: number');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('generates TypeScript types for boolean values', async () => {
|
|
615
|
+
save('enabled', true);
|
|
616
|
+
save('disabled', false);
|
|
617
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
618
|
+
expect(content).toContain('enabled: boolean');
|
|
619
|
+
expect(content).toContain('disabled: boolean');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('generates TypeScript types for null values', async () => {
|
|
623
|
+
save('nullValue', null);
|
|
624
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
625
|
+
expect(content).toContain('nullValue: null');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('generates TypeScript types for empty arrays', async () => {
|
|
629
|
+
save('emptyArr', []);
|
|
630
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
631
|
+
expect(content).toContain('emptyArr: unknown[]');
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it('generates TypeScript types for homogeneous arrays', async () => {
|
|
635
|
+
save('numbers', [1, 2, 3]);
|
|
636
|
+
save('strings', ['a', 'b', 'c']);
|
|
637
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
638
|
+
expect(content).toContain('numbers: number[]');
|
|
639
|
+
expect(content).toContain('strings: string[]');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('generates TypeScript types for arrays with mixed types', async () => {
|
|
643
|
+
save('mixed', [1, 'two', true]);
|
|
644
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
645
|
+
expect(content).toMatch(/mixed: \(.*\)\[\]/);
|
|
646
|
+
expect(content).toContain('number');
|
|
647
|
+
expect(content).toContain('string');
|
|
648
|
+
expect(content).toContain('boolean');
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('generates TypeScript types for empty objects', async () => {
|
|
652
|
+
save('emptyObj', {});
|
|
653
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
654
|
+
expect(content).toContain('emptyObj: Record<string, unknown>');
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it('generates TypeScript types for shallow objects', async () => {
|
|
658
|
+
save('user', { name: 'Alice', age: 30, active: true });
|
|
659
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
660
|
+
expect(content).toContain('user:');
|
|
661
|
+
expect(content).toContain('name: string');
|
|
662
|
+
expect(content).toContain('age: number');
|
|
663
|
+
expect(content).toContain('active: boolean');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('handles keys with special characters in generated types', async () => {
|
|
667
|
+
save('key-with-dash', 'value');
|
|
668
|
+
save('key with space', 'value');
|
|
669
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
670
|
+
// Keys that aren't valid identifiers should be quoted
|
|
671
|
+
expect(content).toContain('"key-with-dash": string');
|
|
672
|
+
expect(content).toContain('"key with space": string');
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('sorts keys alphabetically in generated types', async () => {
|
|
676
|
+
save('zebra', 1);
|
|
677
|
+
save('apple', 2);
|
|
678
|
+
save('banana', 3);
|
|
679
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
680
|
+
const appleIdx = content.indexOf('apple');
|
|
681
|
+
const bananaIdx = content.indexOf('banana');
|
|
682
|
+
const zebraIdx = content.indexOf('zebra');
|
|
683
|
+
expect(appleIdx).toBeLessThan(bananaIdx);
|
|
684
|
+
expect(bananaIdx).toBeLessThan(zebraIdx);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('includes auto-generated comment header', async () => {
|
|
688
|
+
save('test', 'value');
|
|
689
|
+
const content = await fs.readFile(genPath, 'utf8');
|
|
690
|
+
expect(content).toContain('Auto-generated by maker');
|
|
691
|
+
expect(content).toContain('do not edit');
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
317
695
|
describe('run() - additional edge cases', () => {
|
|
318
696
|
it('combines stdout and stderr in output field', async () => {
|
|
319
697
|
vi.resetModules();
|
|
@@ -353,39 +731,13 @@ describe('run() - additional edge cases', () => {
|
|
|
353
731
|
vi.resetModules();
|
|
354
732
|
});
|
|
355
733
|
|
|
356
|
-
it('handles null stdout
|
|
734
|
+
it('handles null stdout and stderr streams gracefully', async () => {
|
|
357
735
|
vi.resetModules();
|
|
358
736
|
vi.doMock('child_process', () => ({
|
|
359
737
|
spawn: () => {
|
|
360
738
|
let closeCb;
|
|
361
739
|
const child = {
|
|
362
740
|
stdout: null,
|
|
363
|
-
stderr: { on: () => {} },
|
|
364
|
-
on: (ev, cb) => {
|
|
365
|
-
if (ev === 'close') closeCb = cb;
|
|
366
|
-
},
|
|
367
|
-
};
|
|
368
|
-
process.nextTick(() => {
|
|
369
|
-
if (closeCb) closeCb(0);
|
|
370
|
-
});
|
|
371
|
-
return child;
|
|
372
|
-
},
|
|
373
|
-
}));
|
|
374
|
-
const { run: mockedRun } = await import('../index.js');
|
|
375
|
-
const res = await mockedRun('anything');
|
|
376
|
-
expect(res.code).toBe(0);
|
|
377
|
-
expect(res.stdout).toBe('');
|
|
378
|
-
expect(res.isError).toBe(false);
|
|
379
|
-
vi.resetModules();
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('handles null stderr stream gracefully', async () => {
|
|
383
|
-
vi.resetModules();
|
|
384
|
-
vi.doMock('child_process', () => ({
|
|
385
|
-
spawn: () => {
|
|
386
|
-
let closeCb;
|
|
387
|
-
const child = {
|
|
388
|
-
stdout: { on: () => {} },
|
|
389
741
|
stderr: null,
|
|
390
742
|
on: (ev, cb) => {
|
|
391
743
|
if (ev === 'close') closeCb = cb;
|
|
@@ -400,12 +752,13 @@ describe('run() - additional edge cases', () => {
|
|
|
400
752
|
const { run: mockedRun } = await import('../index.js');
|
|
401
753
|
const res = await mockedRun('anything');
|
|
402
754
|
expect(res.code).toBe(0);
|
|
755
|
+
expect(res.stdout).toBe('');
|
|
403
756
|
expect(res.stderr).toBe('');
|
|
404
757
|
expect(res.isError).toBe(false);
|
|
405
758
|
vi.resetModules();
|
|
406
759
|
});
|
|
407
760
|
|
|
408
|
-
it('forwards opts
|
|
761
|
+
it('forwards opts including cwd, env, and shell to spawn', async () => {
|
|
409
762
|
vi.resetModules();
|
|
410
763
|
let captured;
|
|
411
764
|
vi.doMock('child_process', () => ({
|
|
@@ -425,40 +778,14 @@ describe('run() - additional edge cases', () => {
|
|
|
425
778
|
const res = await mockedRun('anything', {
|
|
426
779
|
cwd: '/tmp',
|
|
427
780
|
env: { FOO: 'bar' },
|
|
781
|
+
maxLines: 5, // Should be filtered out
|
|
782
|
+
shell: false,
|
|
428
783
|
});
|
|
429
784
|
expect(captured.cwd).toBe('/tmp');
|
|
430
785
|
expect(captured.env).toEqual(expect.objectContaining({ FOO: 'bar' }));
|
|
431
|
-
expect(captured.shell).toBe(true);
|
|
432
|
-
expect(captured.stdio).toBe('pipe');
|
|
433
|
-
expect(res.code).toBe(0);
|
|
434
|
-
vi.resetModules();
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('forwards top-level spawn options (maxLines is capture-only)', async () => {
|
|
438
|
-
vi.resetModules();
|
|
439
|
-
let captured;
|
|
440
|
-
vi.doMock('child_process', () => ({
|
|
441
|
-
spawn: (cmd, spawnOpts) => {
|
|
442
|
-
captured = spawnOpts;
|
|
443
|
-
const child = {
|
|
444
|
-
stdout: { on: () => {} },
|
|
445
|
-
stderr: { on: () => {} },
|
|
446
|
-
on: (ev, cb) => {
|
|
447
|
-
if (ev === 'close') cb(0);
|
|
448
|
-
},
|
|
449
|
-
};
|
|
450
|
-
return child;
|
|
451
|
-
},
|
|
452
|
-
}));
|
|
453
|
-
const { run: mockedRun } = await import('../index.js');
|
|
454
|
-
const res = await mockedRun('anything', {
|
|
455
|
-
cwd: '/cwd',
|
|
456
|
-
maxLines: 5,
|
|
457
|
-
shell: false,
|
|
458
|
-
});
|
|
459
|
-
expect(captured.cwd).toBe('/cwd');
|
|
460
|
-
expect(captured.maxLines).toBeUndefined();
|
|
461
786
|
expect(captured.shell).toBe(false);
|
|
787
|
+
expect(captured.stdio).toBe('pipe');
|
|
788
|
+
expect(captured.maxLines).toBeUndefined(); // maxLines should not be forwarded
|
|
462
789
|
expect(res.code).toBe(0);
|
|
463
790
|
vi.resetModules();
|
|
464
791
|
});
|
|
@@ -1104,18 +1431,6 @@ describe('run() - enhanced edge cases', () => {
|
|
|
1104
1431
|
});
|
|
1105
1432
|
|
|
1106
1433
|
describe('onExit() - enhanced edge cases', () => {
|
|
1107
|
-
it('handles callback that returns a value (not a promise)', async () => {
|
|
1108
|
-
const cb = vi.fn(() => 42);
|
|
1109
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
1110
|
-
const off = onExit(cb);
|
|
1111
|
-
process.emit('SIGINT');
|
|
1112
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
1113
|
-
expect(cb).toHaveBeenCalled();
|
|
1114
|
-
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
1115
|
-
off();
|
|
1116
|
-
exitSpy.mockRestore();
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
1434
|
it('handles callback throwing synchronously', async () => {
|
|
1120
1435
|
const cb = vi.fn(() => {
|
|
1121
1436
|
throw new Error('sync error');
|
|
@@ -1175,7 +1490,7 @@ describe('onExit() - enhanced edge cases', () => {
|
|
|
1175
1490
|
});
|
|
1176
1491
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
1177
1492
|
const off = onExit(cb);
|
|
1178
|
-
process.emit('
|
|
1493
|
+
process.emit('SIGTERM'); // Test different signal
|
|
1179
1494
|
await new Promise((r) => setTimeout(r, 15));
|
|
1180
1495
|
expect(cb).toHaveBeenCalled();
|
|
1181
1496
|
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
@@ -1183,6 +1498,18 @@ describe('onExit() - enhanced edge cases', () => {
|
|
|
1183
1498
|
exitSpy.mockRestore();
|
|
1184
1499
|
});
|
|
1185
1500
|
|
|
1501
|
+
it('responds to SIGQUIT signal', async () => {
|
|
1502
|
+
const cb = vi.fn(async () => {});
|
|
1503
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
|
|
1504
|
+
const off = onExit(cb);
|
|
1505
|
+
process.emit('SIGQUIT');
|
|
1506
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1507
|
+
expect(cb).toHaveBeenCalled();
|
|
1508
|
+
expect(exitSpy).toHaveBeenCalledWith(0);
|
|
1509
|
+
off();
|
|
1510
|
+
exitSpy.mockRestore();
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1186
1513
|
it('unsubscribe can be called multiple times safely', () => {
|
|
1187
1514
|
const cb = vi.fn();
|
|
1188
1515
|
const off = onExit(cb);
|
package/example.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as maker from './index.js';
|
|
2
|
-
|
|
3
|
-
(async () => {
|
|
4
|
-
maker.init();
|
|
5
|
-
|
|
6
|
-
const spinner = await maker.spinner('Testing maker.run()...').start();
|
|
7
|
-
|
|
8
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
9
|
-
|
|
10
|
-
await maker.run('echo "Hello, World!"');
|
|
11
|
-
|
|
12
|
-
spinner.succeed('Done!');
|
|
13
|
-
})();
|