@dahch/heimdall 1.0.0 โ†’ 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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/cli.js +290 -96
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  2. **Aggregated Review & Confirmation**: Shows an interactive tabular overview comparing current and latest package versions across all managers, prompting for confirmation before modifying the system.
21
21
  - ๐Ÿงช **True Simulated Dry-Run (`--dry-run`)**: Traverses the full engine lifecycle, formats outdated packages, and executes mock update routines step-by-step with real-time feedback without touching system files.
22
22
  - โšก **High-Speed Registry Queries**: Yarn and pipx bypass heavy CLI subprocess spawning by querying the npm Registry and PyPI JSON APIs directly over HTTP with strict 3-second abort timeouts.
23
- - ๐Ÿ” **Non-Interactive Sudo Pre-Checks**: Privileged managers (MacPorts, Linux APT) verify cached credentials using `sudo -n true` beforehand, preventing terminal lockups and password prompt hangs.
23
+ - ๐Ÿ” **Non-Interactive Sudo Pre-Checks**: Privileged managers (MacPorts, Linux APT, Linux DNF) verify cached credentials using `sudo -n true` beforehand, preventing terminal lockups and password prompt hangs.
24
24
  - ๐ŸŽจ **Modern Minimalist UX**: Terminal interface powered by `@clack/prompts`, micro-border tabular layouts with directional version transitions (`current โ†’ latest`), persistent real-time step progression with elapsed durations, and actionable error extraction.
25
25
  - ๐Ÿท **Dynamic Version Resolution**: Reads package name and version dynamically from `package.json` at runtime via `import.meta.url`, ensuring banners and `--version` flags remain in sync with repository releases.
26
26
  - โš™ **Flexible Targeting**: Run interactively, execute non-interactively with `--yes`, or filter target managers using `--only` and `--exclude`.
@@ -29,7 +29,7 @@
29
29
 
30
30
  ## ๐Ÿ“ฆ Supported Package Managers
31
31
 
32
- Heimdall (`hmd`) supports 13 package managers across system, runtime, language, and application store categories:
32
+ Heimdall (`hmd`) supports 14 package managers across system, runtime, language, and application store categories:
33
33
 
34
34
  | Manager | Icon | Category | Detection Strategy | Update Routine & Resilience Rules |
35
35
  | :--- | :---: | :---: | :--- | :--- |
@@ -45,6 +45,7 @@ Heimdall (`hmd`) supports 13 package managers across system, runtime, language,
45
45
  | **[MacPorts](https://www.macports.org)** | โš“ | `system` | `port outdated` + non-interactive `sudo -n true` verification | 1. `sudo port selfupdate`<br>2. `sudo port upgrade outdated` |
46
46
  | **[Mac App Store (mas)](https://github.com/mas-cli/mas)** | ๐ŸŽ | `appstore` | `mas outdated` | `mas upgrade` |
47
47
  | **[APT (Debian/Ubuntu)](https://wiki.debian.org/Apt)** | ๐Ÿง | `system` | `apt list --upgradable` + non-interactive `sudo -n true` check | 1. `sudo apt-get update -y`<br>2. `sudo apt-get upgrade -y`<br>3. `sudo apt-get autoremove -y` |
48
+ | **[DNF (Fedora/RHEL)](https://docs.fedoraproject.org/en-US/quick-docs/dnf/)** | ๐Ÿ“ฆ | `system` | `dnf check-update -y` (exit code 100 signals updates) + `sudo -n true` check | 1. `sudo dnf upgrade -y --refresh`<br>2. `sudo dnf autoremove -y` (prompted interactively with confirmation prompt, auto-approved with `--yes` or `--dry-run`) |
48
49
  | **[Flatpak](https://flatpak.org)** | ๐Ÿ“ฆ | `system` | `flatpak remote-ls --updates` | `flatpak update -y --noninteractive` |
49
50
 
50
51
  ---
@@ -178,7 +179,7 @@ Heimdall employs an automated GitHub Actions CI/CD workflow ([`.github/workflows
178
179
 
179
180
  - [SPEC.md](SPEC.md): Technical specification, CLI contract, data models, and exit codes.
180
181
  - [DESIGN.md](DESIGN.md): System architecture, Two-Phase workflow, Engine concurrency pool, and Mermaid diagrams.
181
- - [ADR.md](ADR.md): Architectural Decision Records (ADR-001 through ADR-009) documenting key engineering decisions.
182
+ - [ADR.md](ADR.md): Architectural Decision Records (ADR-001 through ADR-010) documenting key engineering decisions.
182
183
  - [AGENTS.md](AGENTS.md): Developer and AI agent reference manual for maintaining and extending the codebase.
183
184
 
184
185
  ---
package/dist/cli.js CHANGED
@@ -1513,6 +1513,101 @@ var MacAppStoreManager = class extends BasePackageManager {
1513
1513
  }
1514
1514
  };
1515
1515
 
1516
+ // src/utils/formatting.ts
1517
+ import pc from "picocolors";
1518
+ import Table from "cli-table3";
1519
+ function formatDuration(ms) {
1520
+ if (ms < 1e3) return `${ms}ms`;
1521
+ const seconds = (ms / 1e3).toFixed(1);
1522
+ return `${seconds}s`;
1523
+ }
1524
+ function renderUpdatesTable(items) {
1525
+ if (items.length === 0) {
1526
+ return pc.dim(" No updates pending.");
1527
+ }
1528
+ const table = new Table({
1529
+ head: [
1530
+ pc.dim("Manager"),
1531
+ pc.dim("Package"),
1532
+ pc.dim("Type"),
1533
+ pc.dim("Version Transition")
1534
+ ],
1535
+ style: {
1536
+ head: [],
1537
+ border: ["dim"]
1538
+ },
1539
+ chars: {
1540
+ top: "\u2500",
1541
+ "top-mid": "\u2500",
1542
+ "top-left": "\u250C",
1543
+ "top-right": "\u2510",
1544
+ bottom: "\u2500",
1545
+ "bottom-mid": "\u2500",
1546
+ "bottom-left": "\u2514",
1547
+ "bottom-right": "\u2518",
1548
+ left: "\u2502",
1549
+ "left-mid": "\u251C",
1550
+ mid: "\u2500",
1551
+ "mid-mid": "\u253C",
1552
+ right: "\u2502",
1553
+ "right-mid": "\u2524",
1554
+ middle: "\u2502"
1555
+ }
1556
+ });
1557
+ for (const item of items) {
1558
+ const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
1559
+ const currentVer = pc.yellow(item.currentVersion || "?");
1560
+ const arrow = pc.dim(" \u2192 ");
1561
+ const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
1562
+ table.push([
1563
+ pc.cyan(item.managerName),
1564
+ pc.bold(pc.white(item.name)),
1565
+ typeLabel,
1566
+ `${currentVer}${arrow}${latestVer}`
1567
+ ]);
1568
+ }
1569
+ return table.toString();
1570
+ }
1571
+ function renderExecutionSummary(results) {
1572
+ const total = results.length;
1573
+ const successful = results.filter((r) => r.success).length;
1574
+ const failed = results.filter((r) => !r.success).length;
1575
+ const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
1576
+ const lines = [];
1577
+ lines.push("");
1578
+ lines.push(pc.dim("\u2500".repeat(60)));
1579
+ lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
1580
+ lines.push(pc.dim("\u2500".repeat(60)));
1581
+ for (const res of results) {
1582
+ const icon = res.icon || "\u{1F4E6}";
1583
+ const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
1584
+ const time = pc.dim(`(${formatDuration(res.durationMs)})`);
1585
+ const count = res.success ? pc.dim(`\u2022 ${res.updatedCount} updated`) : pc.dim(`\u2022 ${res.updatedCount} updated, ${res.steps.filter((s) => s.status === "failed").length} issue(s)`);
1586
+ lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
1587
+ if (!res.success && res.error) {
1588
+ lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
1589
+ }
1590
+ if (res.steps.length > 0) {
1591
+ for (const step of res.steps) {
1592
+ if (step.status === "skipped") continue;
1593
+ const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
1594
+ const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
1595
+ const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
1596
+ lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
1597
+ }
1598
+ }
1599
+ }
1600
+ lines.push(pc.dim("\u2500".repeat(60)));
1601
+ const summaryParts = [
1602
+ pc.green(`${successful}/${total} managers succeeded`),
1603
+ failed > 0 ? pc.red(`${failed} failed`) : null,
1604
+ pc.cyan(`${totalUpdated} package(s) updated`)
1605
+ ].filter(Boolean);
1606
+ lines.push(` ${summaryParts.join(" \u2022 ")}`);
1607
+ lines.push(pc.dim("\u2500".repeat(60)));
1608
+ return lines.join("\n");
1609
+ }
1610
+
1516
1611
  // src/managers/linux.ts
1517
1612
  var AptManager = class extends BasePackageManager {
1518
1613
  id = "apt";
@@ -1720,6 +1815,192 @@ var FlatpakManager = class extends BasePackageManager {
1720
1815
  };
1721
1816
  }
1722
1817
  };
1818
+ var DnfManager = class extends BasePackageManager {
1819
+ id = "dnf";
1820
+ name = "DNF (Fedora/RHEL)";
1821
+ icon = "\u{1F4E6}";
1822
+ category = "system";
1823
+ binary = "dnf";
1824
+ async checkUpdates(options) {
1825
+ const startTime = Date.now();
1826
+ const isAvail = await this.isAvailable();
1827
+ if (!isAvail) {
1828
+ return {
1829
+ managerId: this.id,
1830
+ managerName: this.name,
1831
+ icon: this.icon,
1832
+ category: this.category,
1833
+ available: false,
1834
+ updates: [],
1835
+ durationMs: 0
1836
+ };
1837
+ }
1838
+ try {
1839
+ const execRes = await safeExec(this.binary, ["check-update", "-y"], {
1840
+ timeoutMs: options?.timeoutMs ?? 25e3
1841
+ });
1842
+ if (execRes.exitCode !== 0 && execRes.exitCode !== 100) {
1843
+ return {
1844
+ managerId: this.id,
1845
+ managerName: this.name,
1846
+ icon: this.icon,
1847
+ category: this.category,
1848
+ available: true,
1849
+ updates: [],
1850
+ durationMs: Date.now() - startTime,
1851
+ error: (execRes.stderr || execRes.stdout).trim() ? extractErrorMessage(execRes.stderr || execRes.stdout) : `dnf check-update failed with code ${execRes.exitCode}`
1852
+ };
1853
+ }
1854
+ if (execRes.exitCode === 0) {
1855
+ return {
1856
+ managerId: this.id,
1857
+ managerName: this.name,
1858
+ icon: this.icon,
1859
+ category: this.category,
1860
+ available: true,
1861
+ updates: [],
1862
+ durationMs: Date.now() - startTime
1863
+ };
1864
+ }
1865
+ const updates = [];
1866
+ const lines = execRes.stdout.split("\n");
1867
+ let pendingPkg = null;
1868
+ for (const rawLine of lines) {
1869
+ const line = rawLine.trim();
1870
+ if (!line) continue;
1871
+ if (line.startsWith("Updating and loading repositories") || line.startsWith("Repositories loaded") || line.startsWith("Last metadata expiration check") || line.startsWith("Obsoleting Packages") || line.startsWith("Loaded plugins") || line.startsWith("Security:")) {
1872
+ continue;
1873
+ }
1874
+ const combinedLine = pendingPkg ? `${pendingPkg} ${line}` : line;
1875
+ const match = combinedLine.match(/^(\S+?)\.([a-zA-Z0-9_]+)\s+(\S+)\s+(\S+)/);
1876
+ if (match) {
1877
+ const [, name, arch, latestVersion, repo] = match;
1878
+ updates.push({
1879
+ managerId: this.id,
1880
+ managerName: this.name,
1881
+ name,
1882
+ currentVersion: "installed",
1883
+ latestVersion,
1884
+ type: "rpm",
1885
+ extra: `${repo} (${arch})`
1886
+ });
1887
+ pendingPkg = null;
1888
+ } else if (!pendingPkg && /^\S+?\.[a-zA-Z0-9_]+$/.test(line)) {
1889
+ pendingPkg = line;
1890
+ } else {
1891
+ pendingPkg = null;
1892
+ }
1893
+ }
1894
+ return {
1895
+ managerId: this.id,
1896
+ managerName: this.name,
1897
+ icon: this.icon,
1898
+ category: this.category,
1899
+ available: true,
1900
+ updates,
1901
+ durationMs: Date.now() - startTime
1902
+ };
1903
+ } catch (err) {
1904
+ return {
1905
+ managerId: this.id,
1906
+ managerName: this.name,
1907
+ icon: this.icon,
1908
+ category: this.category,
1909
+ available: true,
1910
+ updates: [],
1911
+ durationMs: Date.now() - startTime,
1912
+ error: err instanceof Error ? err.message : String(err)
1913
+ };
1914
+ }
1915
+ }
1916
+ async executeUpdate(items, options) {
1917
+ const startTime = Date.now();
1918
+ const steps = [];
1919
+ if (!options.dryRun && process.getuid?.() !== 0) {
1920
+ const sudoCheck = await safeExec("sudo", ["-n", "true"], { timeoutMs: 3e3 });
1921
+ if (!sudoCheck.success) {
1922
+ return {
1923
+ managerId: this.id,
1924
+ managerName: this.name,
1925
+ icon: this.icon,
1926
+ success: false,
1927
+ updatedCount: 0,
1928
+ durationMs: Date.now() - startTime,
1929
+ steps: [
1930
+ {
1931
+ name: "sudo privileges",
1932
+ command: "sudo -n true",
1933
+ status: "failed",
1934
+ error: 'DNF requires sudo privileges. Run "sudo -v" before running hmd.'
1935
+ }
1936
+ ],
1937
+ error: 'DNF requires sudo privileges. Run "sudo -v" before running hmd.'
1938
+ };
1939
+ }
1940
+ }
1941
+ const upgradeStep = await this.executeStep(
1942
+ "sudo dnf upgrade --refresh",
1943
+ "sudo",
1944
+ ["dnf", "upgrade", "-y", "--refresh"],
1945
+ options
1946
+ );
1947
+ steps.push(upgradeStep);
1948
+ if (upgradeStep.status === "success") {
1949
+ if (options.dryRun) {
1950
+ const autoremoveStep = await this.executeStep(
1951
+ "sudo dnf autoremove",
1952
+ "sudo",
1953
+ ["dnf", "autoremove", "-y"],
1954
+ options
1955
+ );
1956
+ steps.push(autoremoveStep);
1957
+ } else if (options.confirmAction) {
1958
+ const shouldAutoremove = await options.confirmAction(
1959
+ "Do you want to run sudo dnf autoremove to clean up orphaned packages?"
1960
+ );
1961
+ if (shouldAutoremove) {
1962
+ const autoremoveStep = await this.executeStep(
1963
+ "sudo dnf autoremove",
1964
+ "sudo",
1965
+ ["dnf", "autoremove", "-y"],
1966
+ options
1967
+ );
1968
+ steps.push(autoremoveStep);
1969
+ } else {
1970
+ steps.push({
1971
+ name: "sudo dnf autoremove",
1972
+ command: "sudo dnf autoremove -y",
1973
+ status: "skipped"
1974
+ });
1975
+ }
1976
+ } else {
1977
+ steps.push({
1978
+ name: "sudo dnf autoremove",
1979
+ command: "sudo dnf autoremove -y",
1980
+ status: "skipped"
1981
+ });
1982
+ }
1983
+ } else {
1984
+ steps.push({
1985
+ name: "sudo dnf autoremove",
1986
+ command: "sudo dnf autoremove -y",
1987
+ status: "skipped"
1988
+ });
1989
+ }
1990
+ const nonSkippedSteps = steps.filter((s) => s.status !== "skipped");
1991
+ const success = nonSkippedSteps.length > 0 && nonSkippedSteps.every((s) => s.status === "success");
1992
+ return {
1993
+ managerId: this.id,
1994
+ managerName: this.name,
1995
+ icon: this.icon,
1996
+ success,
1997
+ updatedCount: success ? items.length : 0,
1998
+ durationMs: Date.now() - startTime,
1999
+ steps,
2000
+ error: !success ? steps.find((s) => s.status === "failed")?.error : void 0
2001
+ };
2002
+ }
2003
+ };
1723
2004
 
1724
2005
  // src/managers/registry.ts
1725
2006
  function createDefaultManagers() {
@@ -1736,6 +2017,7 @@ function createDefaultManagers() {
1736
2017
  new MacPortsManager(),
1737
2018
  new MacAppStoreManager(),
1738
2019
  new AptManager(),
2020
+ new DnfManager(),
1739
2021
  new FlatpakManager()
1740
2022
  ];
1741
2023
  }
@@ -1872,108 +2154,13 @@ var UpdaterEngine = class {
1872
2154
  }
1873
2155
  };
1874
2156
 
1875
- // src/utils/formatting.ts
1876
- import pc from "picocolors";
1877
- import Table from "cli-table3";
1878
- function formatDuration(ms) {
1879
- if (ms < 1e3) return `${ms}ms`;
1880
- const seconds = (ms / 1e3).toFixed(1);
1881
- return `${seconds}s`;
1882
- }
1883
- function renderUpdatesTable(items) {
1884
- if (items.length === 0) {
1885
- return pc.dim(" No updates pending.");
1886
- }
1887
- const table = new Table({
1888
- head: [
1889
- pc.dim("Manager"),
1890
- pc.dim("Package"),
1891
- pc.dim("Type"),
1892
- pc.dim("Version Transition")
1893
- ],
1894
- style: {
1895
- head: [],
1896
- border: ["dim"]
1897
- },
1898
- chars: {
1899
- top: "\u2500",
1900
- "top-mid": "\u2500",
1901
- "top-left": "\u250C",
1902
- "top-right": "\u2510",
1903
- bottom: "\u2500",
1904
- "bottom-mid": "\u2500",
1905
- "bottom-left": "\u2514",
1906
- "bottom-right": "\u2518",
1907
- left: "\u2502",
1908
- "left-mid": "\u251C",
1909
- mid: "\u2500",
1910
- "mid-mid": "\u253C",
1911
- right: "\u2502",
1912
- "right-mid": "\u2524",
1913
- middle: "\u2502"
1914
- }
1915
- });
1916
- for (const item of items) {
1917
- const typeLabel = item.type ? pc.dim(`[${item.type}]`) : pc.dim("-");
1918
- const currentVer = pc.yellow(item.currentVersion || "?");
1919
- const arrow = pc.dim(" \u2192 ");
1920
- const latestVer = pc.green(pc.bold(item.latestVersion || "?"));
1921
- table.push([
1922
- pc.cyan(item.managerName),
1923
- pc.bold(pc.white(item.name)),
1924
- typeLabel,
1925
- `${currentVer}${arrow}${latestVer}`
1926
- ]);
1927
- }
1928
- return table.toString();
1929
- }
1930
- function renderExecutionSummary(results) {
1931
- const total = results.length;
1932
- const successful = results.filter((r) => r.success).length;
1933
- const failed = results.filter((r) => !r.success).length;
1934
- const totalUpdated = results.reduce((acc, r) => acc + r.updatedCount, 0);
1935
- const lines = [];
1936
- lines.push("");
1937
- lines.push(pc.dim("\u2500".repeat(60)));
1938
- lines.push(` ${pc.bold(pc.cyan("Heimdall"))} ${pc.dim("\u2022 Execution Summary")}`);
1939
- lines.push(pc.dim("\u2500".repeat(60)));
1940
- for (const res of results) {
1941
- const icon = res.icon || "\u{1F4E6}";
1942
- const status = res.success ? pc.green("\u2714 SUCCESS") : pc.red("\u2716 FAILED");
1943
- const time = pc.dim(`(${formatDuration(res.durationMs)})`);
1944
- const count = res.success ? pc.dim(`\u2022 ${res.updatedCount} updated`) : pc.dim(`\u2022 ${res.updatedCount} updated, ${res.steps.filter((s) => s.status === "failed").length} issue(s)`);
1945
- lines.push(` ${icon} ${pc.bold(res.managerName)} ${status} ${count} ${time}`);
1946
- if (!res.success && res.error) {
1947
- lines.push(` ${pc.red("\u21B3 Error:")} ${pc.dim(res.error.trim().split("\n")[0])}`);
1948
- }
1949
- if (res.steps.length > 0) {
1950
- for (const step of res.steps) {
1951
- if (step.status === "skipped") continue;
1952
- const stepStatus = step.status === "success" ? pc.green("\u2714") : step.status === "failed" ? pc.red("\u2716") : pc.dim("\u25CB");
1953
- const stepDuration = step.durationMs ? pc.dim(`[${formatDuration(step.durationMs)}]`) : "";
1954
- const stepError = step.error && step.status === "failed" ? pc.red(` (${step.error.split("\n")[0]})`) : "";
1955
- lines.push(` ${stepStatus} ${pc.dim(step.name)} ${stepDuration}${stepError}`);
1956
- }
1957
- }
1958
- }
1959
- lines.push(pc.dim("\u2500".repeat(60)));
1960
- const summaryParts = [
1961
- pc.green(`${successful}/${total} managers succeeded`),
1962
- failed > 0 ? pc.red(`${failed} failed`) : null,
1963
- pc.cyan(`${totalUpdated} package(s) updated`)
1964
- ].filter(Boolean);
1965
- lines.push(` ${summaryParts.join(" \u2022 ")}`);
1966
- lines.push(pc.dim("\u2500".repeat(60)));
1967
- return lines.join("\n");
1968
- }
1969
-
1970
2157
  // src/utils/version.ts
1971
2158
  import { fileURLToPath } from "url";
1972
2159
  import { dirname, resolve } from "path";
1973
2160
  import { existsSync, readFileSync } from "fs";
1974
2161
  var FALLBACK_METADATA = {
1975
2162
  name: "@dahch/heimdall",
1976
- version: "1.0.0",
2163
+ version: "1.0.1",
1977
2164
  description: "Heimdall: resilient validator and updater for system and global packages"
1978
2165
  };
1979
2166
  var cachedMetadata = null;
@@ -2170,6 +2357,13 @@ ${isSimulated ? "[DRY RUN] Simulating" : "Starting"} updates for ${managersToUpd
2170
2357
  {
2171
2358
  verbose: options.verbose,
2172
2359
  dryRun: isSimulated,
2360
+ confirmAction: async (message) => {
2361
+ if (options.yes) return true;
2362
+ updateSpinner.stop(pc2.dim("Waiting for user confirmation..."));
2363
+ const confirmed = await p.confirm({ message, initialValue: false });
2364
+ updateSpinner.start(pc2.dim("Continuing update..."));
2365
+ return typeof confirmed === "boolean" ? confirmed : false;
2366
+ },
2173
2367
  onStepStart: (stepName) => {
2174
2368
  updateSpinner.message(pc2.dim(`Running: ${stepName}...`));
2175
2369
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dahch/heimdall",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Heimdall: resilient validator and updater for system and global packages",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -10,8 +10,8 @@
10
10
  "dist"
11
11
  ],
12
12
  "bin": {
13
- "hmd": "./dist/cli.js",
14
- "heimdall": "./dist/cli.js"
13
+ "hmd": "dist/cli.js",
14
+ "heimdall": "dist/cli.js"
15
15
  },
16
16
  "scripts": {
17
17
  "dev": "tsx src/cli.ts",