@stonyx/utils 0.2.3-alpha.2 → 0.2.3-alpha.20

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,3 +1,7 @@
1
+ [![CI](https://github.com/abofs/stonyx-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-utils/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/utils.svg)](https://www.npmjs.com/package/@stonyx/utils)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+
1
5
  # stonyx-utils
2
6
 
3
7
  Utilities module for the Stonyx Framework. Provides helpers for files, objects, strings, dates, and promises.
@@ -25,10 +29,13 @@ Utilities module for the Stonyx Framework. Provides helpers for files, objects,
25
29
  | | `getOrSet` | Get or set value in a Map. |
26
30
  | **String** | `kebabCaseToCamelCase` | Convert kebab-case to camelCase. |
27
31
  | | `kebabCaseToPascalCase` | Convert kebab-case to PascalCase. |
32
+ | | `camelCaseToKebabCase` | Convert camelCase to kebab-case. |
28
33
  | | `generateRandomString` | Generate a random alphanumeric string. |
29
34
  | | `pluralize` | Return plural form of English nouns. |
30
35
  | **Date** | `getTimestamp` | Return current UNIX timestamp in seconds. |
31
36
  | **Promise** | `sleep` | Async delay for a given number of seconds. |
37
+ | **Prompt** | `confirm` | Prompt user for y/N confirmation. |
38
+ | | `prompt` | Prompt user for free-text input. |
32
39
 
33
40
  ---
34
41
 
@@ -41,6 +48,7 @@ Utilities module for the Stonyx Framework. Provides helpers for files, objects,
41
48
  * [String Utils](#string-utils)
42
49
  * [Date Utils](#date-utils)
43
50
  * [Promise Utils](#promise-utils)
51
+ * [Prompt Utils](#prompt-utils)
44
52
  * [License](#license)
45
53
 
46
54
  ---
@@ -107,6 +115,9 @@ Dynamically imports all `.js` files in a directory and calls `callback(exports,
107
115
  | `fullExport` | Boolean | false | If true, callback receives all exports, not just default. |
108
116
  | `rawName` | Boolean | false | If true, the file name is not converted to camelCase. |
109
117
  | `ignoreAccessFailure` | Boolean | false | If true, directory access errors are ignored. |
118
+ | `recursive` | Boolean | false | If true, recurse into subdirectories. |
119
+ | `recursiveNaming` | Boolean | false | If true, prefix imported names with their directory path. |
120
+ | `namePrefix` | String | `""` | Manual prefix prepended to each imported name. |
110
121
 
111
122
  Example:
112
123
 
@@ -221,6 +232,35 @@ await sleep(2); // waits 2 seconds
221
232
 
222
233
  ---
223
234
 
235
+ ## Prompt Utils
236
+
237
+ Interactive CLI prompt helpers built on Node's `readline`.
238
+
239
+ ### Functions
240
+
241
+ #### `confirm(question, options={})`
242
+
243
+ Prompts the user with `(y/N)` and resolves to `true` only if the answer is `"y"` (case-insensitive).
244
+
245
+ * `options.input` — Readable stream (default: `process.stdin`).
246
+ * `options.output` — Writable stream (default: `process.stdout`).
247
+
248
+ #### `prompt(question, options={})`
249
+
250
+ Prompts the user with a question and resolves to the trimmed input string.
251
+
252
+ * `options.input` — Readable stream (default: `process.stdin`).
253
+ * `options.output` — Writable stream (default: `process.stdout`).
254
+
255
+ ```js
256
+ import { confirm, prompt } from '@stonyx/utils/prompt';
257
+
258
+ const name = await prompt('What is your name?');
259
+ const ok = await confirm('Proceed?');
260
+ ```
261
+
262
+ ---
263
+
224
264
  ## License
225
265
 
226
266
  Apache — do what you want, just keep attribution.
package/dist/date.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getTimestamp(dateObject?: Date | null): number;
package/dist/date.js ADDED
@@ -0,0 +1,4 @@
1
+ export function getTimestamp(dateObject = null) {
2
+ const ts = dateObject ? dateObject.getTime() : Date.now();
3
+ return Math.floor(ts / 1000);
4
+ }
package/dist/file.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ interface FileOptions {
2
+ json?: boolean;
3
+ }
4
+ interface ReadFileOptions extends FileOptions {
5
+ missingFileCallback?: (filePath: string) => string | Record<string, unknown>;
6
+ }
7
+ interface DeleteFileOptions {
8
+ ignoreAccessFailure?: boolean;
9
+ }
10
+ interface ForEachFileImportOptions {
11
+ ignoreAccessFailure?: boolean;
12
+ recursive?: boolean;
13
+ recursiveNaming?: boolean;
14
+ rawName?: boolean;
15
+ namePrefix?: string;
16
+ fullExport?: boolean;
17
+ }
18
+ interface FileImportMeta {
19
+ name: string;
20
+ stats: import('fs').Stats;
21
+ path: string;
22
+ }
23
+ export declare function createFile(filePath: string, data: string | Record<string, unknown>, options?: FileOptions): Promise<void>;
24
+ export declare function updateFile(filePath: string, data: string | Record<string, unknown>, options?: FileOptions): Promise<void>;
25
+ interface CopyFileOptions {
26
+ overwrite?: boolean;
27
+ }
28
+ export declare function copyFile(sourcePath: string, targetPath: string, options?: CopyFileOptions): Promise<boolean>;
29
+ export declare function readFile(filePath: string, options: ReadFileOptions & {
30
+ json: true;
31
+ }): Promise<Record<string, unknown>>;
32
+ export declare function readFile(filePath: string, options?: ReadFileOptions): Promise<string>;
33
+ export declare function deleteFile(filePath: string, options?: DeleteFileOptions): Promise<void>;
34
+ export declare function deleteDirectory(dir: string): Promise<void>;
35
+ export declare function createDirectory(dir: string): Promise<void>;
36
+ export declare function forEachFileImport(dir: string, callback: (output: unknown, meta: FileImportMeta) => void | Promise<void>, options?: ForEachFileImportOptions): Promise<void>;
37
+ export declare function fileExists(filePath: string): Promise<boolean>;
38
+ export {};
package/dist/file.js ADDED
@@ -0,0 +1,135 @@
1
+ import { getTimestamp } from './date.js';
2
+ import { kebabCaseToCamelCase } from './string.js';
3
+ import { objToJson } from './object.js';
4
+ import { promises as fsp } from 'fs';
5
+ import path from 'path';
6
+ function isNodeError(error) {
7
+ return error instanceof Error && 'code' in error;
8
+ }
9
+ export async function createFile(filePath, data, options = {}) {
10
+ try {
11
+ filePath = path.resolve(filePath);
12
+ await createDirectory(path.dirname(filePath));
13
+ await fsp.writeFile(filePath, options.json ? objToJson(data) : String(data), 'utf8');
14
+ }
15
+ catch (error) {
16
+ throw error instanceof Error ? error : new Error(String(error));
17
+ }
18
+ }
19
+ export async function updateFile(filePath, data, options = {}) {
20
+ try {
21
+ await fsp.access(filePath);
22
+ const swapFile = `${filePath}.temp-${getTimestamp()}`;
23
+ await fsp.writeFile(swapFile, options.json ? objToJson(data) : String(data), 'utf8');
24
+ await fsp.rename(swapFile, filePath);
25
+ }
26
+ catch (error) {
27
+ throw error instanceof Error ? error : new Error(String(error));
28
+ }
29
+ }
30
+ export async function copyFile(sourcePath, targetPath, options = {}) {
31
+ try {
32
+ sourcePath = path.resolve(sourcePath);
33
+ targetPath = path.resolve(targetPath);
34
+ await fsp.access(sourcePath);
35
+ }
36
+ catch (error) {
37
+ throw error instanceof Error ? error : new Error(String(error));
38
+ }
39
+ try {
40
+ await fsp.access(targetPath);
41
+ if (!options.overwrite)
42
+ return false;
43
+ }
44
+ catch (error) {
45
+ if (isNodeError(error) && error.code === 'ENOENT') { /* file doesn't exist — proceed with copy */ }
46
+ else
47
+ throw error;
48
+ }
49
+ try {
50
+ await fsp.copyFile(sourcePath, targetPath);
51
+ }
52
+ catch (error) {
53
+ throw error instanceof Error ? error : new Error(String(error));
54
+ }
55
+ return true;
56
+ }
57
+ export async function readFile(filePath, options = {}) {
58
+ try {
59
+ filePath = path.resolve(filePath);
60
+ await fsp.access(filePath);
61
+ const fileData = await fsp.readFile(filePath, 'utf8');
62
+ return options.json ? JSON.parse(fileData) : fileData;
63
+ }
64
+ catch (error) {
65
+ const { missingFileCallback } = options;
66
+ if (isNodeError(error) && error.code === 'ENOENT' && missingFileCallback) {
67
+ return missingFileCallback(filePath);
68
+ }
69
+ throw error instanceof Error ? error : new Error(String(error));
70
+ }
71
+ }
72
+ export async function deleteFile(filePath, options) {
73
+ try {
74
+ filePath = path.resolve(filePath);
75
+ await fsp.access(filePath);
76
+ }
77
+ catch (error) {
78
+ if (options?.ignoreAccessFailure)
79
+ return;
80
+ throw error;
81
+ }
82
+ await fsp.unlink(filePath);
83
+ }
84
+ export async function deleteDirectory(dir) {
85
+ await fsp.rm(dir, { recursive: true, force: true });
86
+ }
87
+ export async function createDirectory(dir) {
88
+ await fsp.mkdir(dir, { recursive: true });
89
+ }
90
+ export async function forEachFileImport(dir, callback, options = {}) {
91
+ if (typeof callback !== 'function')
92
+ throw new Error('Callback must be valid function');
93
+ try {
94
+ await fsp.access(dir);
95
+ }
96
+ catch (error) {
97
+ if (!options.ignoreAccessFailure)
98
+ throw new Error(`Unable to access directory: ${dir}`);
99
+ return;
100
+ }
101
+ const files = await fsp.readdir(dir);
102
+ for (const file of files) {
103
+ const filePath = path.join(dir, file);
104
+ const stats = await fsp.stat(filePath);
105
+ if (options.recursive && stats.isDirectory()) {
106
+ const newOptions = { ...options };
107
+ if (options.recursiveNaming) {
108
+ const pathPrefix = options.rawName ? file : `${kebabCaseToCamelCase(file)}`;
109
+ newOptions.namePrefix = options.namePrefix ? `${options.namePrefix}${pathPrefix}/` : `${pathPrefix}/`;
110
+ }
111
+ await forEachFileImport(filePath, callback, newOptions);
112
+ continue;
113
+ }
114
+ if (!stats.isFile() || !file.endsWith('.js'))
115
+ continue;
116
+ const prefix = process.platform === 'win32' ? 'file://' : '';
117
+ const rawName = file.replace('.js', '');
118
+ let name = options.rawName ? rawName : kebabCaseToCamelCase(rawName);
119
+ if (options.namePrefix)
120
+ name = `${options.namePrefix}${name}`;
121
+ const exported = await import(prefix + path.resolve(filePath));
122
+ const output = !options.fullExport ? exported.default : exported;
123
+ callback(output, { name, stats, path: filePath });
124
+ }
125
+ }
126
+ export async function fileExists(filePath) {
127
+ try {
128
+ filePath = path.resolve(filePath);
129
+ await fsp.access(filePath);
130
+ return true;
131
+ }
132
+ catch {
133
+ return false;
134
+ }
135
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Generic fuzzy string matching for cross-source reconciliation.
3
+ * Handles Unicode normalization, stop-word filtering, and word-set similarity scoring.
4
+ */
5
+ export interface FuzzyMatchOptions {
6
+ stopWords?: string[];
7
+ delimiter?: string;
8
+ threshold?: number;
9
+ }
10
+ export interface FuzzyMatchResult<T extends {
11
+ name: string;
12
+ }> {
13
+ entry: T;
14
+ score: number;
15
+ }
16
+ export default class FuzzyMatch {
17
+ stopWords: string[];
18
+ delimiter: string;
19
+ threshold: number;
20
+ constructor(options?: FuzzyMatchOptions);
21
+ normalize(name: string): string;
22
+ similarity(nameA: string, nameB: string): number;
23
+ findBestMatch<T extends {
24
+ name: string;
25
+ }>(nameA: string, nameB: string, entries: T[], threshold?: number): FuzzyMatchResult<T> | null;
26
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Generic fuzzy string matching for cross-source reconciliation.
3
+ * Handles Unicode normalization, stop-word filtering, and word-set similarity scoring.
4
+ */
5
+ function normalizeString(name, stopWords = []) {
6
+ let result = name
7
+ .normalize('NFD')
8
+ .replace(/[\u0300-\u036f]/g, '')
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9\s]/g, ' ');
11
+ if (stopWords.length) {
12
+ const pattern = new RegExp(`\\b(${stopWords.join('|')})\\b`, 'g');
13
+ result = result.replace(pattern, '');
14
+ }
15
+ return result.replace(/\s+/g, ' ').trim();
16
+ }
17
+ function wordSet(normalized) {
18
+ return new Set(normalized.split(' ').filter(w => w.length > 1));
19
+ }
20
+ export default class FuzzyMatch {
21
+ stopWords;
22
+ delimiter;
23
+ threshold;
24
+ constructor(options = {}) {
25
+ this.stopWords = options.stopWords || [];
26
+ this.delimiter = options.delimiter || '\u00B7';
27
+ this.threshold = options.threshold || 0.35;
28
+ }
29
+ normalize(name) {
30
+ return normalizeString(name, this.stopWords);
31
+ }
32
+ similarity(nameA, nameB) {
33
+ const aN = this.normalize(nameA);
34
+ const sN = this.normalize(nameB);
35
+ if (!aN || !sN)
36
+ return 0;
37
+ if (aN === sN)
38
+ return 1.0;
39
+ if (aN.includes(sN) || sN.includes(aN))
40
+ return 0.9;
41
+ const aWords = wordSet(aN);
42
+ const sWords = wordSet(sN);
43
+ if (aWords.size === 0 || sWords.size === 0)
44
+ return 0;
45
+ let overlap = 0;
46
+ for (const w of aWords) {
47
+ if (sWords.has(w)) {
48
+ overlap++;
49
+ }
50
+ else {
51
+ for (const sw of sWords) {
52
+ if (sw.startsWith(w) || w.startsWith(sw)) {
53
+ overlap += 0.7;
54
+ break;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return overlap / Math.max(aWords.size, sWords.size);
60
+ }
61
+ findBestMatch(nameA, nameB, entries, threshold) {
62
+ const minScore = threshold ?? this.threshold;
63
+ let bestMatch = null;
64
+ let bestScore = 0;
65
+ for (const entry of entries) {
66
+ const parts = entry.name.split(this.delimiter);
67
+ if (parts.length !== 2)
68
+ continue;
69
+ const [entryA, entryB] = parts;
70
+ const normalScore = (this.similarity(nameA, entryA) + this.similarity(nameB, entryB)) / 2;
71
+ const reversedScore = (this.similarity(nameA, entryB) + this.similarity(nameB, entryA)) / 2;
72
+ const score = Math.max(normalScore, reversedScore);
73
+ if (score > bestScore) {
74
+ bestScore = score;
75
+ bestMatch = entry;
76
+ }
77
+ }
78
+ return bestScore >= minScore && bestMatch ? { entry: bestMatch, score: bestScore } : null;
79
+ }
80
+ }
@@ -0,0 +1,14 @@
1
+ export declare function deepCopy<T>(obj: T): T;
2
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export declare function objToJson(obj: JsonValue | Record<string, unknown>, format?: string | number): string;
6
+ export declare function makeArray<T>(obj: T | T[]): T[];
7
+ interface MergeOptions {
8
+ ignoreNewKeys?: boolean;
9
+ }
10
+ export declare function mergeObject(obj1: Record<string, unknown>, obj2: Record<string, unknown>, options?: MergeOptions): Record<string, unknown>;
11
+ export declare function get(obj: Record<string, unknown>, path: string): unknown;
12
+ export declare function get(obj: unknown, path?: unknown): undefined;
13
+ export declare function getOrSet<K, V>(map: Map<K, V>, key: K, defaultValue: V | (() => V)): V;
14
+ export {};
package/dist/object.js ADDED
@@ -0,0 +1,60 @@
1
+ export function deepCopy(obj) {
2
+ return JSON.parse(JSON.stringify(obj));
3
+ }
4
+ export function objToJson(obj, format = '\t') {
5
+ return JSON.stringify(obj, null, format);
6
+ }
7
+ export function makeArray(obj) {
8
+ return Array.isArray(obj) ? obj : [obj];
9
+ }
10
+ function cloneShallow(value) {
11
+ if (Array.isArray(value))
12
+ return value.slice();
13
+ if (value && typeof value === 'object')
14
+ return { ...value };
15
+ return value;
16
+ }
17
+ export function mergeObject(obj1, obj2, options = {}) {
18
+ if (Array.isArray(obj1) || Array.isArray(obj2))
19
+ throw new Error('Cannot merge arrays.');
20
+ if (obj1 === null || typeof obj1 !== 'object')
21
+ return cloneShallow(obj2);
22
+ if (obj2 === null || typeof obj2 !== 'object')
23
+ return cloneShallow(obj1);
24
+ const result = {};
25
+ for (const key of Object.keys(obj1))
26
+ result[key] = cloneShallow(obj1[key]);
27
+ for (const key of Object.keys(obj2)) {
28
+ if (options.ignoreNewKeys && !(key in obj1))
29
+ continue;
30
+ const val1 = obj1[key];
31
+ const val2 = obj2[key];
32
+ const shouldMerge = val1 && val2 && typeof val1 === 'object' && typeof val2 === 'object' && !Array.isArray(val1) && !Array.isArray(val2);
33
+ result[key] = shouldMerge ? mergeObject(val1, val2, options) : cloneShallow(val2);
34
+ }
35
+ return result;
36
+ }
37
+ export function get(obj, path) {
38
+ if (arguments.length !== 2)
39
+ return console.error('Get must be called with two arguments; an object and a property key.');
40
+ if (!obj)
41
+ return console.error(`Cannot call get with '${path}' on an undefined object.`);
42
+ if (typeof path !== 'string')
43
+ return console.error('The path provided to get must be a string.');
44
+ let current = obj;
45
+ for (const key of path.split('.')) {
46
+ if (current[key] === undefined)
47
+ return;
48
+ current = current[key];
49
+ }
50
+ return current;
51
+ }
52
+ export function getOrSet(map, key, defaultValue) {
53
+ if (!(map instanceof Map))
54
+ throw new Error('First argument to getOrSet must be a Map.');
55
+ if (!map.has(key)) {
56
+ const value = typeof defaultValue === 'function' ? defaultValue() : defaultValue;
57
+ map.set(key, value);
58
+ }
59
+ return map.get(key);
60
+ }
@@ -0,0 +1 @@
1
+ export default function pluralize(word: string): string;
@@ -0,0 +1,87 @@
1
+ // --- Irregular nouns ---
2
+ const irregular = {
3
+ person: 'people',
4
+ man: 'men',
5
+ woman: 'women',
6
+ child: 'children',
7
+ tooth: 'teeth',
8
+ foot: 'feet',
9
+ mouse: 'mice',
10
+ goose: 'geese',
11
+ ox: 'oxen',
12
+ cactus: 'cacti',
13
+ nucleus: 'nuclei',
14
+ syllabus: 'syllabi',
15
+ focus: 'foci',
16
+ fungus: 'fungi',
17
+ appendix: 'appendices',
18
+ index: 'indices',
19
+ criterion: 'criteria',
20
+ phenomenon: 'phenomena',
21
+ die: 'dice',
22
+ thesis: 'theses',
23
+ analysis: 'analyses',
24
+ crisis: 'crises',
25
+ radius: 'radii',
26
+ corpus: 'corpora',
27
+ };
28
+ // --- Uncountables ---
29
+ const uncountable = new Set([
30
+ 'sheep', 'fish', 'deer', 'series', 'species', 'news', 'information',
31
+ 'rice', 'moose', 'bison', 'salmon', 'aircraft', 'offspring'
32
+ ]);
33
+ // --- Exceptions ---
34
+ const fExceptions = new Set(['chief', 'roof', 'belief', 'chef', 'cliff', 'reef', 'proof', 'brief']);
35
+ // Keep only true irregular -o exceptions (consonant + o but take just "s")
36
+ const oExceptions = new Set(['piano', 'photo', 'halo', 'canto', 'solo']);
37
+ // --- Utility to preserve casing ---
38
+ function applyCasing(original, plural) {
39
+ if (original === original.toUpperCase())
40
+ return plural.toUpperCase();
41
+ if (original === original.toLowerCase())
42
+ return plural.toLowerCase();
43
+ if (original[0] === original[0].toUpperCase()) {
44
+ return plural.charAt(0).toUpperCase() + plural.slice(1);
45
+ }
46
+ return plural;
47
+ }
48
+ // --- Rule-based pluralization ---
49
+ const rules = [
50
+ // quiz -> quizzes, waltz -> waltzes, topaz -> topazes
51
+ [/z$/i, w => (/iz$/i.test(w) ? w + 'zes' : w + 'es')],
52
+ // bus -> buses, box -> boxes, church -> churches, but stomach -> stomachs (exclude -ach)
53
+ [/(s|x|ch|sh)$/i, w => (/ach$/i.test(w) ? w + 's' : w + 'es')],
54
+ // vowel + y -> +s (key -> keys)
55
+ [/[aeiou]y$/i, w => w + 's'],
56
+ // consonant + y -> -ies (city -> cities)
57
+ [/y$/i, w => w.slice(0, -1) + 'ies'],
58
+ // -fe -> -ves (knife -> knives), but not chief/roof/etc
59
+ [/fe$/i, w => (fExceptions.has(w) ? w + 's' : w.slice(0, -2) + 'ves')],
60
+ // -f -> -ves (wolf -> wolves), but not cliff/etc
61
+ [/f$/i, w => (fExceptions.has(w) ? w + 's' : w.slice(0, -1) + 'ves')],
62
+ // -sis -> -ses (analysis -> analyses, thesis -> theses)
63
+ [/sis$/i, w => w.slice(0, -2) + 'ses'],
64
+ // vowel + o -> +s (zoo -> zoos, video -> videos, patio -> patios)
65
+ [/[aeiou]o$/i, w => w + 's'],
66
+ // consonant + o -> usually +es, unless in oExceptions
67
+ [/o$/i, w => (oExceptions.has(w) ? w + 's' : w + 'es')],
68
+ // default: just +s
69
+ [/$/i, w => w + 's']
70
+ ];
71
+ // --- Exported pluralizer ---
72
+ export default function pluralize(word) {
73
+ if (typeof word !== 'string' || !/^[a-zA-Z]+$/.test(word))
74
+ return word;
75
+ const lower = word.toLowerCase();
76
+ if (uncountable.has(lower))
77
+ return word;
78
+ if (irregular[lower]) {
79
+ return applyCasing(word, irregular[lower]);
80
+ }
81
+ for (const [pattern, transform] of rules) {
82
+ if (pattern.test(lower)) {
83
+ return applyCasing(word, transform(lower));
84
+ }
85
+ }
86
+ return word; // fallback (shouldn't hit)
87
+ }
@@ -0,0 +1 @@
1
+ export declare function sleep(seconds: number): Promise<void>;
@@ -0,0 +1,5 @@
1
+ export async function sleep(seconds) {
2
+ return new Promise(resolve => {
3
+ setTimeout(resolve, 1000 * seconds);
4
+ });
5
+ }
@@ -0,0 +1,8 @@
1
+ import type { Readable, Writable } from 'stream';
2
+ interface PromptOptions {
3
+ input?: Readable;
4
+ output?: Writable;
5
+ }
6
+ export declare function confirm(question: string, { input, output }?: PromptOptions): Promise<boolean>;
7
+ export declare function prompt(question: string, { input, output }?: PromptOptions): Promise<string>;
8
+ export {};
package/dist/prompt.js ADDED
@@ -0,0 +1,25 @@
1
+ import { createInterface } from 'readline';
2
+ export function confirm(question, { input, output } = {}) {
3
+ const rl = createInterface({
4
+ input: input ?? process.stdin,
5
+ output: output ?? process.stdout,
6
+ });
7
+ return new Promise(resolve => {
8
+ rl.question(`${question} (y/N) `, (answer) => {
9
+ rl.close();
10
+ resolve(answer.trim().toLowerCase() === 'y');
11
+ });
12
+ });
13
+ }
14
+ export function prompt(question, { input, output } = {}) {
15
+ const rl = createInterface({
16
+ input: input ?? process.stdin,
17
+ output: output ?? process.stdout,
18
+ });
19
+ return new Promise(resolve => {
20
+ rl.question(`${question} `, (answer) => {
21
+ rl.close();
22
+ resolve(answer.trim());
23
+ });
24
+ });
25
+ }
@@ -0,0 +1,5 @@
1
+ export declare function kebabCaseToCamelCase(str: string): string;
2
+ export declare function kebabCaseToPascalCase(str: string): string;
3
+ export declare function camelCaseToKebabCase(str: string): string;
4
+ export declare function generateRandomString(length?: number): string;
5
+ export { default as pluralize } from './plurarize.js';
package/dist/string.js ADDED
@@ -0,0 +1,32 @@
1
+ function kebabToCase(str, pascal = false) {
2
+ let out = '';
3
+ let upperNext = pascal; // PascalCase starts uppercase
4
+ for (let i = 0; i < str.length; i++) {
5
+ const ch = str.charAt(i);
6
+ if (ch === '-') {
7
+ upperNext = true;
8
+ }
9
+ else if (upperNext) {
10
+ out += ch.toUpperCase();
11
+ upperNext = false;
12
+ }
13
+ else {
14
+ out += ch;
15
+ }
16
+ }
17
+ return out;
18
+ }
19
+ export function kebabCaseToCamelCase(str) {
20
+ return kebabToCase(str, false);
21
+ }
22
+ export function kebabCaseToPascalCase(str) {
23
+ return kebabToCase(str, true);
24
+ }
25
+ export function camelCaseToKebabCase(str) {
26
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
27
+ }
28
+ export function generateRandomString(length = 8) {
29
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
30
+ return Array(length).fill('').map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
31
+ }
32
+ export { default as pluralize } from './plurarize.js';
package/package.json CHANGED
@@ -3,19 +3,46 @@
3
3
  "keywords": [
4
4
  "stonyx-module"
5
5
  ],
6
- "version": "0.2.3-alpha.2",
6
+ "version": "0.2.3-alpha.20",
7
7
  "description": "Utils module for Stonyx Framework",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/abofs/stonyx-utils.git"
11
11
  },
12
12
  "type": "module",
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
13
17
  "exports": {
14
- "./date": "./src/date.js",
15
- "./object": "./src/object.js",
16
- "./file": "./src/file.js",
17
- "./promise": "./src/promise.js",
18
- "./string": "./src/string.js"
18
+ "./date": {
19
+ "types": "./dist/date.d.ts",
20
+ "default": "./dist/date.js"
21
+ },
22
+ "./object": {
23
+ "types": "./dist/object.d.ts",
24
+ "default": "./dist/object.js"
25
+ },
26
+ "./file": {
27
+ "types": "./dist/file.d.ts",
28
+ "default": "./dist/file.js"
29
+ },
30
+ "./promise": {
31
+ "types": "./dist/promise.d.ts",
32
+ "default": "./dist/promise.js"
33
+ },
34
+ "./prompt": {
35
+ "types": "./dist/prompt.d.ts",
36
+ "default": "./dist/prompt.js"
37
+ },
38
+ "./string": {
39
+ "types": "./dist/string.d.ts",
40
+ "default": "./dist/string.js"
41
+ },
42
+ "./fuzzy-match": {
43
+ "types": "./dist/fuzzy-match.d.ts",
44
+ "default": "./dist/fuzzy-match.js"
45
+ }
19
46
  },
20
47
  "publishConfig": {
21
48
  "access": "public",
@@ -27,12 +54,17 @@
27
54
  "Stone Costa <stone.costa@synamicd.com>"
28
55
  ],
29
56
  "devDependencies": {
57
+ "@types/node": "^25.5.2",
58
+ "@types/qunit": "^2.19.13",
59
+ "@types/sinon": "^21.0.1",
30
60
  "fs": "^0.0.1-security",
31
61
  "qunit": "^2.24.1",
32
- "sinon": "^21.0.0"
62
+ "sinon": "^21.0.0",
63
+ "typescript": "^5.8.3"
33
64
  },
34
- "dependencies": {},
35
65
  "scripts": {
36
- "test": "qunit"
66
+ "build": "tsc",
67
+ "build:test": "tsc -p tsconfig.test.json",
68
+ "test": "pnpm build && pnpm build:test && qunit 'dist-test/test/**/*.js'"
37
69
  }
38
70
  }
@@ -1,16 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches: [dev, main]
6
-
7
- concurrency:
8
- group: ci-${{ github.head_ref || github.ref }}
9
- cancel-in-progress: true
10
-
11
- permissions:
12
- contents: read
13
-
14
- jobs:
15
- test:
16
- uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
@@ -1,35 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- version-type:
7
- description: 'Version type'
8
- required: true
9
- type: choice
10
- options:
11
- - patch
12
- - minor
13
- - major
14
- custom-version:
15
- description: 'Custom version (optional, overrides version-type)'
16
- required: false
17
- type: string
18
- pull_request:
19
- types: [opened, synchronize, reopened]
20
- branches: [main, dev]
21
- push:
22
- branches: [main]
23
-
24
- permissions:
25
- contents: write
26
- id-token: write
27
- pull-requests: write
28
-
29
- jobs:
30
- publish:
31
- uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
32
- with:
33
- version-type: ${{ github.event.inputs.version-type }}
34
- custom-version: ${{ github.event.inputs.custom-version }}
35
- secrets: inherit
package/src/date.js DELETED
@@ -1,5 +0,0 @@
1
- export function getTimestamp(dateObject=null) {
2
- const ts = dateObject ? dateObject.getTime() : Date.now();
3
-
4
- return Math.floor(ts / 1000);
5
- }
package/src/file.js DELETED
@@ -1,145 +0,0 @@
1
- import { getTimestamp } from '@stonyx/utils/date';
2
- import { kebabCaseToCamelCase } from '@stonyx/utils/string';
3
- import { objToJson } from '@stonyx/utils/object';
4
- import { promises as fsp } from 'fs';
5
- import path from 'path';
6
-
7
- export async function createFile(filePath, data, options={}) {
8
- try {
9
- filePath = path.resolve(filePath);
10
-
11
- await createDirectory(path.dirname(filePath));
12
- await fsp.writeFile(filePath, options.json ? objToJson(data) : data, 'utf8');
13
- } catch (error) {
14
- throw new Error(error);
15
- }
16
- }
17
-
18
- export async function updateFile(filePath, data, options={}) {
19
- try {
20
- await fsp.access(filePath);
21
-
22
- const swapFile = `${filePath}.temp-${getTimestamp()}`;
23
- await fsp.writeFile(swapFile, options.json ? objToJson(data) : data);
24
- await fsp.rename(swapFile, filePath);
25
- } catch (error) {
26
-
27
- throw new Error(error);
28
- }
29
- }
30
-
31
- export async function copyFile(sourcePath, targetPath, options={}) {
32
- try {
33
- sourcePath = path.resolve(sourcePath);
34
- targetPath = path.resolve(targetPath);
35
- await fsp.access(sourcePath);
36
- } catch (error) {
37
- throw new Error(error);
38
- }
39
-
40
- try {
41
- await fsp.access(targetPath);
42
- if (!options.overwrite) return false;
43
- } catch {}
44
-
45
- try {
46
- await fsp.copyFile(sourcePath, targetPath);
47
- } catch (error) {
48
- throw new Error(error);
49
- }
50
-
51
- return true;
52
- }
53
-
54
- export async function readFile(filePath, options={}) {
55
- try {
56
- filePath = path.resolve(filePath);
57
-
58
- await fsp.access(filePath);
59
- const fileData = await fsp.readFile(filePath, 'utf8');
60
-
61
- return options.json ? JSON.parse(fileData) : fileData;
62
- } catch (error) {
63
- const { missingFileCallback } = options;
64
-
65
- if (error.code === 'ENOENT' && missingFileCallback) {
66
- return missingFileCallback(filePath);
67
- }
68
-
69
- throw new Error(error);
70
- }
71
- }
72
-
73
- export async function deleteFile(filePath, options) {
74
- try {
75
- filePath = path.resolve(filePath);
76
-
77
- await fsp.access(filePath);
78
- } catch (error) {
79
- if (options?.ignoreAccessFailure) return;
80
- throw error;
81
- }
82
-
83
- await fsp.unlink(filePath);
84
- }
85
-
86
- export async function deleteDirectory(dir) {
87
- await fsp.rm(dir, { recursive: true, force: true });
88
- }
89
-
90
- export async function createDirectory(dir) {
91
- await fsp.mkdir(dir, { recursive: true });
92
- }
93
-
94
- export async function forEachFileImport(dir, callback, options={}) {
95
- if (typeof callback !== 'function') throw new Error('Callback must be valid function');
96
-
97
- try {
98
- await fsp.access(dir);
99
- } catch (error) {
100
- if (!options.ignoreAccessFailure) throw new Error(`Unable to access directory: ${dir}`);
101
- return;
102
- }
103
-
104
- const files = await fsp.readdir(dir);
105
-
106
- for (const file of files) {
107
- const filePath = path.join(dir, file);
108
- const stats = await fsp.stat(filePath);
109
-
110
- if (options.recursive && stats.isDirectory()) {
111
- const newOptions = { ...options };
112
-
113
- if (options.recursiveNaming) {
114
- const pathPrefix = options.rawName ? file : `${kebabCaseToCamelCase(file)}`;
115
- newOptions.namePrefix = options.namePrefix ? `${options.namePrefix}${pathPrefix}/` : `${pathPrefix}/`;
116
- }
117
-
118
- await forEachFileImport(filePath, callback, newOptions);
119
- continue;
120
- }
121
-
122
- if (!stats.isFile() || !file.endsWith('.js')) continue;
123
-
124
- const prefix = process.platform === 'win32' ? 'file://' : '';
125
- const rawName = file.replace('.js', '');
126
- let name = options.rawName ? rawName : kebabCaseToCamelCase(rawName);
127
-
128
- if (options.namePrefix) name = `${options.namePrefix}${name}`;
129
-
130
- const exported = await import(prefix + path.resolve(filePath));
131
- const output = !options.fullExport ? exported.default : exported;
132
-
133
- callback(output, { name, stats, path: filePath });
134
- }
135
- }
136
-
137
- export async function fileExists(filePath) {
138
- try {
139
- filePath = path.resolve(filePath);
140
- await fsp.access(filePath);
141
- return true;
142
- } catch (error) {
143
- return false;
144
- }
145
- }
package/src/object.js DELETED
@@ -1,61 +0,0 @@
1
- export function deepCopy(obj) {
2
- return JSON.parse(JSON.stringify(obj));
3
- }
4
-
5
- export function objToJson(obj, format='\t') {
6
- return JSON.stringify(obj, null, format);
7
- }
8
-
9
- export function makeArray(obj) {
10
- return Array.isArray(obj) ? obj : [obj];
11
- }
12
-
13
- function cloneShallow(value) {
14
- if (Array.isArray(value)) return value.slice();
15
- if (value && typeof value === 'object') return { ...value };
16
- return value;
17
- }
18
-
19
- export function mergeObject(obj1, obj2, options={}) {
20
- if (Array.isArray(obj1) || Array.isArray(obj2)) throw new Error('Cannot merge arrays.');
21
-
22
- if (obj1 === null || typeof obj1 !== 'object') return cloneShallow(obj2);
23
- if (obj2 === null || typeof obj2 !== 'object') return cloneShallow(obj1);
24
-
25
- const result = {};
26
-
27
- for (const key of Object.keys(obj1)) result[key] = cloneShallow(obj1[key]);
28
- for (const key of Object.keys(obj2)) {
29
- if (options.ignoreNewKeys && !(key in obj1)) continue;
30
-
31
- const val1 = obj1[key];
32
- const val2 = obj2[key];
33
- const shouldMerge = val1 && val2 && typeof val1 === 'object' && typeof val2 === 'object' && !Array.isArray(val1) && !Array.isArray(val2);
34
- result[key] = shouldMerge ? mergeObject(val1, val2, options) : cloneShallow(val2);
35
-
36
- }
37
-
38
- return result;
39
- }
40
-
41
- export function get(obj, path) {
42
- if (arguments.length !== 2) return console.error('Get must be called with two arguments; an object and a property key.');
43
- if (!obj) return console.error(`Cannot call get with '${path}' on an undefined object.`);
44
- if (typeof path !== 'string') return console.error('The path provided to get must be a string.');
45
-
46
- for (const key of path.split('.')) {
47
- if (obj[key] === undefined) return null;
48
-
49
- obj = obj[key];
50
- }
51
-
52
- return obj;
53
- }
54
-
55
- export function getOrSet(map, key, defaultValue) {
56
- if (!(map instanceof Map)) throw new Error('First argument to getOrSet must be a Map.');
57
-
58
- if (!map.has(key)) map.set(key, typeof defaultValue === "function" ? defaultValue() : defaultValue);
59
-
60
- return map.get(key);
61
- }
package/src/plurarize.js DELETED
@@ -1,103 +0,0 @@
1
- // --- Irregular nouns ---
2
- const irregular = {
3
- person: 'people',
4
- man: 'men',
5
- woman: 'women',
6
- child: 'children',
7
- tooth: 'teeth',
8
- foot: 'feet',
9
- mouse: 'mice',
10
- goose: 'geese',
11
- ox: 'oxen',
12
- cactus: 'cacti',
13
- nucleus: 'nuclei',
14
- syllabus: 'syllabi',
15
- focus: 'foci',
16
- fungus: 'fungi',
17
- appendix: 'appendices',
18
- index: 'indices',
19
- criterion: 'criteria',
20
- phenomenon: 'phenomena',
21
- die: 'dice',
22
- thesis: 'theses',
23
- analysis: 'analyses',
24
- crisis: 'crises',
25
- radius: 'radii',
26
- corpus: 'corpora',
27
- };
28
-
29
- // --- Uncountables ---
30
- const uncountable = new Set([
31
- 'sheep', 'fish', 'deer', 'series', 'species', 'news', 'information',
32
- 'rice', 'moose', 'bison', 'salmon', 'aircraft', 'offspring'
33
- ]);
34
-
35
- // --- Exceptions ---
36
- const fExceptions = new Set(['chief', 'roof', 'belief', 'chef', 'cliff', 'reef', 'proof', 'brief']);
37
-
38
- // Keep only true irregular -o exceptions (consonant + o but take just "s")
39
- const oExceptions = new Set(['piano', 'photo', 'halo', 'canto', 'solo']);
40
-
41
- // --- Utility to preserve casing ---
42
- function applyCasing(original, plural) {
43
- if (original === original.toUpperCase()) return plural.toUpperCase();
44
- if (original === original.toLowerCase()) return plural.toLowerCase();
45
- if (original[0] === original[0].toUpperCase()) {
46
- return plural.charAt(0).toUpperCase() + plural.slice(1);
47
- }
48
- return plural;
49
- }
50
-
51
- // --- Rule-based pluralization ---
52
- const rules = [
53
- // quiz → quizzes, waltz → waltzes, topaz → topazes
54
- [/z$/i, w => (/iz$/i.test(w) ? w + 'zes' : w + 'es')],
55
-
56
- // bus → buses, box → boxes, church → churches, but stomach → stomachs (exclude -ach)
57
- [/(s|x|ch|sh)$/i, w => (/ach$/i.test(w) ? w + 's' : w + 'es')],
58
-
59
- // vowel + y → +s (key → keys)
60
- [/[aeiou]y$/i, w => w + 's'],
61
-
62
- // consonant + y → -ies (city → cities)
63
- [/y$/i, w => w.slice(0, -1) + 'ies'],
64
-
65
- // -fe → -ves (knife → knives), but not chief/roof/etc
66
- [/fe$/i, w => (fExceptions.has(w) ? w + 's' : w.slice(0, -2) + 'ves')],
67
-
68
- // -f → -ves (wolf → wolves), but not cliff/etc
69
- [/f$/i, w => (fExceptions.has(w) ? w + 's' : w.slice(0, -1) + 'ves')],
70
-
71
- // -sis → -ses (analysis → analyses, thesis → theses)
72
- [/sis$/i, w => w.slice(0, -2) + 'ses'],
73
-
74
- // vowel + o → +s (zoo → zoos, video → videos, patio → patios)
75
- [/[aeiou]o$/i, w => w + 's'],
76
-
77
- // consonant + o → usually +es, unless in oExceptions
78
- [/o$/i, w => (oExceptions.has(w) ? w + 's' : w + 'es')],
79
-
80
- // default: just +s
81
- [/$/i, w => w + 's']
82
- ];
83
-
84
- // --- Exported pluralizer ---
85
- export default function pluralize(word) {
86
- if (typeof word !== 'string' || !/^[a-zA-Z]+$/.test(word)) return word;
87
-
88
- const lower = word.toLowerCase();
89
-
90
- if (uncountable.has(lower)) return word;
91
-
92
- if (irregular[lower]) {
93
- return applyCasing(word, irregular[lower]);
94
- }
95
-
96
- for (const [pattern, transform] of rules) {
97
- if (pattern.test(lower)) {
98
- return applyCasing(word, transform(lower));
99
- }
100
- }
101
-
102
- return word; // fallback (shouldn't hit)
103
- }
package/src/promise.js DELETED
@@ -1,5 +0,0 @@
1
- export async function sleep(seconds) {
2
- return new Promise(resolve => {
3
- setTimeout(resolve, 1000 * seconds);
4
- })
5
- }
package/src/string.js DELETED
@@ -1,35 +0,0 @@
1
- function kebabToCase(str, pascal=false) {
2
- let out = '';
3
- let upperNext = pascal; // PascalCase starts uppercase
4
- for (let i = 0; i < str.length; i++) {
5
- const ch = str.charAt(i);
6
- if (ch === '-') {
7
- upperNext = true;
8
- } else if (upperNext) {
9
- out += ch.toUpperCase();
10
- upperNext = false;
11
- } else {
12
- out += ch;
13
- }
14
- }
15
- return out;
16
- }
17
-
18
- export function kebabCaseToCamelCase(str) {
19
- return kebabToCase(str, false);
20
- }
21
-
22
- export function kebabCaseToPascalCase(str) {
23
- return kebabToCase(str, true);
24
- }
25
-
26
- export function camelCaseToKebabCase(str) {
27
- return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
28
- }
29
-
30
- export function generateRandomString(length=8) {
31
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
32
- return Array(length).fill('').map(() => characters.charAt(Math.floor(Math.random() * characters.length))).join('');
33
- }
34
-
35
- export { default as pluralize } from './plurarize.js';