@acidgreen-au/ag-cicd-cli 0.0.1

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/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,584 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import { execSync, spawn } from "node:child_process";
4
+ import { readFileSync, writeFileSync } from "node:fs";
5
+
6
+ //#region src/commands/check-tag.ts
7
+ function getVersionFromPackage(packagePath) {
8
+ const content = readFileSync(packagePath, "utf-8");
9
+ return JSON.parse(content).version;
10
+ }
11
+ function tagExists(tag) {
12
+ try {
13
+ execSync(`git rev-parse --verify "refs/tags/${tag}"`, {
14
+ encoding: "utf-8",
15
+ stdio: [
16
+ "pipe",
17
+ "pipe",
18
+ "pipe"
19
+ ]
20
+ });
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ function createTag(tag, message) {
27
+ execSync(`git tag -a "${tag}" -m "${message}"`, {
28
+ encoding: "utf-8",
29
+ stdio: [
30
+ "pipe",
31
+ "pipe",
32
+ "pipe"
33
+ ]
34
+ });
35
+ }
36
+ function pushTag(tag, remote) {
37
+ execSync(`git push "${remote}" "${tag}"`, {
38
+ encoding: "utf-8",
39
+ stdio: [
40
+ "pipe",
41
+ "pipe",
42
+ "pipe"
43
+ ]
44
+ });
45
+ }
46
+ function checkTag(options) {
47
+ const packagePath = options.packagePath ?? "package.json";
48
+ const prefix = options.prefix ?? "v";
49
+ const remote = options.remote ?? "origin";
50
+ const version = getVersionFromPackage(packagePath);
51
+ const tag = `${prefix}${version}`;
52
+ if (tagExists(tag)) return {
53
+ version,
54
+ tag,
55
+ exists: true,
56
+ created: false,
57
+ pushed: false,
58
+ message: `Tag ${tag} already exists`
59
+ };
60
+ if (!options.create) return {
61
+ version,
62
+ tag,
63
+ exists: false,
64
+ created: false,
65
+ pushed: false,
66
+ message: `Tag ${tag} does not exist`
67
+ };
68
+ createTag(tag, `Release ${version}`);
69
+ if (!options.push) return {
70
+ version,
71
+ tag,
72
+ exists: false,
73
+ created: true,
74
+ pushed: false,
75
+ message: `Created tag ${tag}`
76
+ };
77
+ pushTag(tag, remote);
78
+ return {
79
+ version,
80
+ tag,
81
+ exists: false,
82
+ created: true,
83
+ pushed: true,
84
+ message: `Created and pushed tag ${tag}`
85
+ };
86
+ }
87
+ function checkTagCommand(options) {
88
+ try {
89
+ const result = checkTag(options);
90
+ console.log(result.message);
91
+ if (!result.exists && !options.create) process.exit(1);
92
+ } catch (error) {
93
+ console.error("Error:", error instanceof Error ? error.message : String(error));
94
+ process.exit(1);
95
+ }
96
+ }
97
+
98
+ //#endregion
99
+ //#region src/utils/codequality.ts
100
+ function runBiome() {
101
+ try {
102
+ const result = execSync("pnpm exec biome ci --reporter=gitlab --colors=off", {
103
+ encoding: "utf-8",
104
+ stdio: [
105
+ "pipe",
106
+ "pipe",
107
+ "pipe"
108
+ ]
109
+ });
110
+ return JSON.parse(result);
111
+ } catch (error) {
112
+ if (error instanceof Error && "stdout" in error) {
113
+ const stdout = error.stdout;
114
+ try {
115
+ return JSON.parse(stdout);
116
+ } catch {
117
+ return [];
118
+ }
119
+ }
120
+ return [];
121
+ }
122
+ }
123
+ function runPrettier() {
124
+ let files = [];
125
+ try {
126
+ files = parsePrettierOutput(execSync("pnpm exec prettier --check --list-different ./theme", {
127
+ encoding: "utf-8",
128
+ stdio: [
129
+ "pipe",
130
+ "pipe",
131
+ "pipe"
132
+ ]
133
+ }));
134
+ } catch (error) {
135
+ if (error instanceof Error && "stdout" in error) {
136
+ const stdout = error.stdout;
137
+ files = parsePrettierOutput(stdout);
138
+ }
139
+ }
140
+ return files.map((file) => ({
141
+ description: "File not formatted according to Prettier rules",
142
+ check_name: "prettier",
143
+ fingerprint: Buffer.from(file).toString("base64"),
144
+ severity: "minor",
145
+ location: {
146
+ path: file,
147
+ lines: { begin: 1 }
148
+ }
149
+ }));
150
+ }
151
+ function parsePrettierOutput(output) {
152
+ return output.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith(">") && !line.startsWith("$"));
153
+ }
154
+ function runThemeCheck(themePath) {
155
+ let themeCheckResult = [];
156
+ try {
157
+ const result = execSync(`shopify theme check --path ${themePath} --output json`, {
158
+ encoding: "utf-8",
159
+ stdio: [
160
+ "pipe",
161
+ "pipe",
162
+ "pipe"
163
+ ]
164
+ });
165
+ themeCheckResult = JSON.parse(result);
166
+ } catch (error) {
167
+ if (error instanceof Error && "stdout" in error) {
168
+ const stdout = error.stdout;
169
+ try {
170
+ themeCheckResult = JSON.parse(stdout);
171
+ } catch {
172
+ themeCheckResult = [];
173
+ }
174
+ }
175
+ }
176
+ return themeCheckResult.flatMap((file) => file.offenses.map((offense) => ({
177
+ description: offense.message,
178
+ check_name: offense.check,
179
+ fingerprint: Buffer.from(`${file.path}:${offense.start_row}:${offense.check}`).toString("base64"),
180
+ severity: mapThemeSeverity(offense.severity),
181
+ location: {
182
+ path: file.path,
183
+ lines: { begin: offense.start_row || 1 }
184
+ }
185
+ })));
186
+ }
187
+ function mapThemeSeverity(severity) {
188
+ switch (severity) {
189
+ case 0: return "info";
190
+ case 1: return "minor";
191
+ case 2: return "major";
192
+ default: return "critical";
193
+ }
194
+ }
195
+ function runTsc() {
196
+ let errors = [];
197
+ try {
198
+ execSync("pnpm exec tsc --noEmit", {
199
+ encoding: "utf-8",
200
+ stdio: [
201
+ "pipe",
202
+ "pipe",
203
+ "pipe"
204
+ ]
205
+ });
206
+ } catch (error) {
207
+ if (error instanceof Error && "stdout" in error) {
208
+ const stdout = error.stdout;
209
+ errors = parseTscOutput(stdout);
210
+ }
211
+ }
212
+ return errors.map((err) => ({
213
+ description: `TS${err.code}: ${err.message}`,
214
+ check_name: `typescript/${err.code}`,
215
+ fingerprint: Buffer.from(`${err.file}:${err.line}:${err.column}:${err.code}`).toString("base64"),
216
+ severity: "major",
217
+ location: {
218
+ path: err.file,
219
+ lines: { begin: err.line }
220
+ }
221
+ }));
222
+ }
223
+ function parseTscOutput(output) {
224
+ const errors = [];
225
+ const lines = output.split("\n");
226
+ const errorRegex = /^(.+)\((\d+),(\d+)\):\s*error\s+TS(\d+):\s*(.+)$/;
227
+ for (const line of lines) {
228
+ const match = line.match(errorRegex);
229
+ if (match) errors.push({
230
+ file: match[1],
231
+ line: parseInt(match[2], 10),
232
+ column: parseInt(match[3], 10),
233
+ code: match[4],
234
+ message: match[5]
235
+ });
236
+ }
237
+ return errors;
238
+ }
239
+
240
+ //#endregion
241
+ //#region src/commands/codequality.ts
242
+ const ALL_CHECKS = [
243
+ "biome",
244
+ "prettier",
245
+ "theme-check",
246
+ "tsc"
247
+ ];
248
+ function codequality(options) {
249
+ const { output, path, checks = ALL_CHECKS } = options;
250
+ const allIssues = [];
251
+ const enabledChecks = new Set(checks);
252
+ console.log(`Running code quality checks: ${checks.join(", ")}\n`);
253
+ if (enabledChecks.has("biome")) {
254
+ console.log("Running Biome...");
255
+ const biomeIssues = runBiome();
256
+ allIssues.push(...biomeIssues);
257
+ console.log(` Found ${biomeIssues.length} issues\n`);
258
+ }
259
+ if (enabledChecks.has("prettier")) {
260
+ console.log("Running Prettier...");
261
+ const prettierIssues = runPrettier();
262
+ allIssues.push(...prettierIssues);
263
+ console.log(` Found ${prettierIssues.length} issues\n`);
264
+ }
265
+ if (enabledChecks.has("theme-check")) {
266
+ console.log("Running Theme Check...");
267
+ const themeIssues = runThemeCheck(path);
268
+ allIssues.push(...themeIssues);
269
+ console.log(` Found ${themeIssues.length} issues\n`);
270
+ }
271
+ if (enabledChecks.has("tsc")) {
272
+ console.log("Running TypeScript...");
273
+ const tscIssues = runTsc();
274
+ allIssues.push(...tscIssues);
275
+ console.log(` Found ${tscIssues.length} issues\n`);
276
+ }
277
+ writeFileSync(output, JSON.stringify(allIssues, null, 2));
278
+ console.log(`Wrote ${allIssues.length} total issues to ${output}`);
279
+ }
280
+
281
+ //#endregion
282
+ //#region src/commands/commit-msg.ts
283
+ function validateCommitMsg(options) {
284
+ const { file, prefix } = options;
285
+ const ticketRegex = /* @__PURE__ */ new RegExp(`^${prefix}-[0-9]+`);
286
+ let message;
287
+ try {
288
+ message = readFileSync(file, "utf-8").trim();
289
+ } catch {
290
+ return {
291
+ success: false,
292
+ message: `Error: Unable to read commit message file: ${file}`
293
+ };
294
+ }
295
+ const match = message.match(ticketRegex);
296
+ if (!match) return {
297
+ success: false,
298
+ message: `Error: Commit message must start with a JIRA ticket ID (e.g., ${prefix}-69420).`
299
+ };
300
+ const ticket = match[0];
301
+ let branch;
302
+ try {
303
+ branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
304
+ } catch {
305
+ return {
306
+ success: true,
307
+ message: "Warning: Unable to determine current branch name; skipping branch-ticket check.",
308
+ ticket
309
+ };
310
+ }
311
+ if (!branch.match(/* @__PURE__ */ new RegExp(`${prefix}-[0-9]+`))) return {
312
+ success: true,
313
+ message: "OK",
314
+ ticket,
315
+ branch
316
+ };
317
+ if (!branch.includes(ticket)) return {
318
+ success: false,
319
+ message: `Error: Branch name \`${branch}\` does not contain ticket ID ${ticket}.`,
320
+ ticket,
321
+ branch
322
+ };
323
+ return {
324
+ success: true,
325
+ message: "OK",
326
+ ticket,
327
+ branch
328
+ };
329
+ }
330
+ function commitMsg(options) {
331
+ const result = validateCommitMsg(options);
332
+ if (!result.success) {
333
+ console.error(result.message);
334
+ process.exit(1);
335
+ }
336
+ if (result.message.startsWith("Warning:")) console.warn(result.message);
337
+ process.exit(0);
338
+ }
339
+
340
+ //#endregion
341
+ //#region src/utils/shopify.ts
342
+ function listThemes() {
343
+ const result = execSync("shopify theme list --json", { encoding: "utf-8" });
344
+ return JSON.parse(result);
345
+ }
346
+ function findThemeByName(name) {
347
+ return listThemes().find((t) => t.name === name);
348
+ }
349
+ function findLiveTheme() {
350
+ return listThemes().find((t) => t.role === "live");
351
+ }
352
+ function duplicateTheme(themeId, name) {
353
+ const result = execSync(`shopify theme duplicate -j --theme "${themeId}" --name "${name}"`, { encoding: "utf-8" });
354
+ return JSON.parse(result);
355
+ }
356
+ function pushTheme(path, themeId, ignorePatterns = []) {
357
+ const result = execSync(`shopify theme push --path ${path} --theme "${themeId}" ${ignorePatterns.map((p) => `--ignore "${p}"`).join(" ")} --json`, { encoding: "utf-8" });
358
+ return JSON.parse(result);
359
+ }
360
+ function deleteTheme(themeId) {
361
+ execSync(`shopify theme delete --theme "${themeId}" --force`, { encoding: "utf-8" });
362
+ }
363
+ function writeDeployEnv(previewUrl, editorUrl, themeId, extras = {}) {
364
+ let content = `PREVIEW_URL=${previewUrl}\nEDITOR_URL=${editorUrl}\nTHEME_ID=${themeId}\n`;
365
+ for (const [key, value] of Object.entries(extras)) content += `${key}=${value}\n`;
366
+ writeFileSync("deploy.env", content);
367
+ }
368
+ const defaultIgnores = [
369
+ "templates/**/*.json",
370
+ "locales/**/*.json",
371
+ "config/settings_data.json",
372
+ "src/*",
373
+ "public/*"
374
+ ];
375
+
376
+ //#endregion
377
+ //#region src/commands/deploy.ts
378
+ function deploy(options) {
379
+ const { themeId, path } = options;
380
+ const themeIdNum = parseInt(themeId, 10);
381
+ const { preview_url, editor_url } = pushTheme(path, themeIdNum, defaultIgnores).theme;
382
+ console.log("Release deployed successfully!");
383
+ console.log(`Preview URL: ${preview_url}`);
384
+ console.log(`Editor URL: ${editor_url}`);
385
+ writeDeployEnv(preview_url, editor_url, themeIdNum);
386
+ }
387
+
388
+ //#endregion
389
+ //#region src/commands/deploy-review.ts
390
+ function deployReview(options) {
391
+ const { branch, path } = options;
392
+ const themeName = `AG Preview: ${branch}`;
393
+ console.log(`Setting up review app for branch: ${branch}`);
394
+ let reviewThemeId;
395
+ const existingTheme = findThemeByName(themeName);
396
+ if (!existingTheme) {
397
+ console.log("Creating new unpublished theme...");
398
+ const liveTheme = findLiveTheme();
399
+ if (!liveTheme) throw new Error("No live theme found to clone from");
400
+ console.log(`Cloning from published theme ID: ${liveTheme.id}`);
401
+ reviewThemeId = duplicateTheme(liveTheme.id, themeName).theme.id;
402
+ } else {
403
+ console.log(`Theme already exists with ID: ${existingTheme.id}`);
404
+ reviewThemeId = existingTheme.id;
405
+ }
406
+ console.log(`Deploying branch ${branch} to theme ID: ${reviewThemeId}`);
407
+ const { preview_url, editor_url } = pushTheme(path, reviewThemeId, defaultIgnores).theme;
408
+ console.log("Review app deployed successfully!");
409
+ console.log(`Preview URL: ${preview_url}`);
410
+ console.log(`Editor URL: ${editor_url}`);
411
+ writeDeployEnv(preview_url, editor_url, reviewThemeId, { EXISTING_THEME_ID: existingTheme?.id?.toString() ?? "" });
412
+ }
413
+
414
+ //#endregion
415
+ //#region src/commands/dev.ts
416
+ function dev(_options, command) {
417
+ const themeArgs = command.args;
418
+ console.log("Starting development servers...");
419
+ const vite = spawn("pnpm", ["exec", "vite"], {
420
+ stdio: "inherit",
421
+ shell: true
422
+ });
423
+ const shopify = spawn("pnpm", [
424
+ "exec",
425
+ "shopify",
426
+ "theme",
427
+ "dev",
428
+ ...themeArgs
429
+ ], {
430
+ stdio: "inherit",
431
+ shell: true
432
+ });
433
+ const cleanup = () => {
434
+ vite.kill();
435
+ shopify.kill();
436
+ process.exit(0);
437
+ };
438
+ process.on("SIGINT", cleanup);
439
+ process.on("SIGTERM", cleanup);
440
+ vite.on("close", (code) => {
441
+ if (code !== 0) {
442
+ console.error(`Vite exited with code ${code}`);
443
+ shopify.kill();
444
+ process.exit(code ?? 1);
445
+ }
446
+ });
447
+ shopify.on("close", (code) => {
448
+ if (code !== 0) {
449
+ console.error(`Shopify theme dev exited with code ${code}`);
450
+ vite.kill();
451
+ process.exit(code ?? 1);
452
+ }
453
+ });
454
+ }
455
+
456
+ //#endregion
457
+ //#region src/commands/release.ts
458
+ function release(options) {
459
+ const { name, path } = options;
460
+ const themeName = `AG Release: ${name}`;
461
+ const liveTheme = findLiveTheme();
462
+ if (!liveTheme) throw new Error("No live theme found to clone from");
463
+ console.log(`Cloning from published theme ID: ${liveTheme.id}`);
464
+ const releaseThemeId = duplicateTheme(liveTheme.id, themeName).theme.id;
465
+ const { preview_url, editor_url } = pushTheme(path, releaseThemeId, defaultIgnores).theme;
466
+ console.log("Release deployed successfully!");
467
+ console.log(`Preview URL: ${preview_url}`);
468
+ console.log(`Editor URL: ${editor_url}`);
469
+ writeDeployEnv(preview_url, editor_url, releaseThemeId, { PUBLISHED_THEME_ID: liveTheme.id.toString() });
470
+ }
471
+
472
+ //#endregion
473
+ //#region src/commands/stop-review.ts
474
+ function stopReview(options) {
475
+ const { branch } = options;
476
+ const themeName = `AG Preview: ${branch}`;
477
+ console.log(`Cleaning up review app for branch: ${branch}`);
478
+ const theme = findThemeByName(themeName);
479
+ if (theme) {
480
+ console.log(`Deleting theme: ${themeName} (ID: ${theme.id})`);
481
+ deleteTheme(theme.id);
482
+ console.log("Review app cleaned up successfully!");
483
+ } else console.log(`No theme found with name: ${themeName}`);
484
+ }
485
+
486
+ //#endregion
487
+ //#region src/commands/validate-env.ts
488
+ const commonVars = [{
489
+ name: "SHOPIFY_CLI_THEME_TOKEN",
490
+ required: true,
491
+ description: "Shopify CLI authentication token"
492
+ }, {
493
+ name: "SHOPIFY_FLAG_STORE",
494
+ required: true,
495
+ description: "Shopify store URL"
496
+ }];
497
+ const contextVars = {
498
+ deploy: [{
499
+ name: "SHOPIFY_THEME_ID",
500
+ required: true,
501
+ description: "Target theme ID for deployment"
502
+ }],
503
+ review: [{
504
+ name: "CI_COMMIT_REF_NAME",
505
+ required: true,
506
+ description: "Git branch name for review app"
507
+ }],
508
+ release: [{
509
+ name: "CI_COMMIT_TAG",
510
+ required: true,
511
+ description: "Git tag for release"
512
+ }],
513
+ codequality: []
514
+ };
515
+ function getVarsToCheck(options) {
516
+ const { context, vars } = options;
517
+ if (vars && vars.length > 0) return vars.map((name) => ({
518
+ name,
519
+ required: true,
520
+ description: "User-specified variable"
521
+ }));
522
+ const varsToCheck = [...commonVars];
523
+ const ctx = context ?? "all";
524
+ if (ctx !== "all" && contextVars[ctx]) varsToCheck.push(...contextVars[ctx]);
525
+ else if (ctx === "all") for (const ctxVars of Object.values(contextVars)) varsToCheck.push(...ctxVars);
526
+ return varsToCheck;
527
+ }
528
+ function checkEnvVars(varsToCheck, env = process.env) {
529
+ const missing = [];
530
+ const present = [];
531
+ for (const envVar of varsToCheck) {
532
+ const value = env[envVar.name];
533
+ if (!value && envVar.required) missing.push(envVar);
534
+ else present.push({
535
+ name: envVar.name,
536
+ value,
537
+ redacted: envVar.name.includes("TOKEN")
538
+ });
539
+ }
540
+ return {
541
+ success: missing.length === 0,
542
+ present,
543
+ missing
544
+ };
545
+ }
546
+ function validateEnv(options) {
547
+ const { context, vars } = options;
548
+ const ctx = context ?? "all";
549
+ if (vars && vars.length > 0) console.log("Validating specified environment variables:\n");
550
+ else console.log(`Validating environment for context: ${ctx}\n`);
551
+ const result = checkEnvVars(getVarsToCheck(options));
552
+ if (result.present.length > 0) {
553
+ console.log("✓ Present:");
554
+ for (const item of result.present) {
555
+ const displayValue = item.redacted ? "[REDACTED]" : item.value;
556
+ console.log(` ${item.name}=${displayValue}`);
557
+ }
558
+ console.log();
559
+ }
560
+ if (result.missing.length > 0) {
561
+ console.log("✗ Missing:");
562
+ for (const envVar of result.missing) console.log(` ${envVar.name} - ${envVar.description}`);
563
+ console.log();
564
+ process.exit(1);
565
+ }
566
+ console.log("All required environment variables are set.");
567
+ }
568
+
569
+ //#endregion
570
+ //#region src/cli.ts
571
+ program.name("agci").description("Acidgreen CI/CD CLI tools").version("1.0.0");
572
+ program.command("codequality").description("Run code quality checks and combine output").option("-o, --output <file>", "Output file", "codequality.json").option("-p, --path <path>", "Theme path", "theme").option("-c, --checks <checks...>", "Checks to run (biome, prettier, theme-check, tsc)").action(codequality);
573
+ program.command("deploy:review").description("Deploy a review app for the current branch").requiredOption("-b, --branch <branch>", "Branch name (CI_COMMIT_REF_NAME)").option("-p, --path <path>", "Theme path", "theme").action(deployReview);
574
+ program.command("stop:review").description("Stop and delete a review app").requiredOption("-b, --branch <branch>", "Branch name (CI_COMMIT_REF_NAME)").action(stopReview);
575
+ program.command("deploy").description("Deploy to a specific theme").requiredOption("-t, --theme-id <id>", "Theme ID (SHOPIFY_THEME_ID)").option("-p, --path <path>", "Theme path", "theme").action(deploy);
576
+ program.command("release").description("Create a release by cloning live theme and pushing").requiredOption("-n, --name <name>", "Release name/tag").option("-p, --path <path>", "Theme path", "theme").action(release);
577
+ program.command("validate-env").description("Validate required environment variables are set").option("-c, --context <context>", "Context to validate (deploy, review, release, codequality, all)").option("-v, --vars <vars...>", "Specific environment variables to check").action(validateEnv);
578
+ program.command("dev").description("Start Vite and Shopify theme dev servers concurrently").allowUnknownOption().allowExcessArguments().action(dev);
579
+ program.command("commit-msg").description("Validate commit message format (for git hooks)").requiredOption("-f, --file <file>", "Path to commit message file").option("-p, --prefix <prefix>", "JIRA project prefix", "AIR").action(commitMsg);
580
+ program.command("check-tag").description("Check if git tag exists for package.json version").option("-p, --package-path <path>", "Path to package.json", "package.json").option("--prefix <prefix>", "Tag prefix", "v").option("-c, --create", "Create the tag if it does not exist").option("--push", "Push the tag to remote (requires --create)").option("-r, --remote <remote>", "Remote to push to", "origin").action(checkTagCommand);
581
+ program.parse();
582
+
583
+ //#endregion
584
+ export { };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@acidgreen-au/ag-cicd-cli",
3
+ "version": "0.0.1",
4
+ "description": "Acidgreen CI/CD CLI tools",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://gitlab.com/acidgreen/internal/agcicd.git"
8
+ },
9
+ "type": "module",
10
+ "bin": {
11
+ "agci": "./dist/cli.mjs"
12
+ },
13
+ "main": "./dist/cli.mjs",
14
+ "types": "./dist/cli.d.mts",
15
+ "exports": {
16
+ ".": "./dist/cli.mjs",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "dependencies": {
23
+ "commander": "13.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@biomejs/biome": "^2.3.10",
27
+ "@types/node": "24.10.4",
28
+ "husky": "^9.1.7",
29
+ "tsdown": "^0.18.2",
30
+ "typescript": "^5.9.3",
31
+ "vitest": "^4.0.16"
32
+ },
33
+ "engines": {
34
+ "node": ">=24.0.0"
35
+ },
36
+ "volta": {
37
+ "node": "24.12.0"
38
+ },
39
+ "module": "./dist/cli.mjs",
40
+ "scripts": {
41
+ "build": "tsdown",
42
+ "cli": "node src/cli.ts",
43
+ "test": "vitest",
44
+ "test:run": "vitest run",
45
+ "biome:unsafe": "biome check --write --unsafe",
46
+ "biome:check": "biome check",
47
+ "biome:ci": "biome ci",
48
+ "typecheck": "tsc",
49
+ "precommit": "pnpm run biome:ci && pnpm run typecheck"
50
+ }
51
+ }