@gajae-code/utils 0.8.1 → 0.9.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/dist/types/cli.d.ts +8 -0
- package/dist/types/tab-spacing.d.ts +3 -0
- package/package.json +2 -2
- package/src/cli.ts +51 -13
- package/src/env.ts +5 -2
- package/src/frontmatter.ts +14 -2
- package/src/glob.ts +25 -13
- package/src/tab-spacing.ts +16 -1
package/dist/types/cli.d.ts
CHANGED
|
@@ -39,6 +39,14 @@ export declare const Flags: {
|
|
|
39
39
|
export declare const Args: {
|
|
40
40
|
string<T extends ArgInput>(opts?: T): ArgDescriptor & T;
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when CLI argument/flag parsing or validation fails (unknown flag,
|
|
44
|
+
* bad option value, missing required arg, etc.). `run()` catches this to print
|
|
45
|
+
* the message and render usage instead of crashing as an uncaught exception.
|
|
46
|
+
*/
|
|
47
|
+
export declare class CliParseError extends Error {
|
|
48
|
+
constructor(message: string);
|
|
49
|
+
}
|
|
42
50
|
type FlagValue<D extends FlagDescriptor> = D["kind"] extends "boolean" ? D extends {
|
|
43
51
|
default: boolean;
|
|
44
52
|
} ? boolean : boolean | undefined : D["kind"] extends "integer" ? D extends {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export declare const MIN_TAB_WIDTH = 1;
|
|
2
2
|
export declare const MAX_TAB_WIDTH = 16;
|
|
3
3
|
export declare const DEFAULT_TAB_WIDTH = 3;
|
|
4
|
+
type TabWidthChangeListener = (width: number) => void;
|
|
5
|
+
export declare function onDefaultTabWidthChange(listener: TabWidthChangeListener): () => void;
|
|
4
6
|
export declare function getDefaultTabWidth(): number;
|
|
5
7
|
export declare function setDefaultTabWidth(width: number): void;
|
|
6
8
|
/**
|
|
7
9
|
* Visible tab width in columns for `file` (from `.editorconfig` + default), or the default when `file` is omitted.
|
|
8
10
|
*/
|
|
9
11
|
export declare function getIndentation(file?: string | null, projectDir?: string | null): number;
|
|
12
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/utils",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@gajae-code/natives": "0.
|
|
34
|
+
"@gajae-code/natives": "0.9.0",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/cli.ts
CHANGED
|
@@ -68,6 +68,18 @@ export const Args = {
|
|
|
68
68
|
},
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Thrown when CLI argument/flag parsing or validation fails (unknown flag,
|
|
73
|
+
* bad option value, missing required arg, etc.). `run()` catches this to print
|
|
74
|
+
* the message and render usage instead of crashing as an uncaught exception.
|
|
75
|
+
*/
|
|
76
|
+
export class CliParseError extends Error {
|
|
77
|
+
constructor(message: string) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "CliParseError";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// ---------------------------------------------------------------------------
|
|
72
84
|
// Parse result types — mirrors oclif's typed output from this.parse()
|
|
73
85
|
// ---------------------------------------------------------------------------
|
|
@@ -174,12 +186,22 @@ export abstract class Command {
|
|
|
174
186
|
|
|
175
187
|
// strict=false when command declares args (positionals must pass through)
|
|
176
188
|
// or when the command itself opts out
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
let rawValues: Record<string, string | boolean | Array<string | boolean> | undefined>;
|
|
190
|
+
let positionals: string[];
|
|
191
|
+
try {
|
|
192
|
+
const parsed = nodeParseArgs({
|
|
193
|
+
args: this.argv,
|
|
194
|
+
options,
|
|
195
|
+
allowPositionals: true,
|
|
196
|
+
strict,
|
|
197
|
+
});
|
|
198
|
+
rawValues = parsed.values;
|
|
199
|
+
positionals = parsed.positionals;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// node:util parseArgs throws on unknown flags / malformed input — surface
|
|
202
|
+
// it as a CliParseError so run() renders usage instead of crashing.
|
|
203
|
+
throw new CliParseError(err instanceof Error ? err.message : String(err));
|
|
204
|
+
}
|
|
183
205
|
|
|
184
206
|
// Convert raw values to proper types and validate
|
|
185
207
|
const flags: Record<string, unknown> = {};
|
|
@@ -191,7 +213,7 @@ export abstract class Command {
|
|
|
191
213
|
} else {
|
|
192
214
|
const n = Number.parseInt(raw as string, 10);
|
|
193
215
|
if (Number.isNaN(n)) {
|
|
194
|
-
throw new
|
|
216
|
+
throw new CliParseError(`Expected integer for --${name}, got "${raw}"`);
|
|
195
217
|
}
|
|
196
218
|
flags[name] = n;
|
|
197
219
|
}
|
|
@@ -204,14 +226,16 @@ export abstract class Command {
|
|
|
204
226
|
// Validate options constraint
|
|
205
227
|
if (val !== undefined && desc.options && !Array.isArray(val)) {
|
|
206
228
|
if (!desc.options.includes(val as string)) {
|
|
207
|
-
throw new
|
|
229
|
+
throw new CliParseError(
|
|
230
|
+
`Expected --${name} to be one of: ${[...desc.options].join(", ")}; got "${val}"`,
|
|
231
|
+
);
|
|
208
232
|
}
|
|
209
233
|
}
|
|
210
234
|
flags[name] = val;
|
|
211
235
|
}
|
|
212
236
|
// Validate required
|
|
213
237
|
if (desc.required && flags[name] === undefined) {
|
|
214
|
-
throw new
|
|
238
|
+
throw new CliParseError(`Missing required flag: --${name}`);
|
|
215
239
|
}
|
|
216
240
|
}
|
|
217
241
|
|
|
@@ -230,13 +254,15 @@ export abstract class Command {
|
|
|
230
254
|
}
|
|
231
255
|
// Validate required
|
|
232
256
|
if (desc.required && args[argName] === undefined) {
|
|
233
|
-
throw new
|
|
257
|
+
throw new CliParseError(`Missing required argument: ${argName}`);
|
|
234
258
|
}
|
|
235
259
|
// Validate options constraint
|
|
236
260
|
const argVal = args[argName];
|
|
237
261
|
if (argVal !== undefined && desc.options && typeof argVal === "string") {
|
|
238
262
|
if (!desc.options.includes(argVal)) {
|
|
239
|
-
throw new
|
|
263
|
+
throw new CliParseError(
|
|
264
|
+
`Expected ${argName} to be one of: ${[...desc.options].join(", ")}; got "${argVal}"`,
|
|
265
|
+
);
|
|
240
266
|
}
|
|
241
267
|
}
|
|
242
268
|
}
|
|
@@ -407,7 +433,7 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
407
433
|
const instance = new Cmd(commandArgv, config);
|
|
408
434
|
await instance.run();
|
|
409
435
|
} else {
|
|
410
|
-
const config =
|
|
436
|
+
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
411
437
|
renderCommandHelp(bin, entry.name, config.commands.get(entry.name) ?? Cmd);
|
|
412
438
|
}
|
|
413
439
|
return;
|
|
@@ -425,7 +451,19 @@ export async function run(opts: RunOptions): Promise<void> {
|
|
|
425
451
|
const Cmd = await entry.load();
|
|
426
452
|
const config: CliConfig = { bin, version, commands: new Map([[entry.name, Cmd]]) };
|
|
427
453
|
const instance = new Cmd(commandArgv, config);
|
|
428
|
-
|
|
454
|
+
try {
|
|
455
|
+
await instance.run();
|
|
456
|
+
} catch (err) {
|
|
457
|
+
if (err instanceof CliParseError) {
|
|
458
|
+
// Invalid args/flags for a real command: print the problem + usage and
|
|
459
|
+
// exit with a usage error, instead of crashing as an uncaught exception.
|
|
460
|
+
process.stderr.write(`${err.message}\n\n`);
|
|
461
|
+
renderCommandHelp(bin, entry.name, Cmd);
|
|
462
|
+
process.exitCode = 2;
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
throw err;
|
|
466
|
+
}
|
|
429
467
|
}
|
|
430
468
|
|
|
431
469
|
/** Resolve all command loaders for help/alias display. */
|
package/src/env.ts
CHANGED
|
@@ -272,7 +272,10 @@ export function isCompiledBinary(): boolean {
|
|
|
272
272
|
|
|
273
273
|
const TRUTHY: Dict<boolean> = { "1": true, Y: true, TRUE: true, YES: true, ON: true };
|
|
274
274
|
export function $flag(name: string, def: boolean = false): boolean {
|
|
275
|
-
const value = $env[name];
|
|
275
|
+
const value = $env[name]?.trim();
|
|
276
276
|
if (!value) return def;
|
|
277
|
-
|
|
277
|
+
// Boolean-like env values are documented as case-insensitive (`1`/`true`/`yes`/`on`),
|
|
278
|
+
// so normalize before the lookup — otherwise `FOO=true` (the common lowercase spelling)
|
|
279
|
+
// would silently read as false while only `FOO=TRUE`/`FOO=1` worked.
|
|
280
|
+
return TRUTHY[value.toUpperCase()] === true;
|
|
278
281
|
}
|
package/src/frontmatter.ts
CHANGED
|
@@ -17,15 +17,27 @@ function stripLooseScalarTrailingCommas(metadata: string): string {
|
|
|
17
17
|
.join("\n");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function asFrontmatterRecord(parsed: unknown): Record<string, unknown> | null {
|
|
21
|
+
if (parsed === null) return null;
|
|
22
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
23
|
+
throw new Error("YAML frontmatter root must be an object");
|
|
24
|
+
}
|
|
25
|
+
const prototype = Object.getPrototypeOf(parsed);
|
|
26
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
27
|
+
throw new Error("YAML frontmatter root must be an object");
|
|
28
|
+
}
|
|
29
|
+
return parsed as Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
function parseYamlMetadata(metadata: string): Record<string, unknown> | null {
|
|
21
33
|
const normalized = metadata.replaceAll("\t", " ");
|
|
22
34
|
try {
|
|
23
|
-
return YAML.parse(normalized)
|
|
35
|
+
return asFrontmatterRecord(YAML.parse(normalized));
|
|
24
36
|
} catch (strictError) {
|
|
25
37
|
const loose = stripLooseScalarTrailingCommas(normalized);
|
|
26
38
|
if (loose === normalized) throw strictError;
|
|
27
39
|
try {
|
|
28
|
-
return YAML.parse(loose)
|
|
40
|
+
return asFrontmatterRecord(YAML.parse(loose));
|
|
29
41
|
} catch {
|
|
30
42
|
throw strictError;
|
|
31
43
|
}
|
package/src/glob.ts
CHANGED
|
@@ -70,20 +70,25 @@ function parseGitignorePatterns(content: string, gitignoreDir: string, baseDir:
|
|
|
70
70
|
} else {
|
|
71
71
|
patterns.push(pattern);
|
|
72
72
|
}
|
|
73
|
+
} else if (pattern.includes("/") && !pattern.startsWith("**/")) {
|
|
74
|
+
// Separator in the middle: git anchors these to the .gitignore's
|
|
75
|
+
// directory, same as rooted patterns
|
|
76
|
+
const absolutePattern = path.join(gitignoreDir, pattern);
|
|
77
|
+
const relativeToBase = path.relative(baseDir, absolutePattern);
|
|
78
|
+
if (relativeToBase.startsWith("..")) {
|
|
79
|
+
// Pattern is outside the search directory, skip
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
pattern = relativeToBase.replace(/\\/g, "/");
|
|
83
|
+
patterns.push(pattern);
|
|
84
|
+
if (isDirectoryOnly) {
|
|
85
|
+
patterns.push(`${pattern}/**`);
|
|
86
|
+
}
|
|
73
87
|
} else {
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
patterns.push(`**/${pattern}
|
|
78
|
-
if (isDirectoryOnly) {
|
|
79
|
-
patterns.push(`**/${pattern}/**`);
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
// No slash: match file/dir name anywhere
|
|
83
|
-
patterns.push(`**/${pattern}`);
|
|
84
|
-
if (isDirectoryOnly) {
|
|
85
|
-
patterns.push(`**/${pattern}/**`);
|
|
86
|
-
}
|
|
88
|
+
// No middle separator: match file/dir name anywhere in the tree
|
|
89
|
+
patterns.push(`**/${pattern}`);
|
|
90
|
+
if (isDirectoryOnly) {
|
|
91
|
+
patterns.push(`**/${pattern}/**`);
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -149,6 +154,9 @@ export async function globPaths(patterns: string | string[], options: GlobPathsO
|
|
|
149
154
|
|
|
150
155
|
const base = cwd ?? getProjectDir();
|
|
151
156
|
const allResults: string[] = [];
|
|
157
|
+
// Overlapping patterns (e.g. `["**/*.ts", "src/*.ts"]`) can both match the same
|
|
158
|
+
// file; dedupe so a path is returned at most once regardless of pattern overlap.
|
|
159
|
+
const seen = new Set<string>();
|
|
152
160
|
|
|
153
161
|
// Combine timeout and abort signals
|
|
154
162
|
const timeoutSignal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
@@ -176,6 +184,10 @@ export async function globPaths(patterns: string | string[], options: GlobPathsO
|
|
|
176
184
|
if (excludeGlobs.some(excludeGlob => excludeGlob.match(normalized))) {
|
|
177
185
|
continue;
|
|
178
186
|
}
|
|
187
|
+
if (seen.has(normalized)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
seen.add(normalized);
|
|
179
191
|
allResults.push(normalized);
|
|
180
192
|
}
|
|
181
193
|
}
|
package/src/tab-spacing.ts
CHANGED
|
@@ -13,6 +13,9 @@ export const DEFAULT_TAB_WIDTH = 3;
|
|
|
13
13
|
const EDITORCONFIG_NAME = ".editorconfig";
|
|
14
14
|
|
|
15
15
|
let defaultTabWidth = DEFAULT_TAB_WIDTH;
|
|
16
|
+
type TabWidthChangeListener = (width: number) => void;
|
|
17
|
+
|
|
18
|
+
const tabWidthChangeListeners = new Set<TabWidthChangeListener>();
|
|
16
19
|
|
|
17
20
|
const editorConfigCache = new Map<string, ParsedEditorConfig>();
|
|
18
21
|
const editorConfigChainCache = new Map<string, ChainEntry[]>();
|
|
@@ -278,13 +281,25 @@ function resolveEditorConfigTabWidth(match: EditorConfigMatch | undefined, fallb
|
|
|
278
281
|
|
|
279
282
|
return undefined;
|
|
280
283
|
}
|
|
284
|
+
export function onDefaultTabWidthChange(listener: TabWidthChangeListener): () => void {
|
|
285
|
+
tabWidthChangeListeners.add(listener);
|
|
286
|
+
return () => {
|
|
287
|
+
tabWidthChangeListeners.delete(listener);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
281
290
|
|
|
282
291
|
export function getDefaultTabWidth(): number {
|
|
283
292
|
return defaultTabWidth;
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
export function setDefaultTabWidth(width: number): void {
|
|
287
|
-
|
|
296
|
+
const next = clampTabWidth(width);
|
|
297
|
+
if (defaultTabWidth === next) return;
|
|
298
|
+
defaultTabWidth = next;
|
|
299
|
+
indentationCache.clear();
|
|
300
|
+
for (const listener of tabWidthChangeListeners) {
|
|
301
|
+
listener(next);
|
|
302
|
+
}
|
|
288
303
|
}
|
|
289
304
|
|
|
290
305
|
/**
|