digpile 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/constants.js +51 -0
- package/dist/index.js +57 -0
- package/dist/types.js +1 -0
- package/dist/utils/bookmark.js +108 -0
- package/dist/utils/browser.js +63 -0
- package/dist/utils/index.js +22 -0
- package/dist/utils/platform.js +7 -0
- package/dist/utils/prompt.js +51 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# digpile
|
|
2
|
+
|
|
3
|
+
`digpile` is a small CLI that pulls one or more random bookmarks out of a bookmark folder so you can rediscover links you meant to revisit.
|
|
4
|
+
|
|
5
|
+
It can read directly from:
|
|
6
|
+
|
|
7
|
+
- Firefox profiles via `places.sqlite`
|
|
8
|
+
- Chrome bookmarks
|
|
9
|
+
- Brave bookmarks
|
|
10
|
+
- Chromium bookmarks
|
|
11
|
+
- Exported bookmarks HTML files
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js `>=22`
|
|
16
|
+
- macOS or Linux
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g digpile
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For local development:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm install
|
|
28
|
+
pnpm build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Run against an installed browser profile:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
digpile
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run against an exported bookmarks file:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
digpile ~/bookmarks.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Show help:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
digpile --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
- `-f, --folder <name>`: bookmark folder to search. Defaults to `Pile`.
|
|
54
|
+
- `-n <number>`: number of bookmarks to surface. Defaults to `1`.
|
|
55
|
+
|
|
56
|
+
## Examples
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
digpile
|
|
60
|
+
digpile --folder "read later" -n 3
|
|
61
|
+
digpile -f inbox -n 5 ~/bookmarks.html
|
|
62
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const DEFAULT_FOLDER = "Pile";
|
|
2
|
+
export const BROWSERS = new Map([
|
|
3
|
+
[
|
|
4
|
+
"firefox",
|
|
5
|
+
{
|
|
6
|
+
name: "Firefox",
|
|
7
|
+
icon: "🦊",
|
|
8
|
+
type: "sqlite",
|
|
9
|
+
paths: {
|
|
10
|
+
darwin: "~/Library/Application Support/Firefox/Profiles",
|
|
11
|
+
linux: "~/.mozilla/firefox",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
[
|
|
16
|
+
"chrome",
|
|
17
|
+
{
|
|
18
|
+
name: "Chrome",
|
|
19
|
+
icon: "🌐",
|
|
20
|
+
type: "json",
|
|
21
|
+
paths: {
|
|
22
|
+
darwin: "~/Library/Application Support/Google/Chrome",
|
|
23
|
+
linux: "~/.config/google-chrome",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"brave",
|
|
29
|
+
{
|
|
30
|
+
name: "Brave",
|
|
31
|
+
icon: "🦁",
|
|
32
|
+
type: "json",
|
|
33
|
+
paths: {
|
|
34
|
+
darwin: "~/Library/Application Support/BraveSoftware/Brave-Browser",
|
|
35
|
+
linux: "~/.config/BraveSoftware/Brave-Browser",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
"chromium",
|
|
41
|
+
{
|
|
42
|
+
name: "Chromium",
|
|
43
|
+
icon: "⚙️",
|
|
44
|
+
type: "json",
|
|
45
|
+
paths: {
|
|
46
|
+
darwin: "~/Library/Application Support/Chromium",
|
|
47
|
+
linux: "~/.config/chromium",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
]);
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { Command } from "@commander-js/extra-typings";
|
|
5
|
+
import { intro, outro } from "@clack/prompts";
|
|
6
|
+
import { DEFAULT_FOLDER } from "./constants.js";
|
|
7
|
+
import { detectAvailableBrowsers } from "./utils/browser.js";
|
|
8
|
+
import { printResult, readChromiumBookmarks, readFirefoxBookmarks, readHTMLBookmarks, } from "./utils/bookmark.js";
|
|
9
|
+
import { promptBrowserAndProfile } from "./utils/prompt.js";
|
|
10
|
+
import { fatal, getRandom } from "./utils/index.js";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("digpile")
|
|
14
|
+
.description("Dig a random link out of your bookmark pile")
|
|
15
|
+
.argument("[file]")
|
|
16
|
+
.option("-f, --folder <name>", "folder name to dig from", DEFAULT_FOLDER)
|
|
17
|
+
.option("-n <number>", "how many links to surface", (v) => parseInt(v, 10), 1)
|
|
18
|
+
.action(async (file, options) => {
|
|
19
|
+
intro(chalk.bold.green("⛏ digpile"));
|
|
20
|
+
let bookmarks;
|
|
21
|
+
if (file) {
|
|
22
|
+
if (!fs.existsSync(file)) {
|
|
23
|
+
return fatal(chalk.red(`File not found: ${file}`));
|
|
24
|
+
}
|
|
25
|
+
bookmarks = await readHTMLBookmarks(file, options.folder);
|
|
26
|
+
if (bookmarks.length === 0) {
|
|
27
|
+
return fatal(chalk.red(`Folder "${options.folder}" not found in ${file}.`) +
|
|
28
|
+
chalk.dim(" Try -f with a different folder name."));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const available = detectAvailableBrowsers();
|
|
33
|
+
const { browser, profile } = await promptBrowserAndProfile(available);
|
|
34
|
+
if (browser.browser.type === "sqlite") {
|
|
35
|
+
bookmarks = await readFirefoxBookmarks(profile.path, options.folder);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
bookmarks = await readChromiumBookmarks(profile.path, options.folder);
|
|
39
|
+
}
|
|
40
|
+
if (bookmarks.length === 0) {
|
|
41
|
+
return fatal(chalk.red(`Folder "${options.folder}" not found.`) +
|
|
42
|
+
chalk.dim(" Try -f with a different folder name."));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const picks = getRandom(bookmarks, Math.max(1, options.n));
|
|
46
|
+
printResult(picks, bookmarks.length, options.folder);
|
|
47
|
+
outro("Happy browsing!");
|
|
48
|
+
});
|
|
49
|
+
program.addHelpText("after", `
|
|
50
|
+
Examples:
|
|
51
|
+
digpile
|
|
52
|
+
digpile --folder "read later" -n 3
|
|
53
|
+
digpile -f inbox -n 5 ~/bookmarks.html
|
|
54
|
+
|
|
55
|
+
Supported browsers: Firefox 🦊 Chrome 🌐 Brave 🦁 Chromium ⚙️
|
|
56
|
+
Supported platforms: macOS · Linux`);
|
|
57
|
+
program.parse();
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { box, log } from "@clack/prompts";
|
|
2
|
+
import { readFile, copyFile, unlink } from "fs/promises";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import { JSDOM } from "jsdom";
|
|
8
|
+
import * as v from "valibot";
|
|
9
|
+
export const readHTMLBookmarks = async (filePath, folderName) => {
|
|
10
|
+
const html = await readFile(filePath, "utf8");
|
|
11
|
+
const dom = new JSDOM(html);
|
|
12
|
+
const doc = dom.window.document;
|
|
13
|
+
const headers = Array.from(doc.querySelectorAll("h3"));
|
|
14
|
+
const targetHeader = headers.find((header) => header.textContent?.trim().toLowerCase() === folderName.toLowerCase());
|
|
15
|
+
const container = targetHeader?.nextElementSibling;
|
|
16
|
+
if (!container)
|
|
17
|
+
return [];
|
|
18
|
+
return Array.from(container.querySelectorAll("a")).map((anchor) => ({
|
|
19
|
+
title: anchor.textContent?.trim() || anchor.href,
|
|
20
|
+
url: anchor.href,
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
export const readFirefoxBookmarks = async (dbPath, folderName) => {
|
|
24
|
+
const tmpPath = join(tmpdir(), `digpile-${Date.now()}.sqlite`);
|
|
25
|
+
await copyFile(dbPath, tmpPath);
|
|
26
|
+
try {
|
|
27
|
+
const db = new Database(tmpPath, { readonly: true });
|
|
28
|
+
const folderRows = db
|
|
29
|
+
.prepare("SELECT id FROM moz_bookmarks WHERE type = 2 AND LOWER(title) = LOWER(?)")
|
|
30
|
+
.all(folderName);
|
|
31
|
+
if (folderRows.length === 0) {
|
|
32
|
+
db.close();
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const folderIds = folderRows.map((row) => row.id);
|
|
36
|
+
const placeholders = folderIds.map(() => "?").join(",");
|
|
37
|
+
const rows = db
|
|
38
|
+
.prepare(`WITH RECURSIVE subtree(id, type, fk, title) AS (
|
|
39
|
+
SELECT id, type, fk, title FROM moz_bookmarks
|
|
40
|
+
WHERE parent IN (${placeholders})
|
|
41
|
+
UNION ALL
|
|
42
|
+
SELECT b.id, b.type, b.fk, b.title
|
|
43
|
+
FROM moz_bookmarks b JOIN subtree s ON b.parent = s.id
|
|
44
|
+
)
|
|
45
|
+
SELECT s.title, p.url
|
|
46
|
+
FROM subtree s JOIN moz_places p ON s.fk = p.id
|
|
47
|
+
WHERE s.type = 1 AND p.url NOT LIKE 'place:%'`)
|
|
48
|
+
.all(...folderIds);
|
|
49
|
+
db.close();
|
|
50
|
+
return rows.map((row) => ({
|
|
51
|
+
title: row.title || row.url,
|
|
52
|
+
url: row.url,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
try {
|
|
57
|
+
await unlink(tmpPath);
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const ChromiumBookmarkNode = v.union([
|
|
63
|
+
v.object({
|
|
64
|
+
type: v.literal("folder"),
|
|
65
|
+
name: v.string(),
|
|
66
|
+
children: v.array(v.lazy(() => ChromiumBookmarkNode)),
|
|
67
|
+
}),
|
|
68
|
+
v.object({ type: v.literal("url"), name: v.string(), url: v.string() }),
|
|
69
|
+
]);
|
|
70
|
+
const ChromiumBookmarksFile = v.object({
|
|
71
|
+
roots: v.record(v.string(), ChromiumBookmarkNode),
|
|
72
|
+
});
|
|
73
|
+
export const readChromiumBookmarks = async (bookmarksPath, folderName) => {
|
|
74
|
+
const raw = await v.parseAsync(ChromiumBookmarksFile, JSON.parse(await readFile(bookmarksPath, "utf8")));
|
|
75
|
+
const bookmarks = [];
|
|
76
|
+
const collectAll = (node) => {
|
|
77
|
+
if (node.type === "url") {
|
|
78
|
+
bookmarks.push({ title: node.name, url: node.url });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const child of node.children) {
|
|
82
|
+
collectAll(child);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const findFolder = (node) => {
|
|
86
|
+
if (node.type !== "folder")
|
|
87
|
+
return false;
|
|
88
|
+
if (node.name.toLowerCase() === folderName.toLowerCase()) {
|
|
89
|
+
collectAll(node);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return node.children.some(findFolder);
|
|
93
|
+
};
|
|
94
|
+
for (const root of Object.values(raw.roots)) {
|
|
95
|
+
if (findFolder(root))
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
return bookmarks;
|
|
99
|
+
};
|
|
100
|
+
export const printResult = (bookmarks, total, folder) => {
|
|
101
|
+
box(bookmarks
|
|
102
|
+
.map((bookmark) => `${chalk.bold(bookmark.title)}\n${chalk.cyan(` ${bookmark.url}`)}`)
|
|
103
|
+
.join("\n\n"), "", {
|
|
104
|
+
rounded: true,
|
|
105
|
+
formatBorder: (text) => chalk.dim(text),
|
|
106
|
+
});
|
|
107
|
+
log.success(chalk.dim(`${bookmarks.length} of ${total} bookmarks surfaced from ` + chalk.italic(`"${folder}"`)));
|
|
108
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { BROWSERS } from "../constants.js";
|
|
4
|
+
import { expandHome } from "./index.js";
|
|
5
|
+
import { detectPlatform } from "./platform.js";
|
|
6
|
+
const findFirefoxProfiles = (basePath) => {
|
|
7
|
+
const dir = expandHome(basePath);
|
|
8
|
+
if (!fs.existsSync(dir))
|
|
9
|
+
return [];
|
|
10
|
+
return fs
|
|
11
|
+
.readdirSync(dir)
|
|
12
|
+
.filter((name) => {
|
|
13
|
+
try {
|
|
14
|
+
return (fs.statSync(join(dir, name)).isDirectory() &&
|
|
15
|
+
fs.existsSync(join(dir, name, "places.sqlite")));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
.map((name) => ({
|
|
22
|
+
name,
|
|
23
|
+
path: join(dir, name, "places.sqlite"),
|
|
24
|
+
}));
|
|
25
|
+
};
|
|
26
|
+
const findChromiumProfiles = (basePath) => {
|
|
27
|
+
const dir = expandHome(basePath);
|
|
28
|
+
if (!fs.existsSync(dir))
|
|
29
|
+
return [];
|
|
30
|
+
const profiles = [];
|
|
31
|
+
const defaultBookmarksPath = join(dir, "Default", "Bookmarks");
|
|
32
|
+
if (fs.existsSync(defaultBookmarksPath)) {
|
|
33
|
+
profiles.push({ name: "Default", path: defaultBookmarksPath });
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
fs.readdirSync(dir)
|
|
37
|
+
.filter((name) => /^Profile \d+$/.test(name))
|
|
38
|
+
.forEach((name) => {
|
|
39
|
+
const bookmarksPath = join(dir, name, "Bookmarks");
|
|
40
|
+
if (fs.existsSync(bookmarksPath)) {
|
|
41
|
+
profiles.push({ name, path: bookmarksPath });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
return profiles;
|
|
47
|
+
};
|
|
48
|
+
export const detectAvailableBrowsers = () => {
|
|
49
|
+
const platform = detectPlatform();
|
|
50
|
+
if (!platform)
|
|
51
|
+
return [];
|
|
52
|
+
const found = [];
|
|
53
|
+
for (const [key, browser] of BROWSERS) {
|
|
54
|
+
const basePath = browser.paths[platform];
|
|
55
|
+
if (!basePath)
|
|
56
|
+
continue;
|
|
57
|
+
const profiles = browser.type === "sqlite" ? findFirefoxProfiles(basePath) : findChromiumProfiles(basePath);
|
|
58
|
+
if (profiles.length > 0) {
|
|
59
|
+
found.push({ key, browser, profiles });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cancel } from "@clack/prompts";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
export const expandHome = (path) => path.replace(/^~/, homedir());
|
|
5
|
+
export const fatal = (message) => {
|
|
6
|
+
cancel(message);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
};
|
|
9
|
+
export const nonFatal = (message) => {
|
|
10
|
+
cancel(message);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
};
|
|
13
|
+
export const exists = async (f) => {
|
|
14
|
+
try {
|
|
15
|
+
await stat(f);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const getRandom = (arr, n) => arr.toSorted(() => Math.random() - 0.5).slice(0, n);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { isCancel, log, select } from "@clack/prompts";
|
|
3
|
+
import { fatal, nonFatal } from "./index.js";
|
|
4
|
+
export const promptBrowserAndProfile = async (available) => {
|
|
5
|
+
if (available.length === 0) {
|
|
6
|
+
return fatal("No supported browsers found. Supported: Firefox, Chrome, Brave, Chromium.");
|
|
7
|
+
}
|
|
8
|
+
let selectedBrowser = available[0];
|
|
9
|
+
if (available.length === 1) {
|
|
10
|
+
log.info(chalk.dim(`Using ${selectedBrowser.browser.icon} ${selectedBrowser.browser.name}`));
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const key = await select({
|
|
14
|
+
message: "Browser",
|
|
15
|
+
options: available.map(({ browser, key }) => ({
|
|
16
|
+
label: `${browser.icon} ${browser.name}`,
|
|
17
|
+
value: key,
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
if (isCancel(key)) {
|
|
21
|
+
return nonFatal("Operation cancelled");
|
|
22
|
+
}
|
|
23
|
+
const browser = available.find((entry) => entry.key === key);
|
|
24
|
+
if (!browser) {
|
|
25
|
+
return fatal(`Unknown browser selection: ${key}`);
|
|
26
|
+
}
|
|
27
|
+
selectedBrowser = browser;
|
|
28
|
+
}
|
|
29
|
+
let selectedProfile = selectedBrowser.profiles[0];
|
|
30
|
+
if (selectedBrowser.profiles.length === 1) {
|
|
31
|
+
log.info(chalk.dim(`Profile: ${selectedProfile.name}`));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const profileName = await select({
|
|
35
|
+
message: "Profile",
|
|
36
|
+
options: selectedBrowser.profiles.map(({ name }) => ({
|
|
37
|
+
label: name,
|
|
38
|
+
value: name,
|
|
39
|
+
})),
|
|
40
|
+
});
|
|
41
|
+
if (isCancel(profileName)) {
|
|
42
|
+
return nonFatal("Operation cancelled");
|
|
43
|
+
}
|
|
44
|
+
const profile = selectedBrowser.profiles.find((entry) => entry.name === profileName);
|
|
45
|
+
if (!profile) {
|
|
46
|
+
return fatal(`Unknown profile selection: ${profileName}`);
|
|
47
|
+
}
|
|
48
|
+
selectedProfile = profile;
|
|
49
|
+
}
|
|
50
|
+
return { browser: selectedBrowser, profile: selectedProfile };
|
|
51
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "digpile",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dig a random link out of your bookmark pile",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bookmarks",
|
|
7
|
+
"chrome",
|
|
8
|
+
"cli",
|
|
9
|
+
"firefox",
|
|
10
|
+
"productivity",
|
|
11
|
+
"random"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "github:masiama/digpile",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/masiama/digpile.git"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"digpile": "dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"os": [
|
|
27
|
+
"darwin",
|
|
28
|
+
"linux"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "rm -rf ./dist && tsc -p tsconfig.build.json",
|
|
33
|
+
"prepack": "pnpm build",
|
|
34
|
+
"start": "node ./src/index.ts",
|
|
35
|
+
"start:dist": "node ./dist/index.js",
|
|
36
|
+
"lint": "oxlint",
|
|
37
|
+
"lint:fix": "oxlint --fix",
|
|
38
|
+
"fmt": "oxfmt",
|
|
39
|
+
"fmt:check": "oxfmt --check",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.4.0",
|
|
44
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
45
|
+
"better-sqlite3": "12.10.0",
|
|
46
|
+
"chalk": "^5.6.2",
|
|
47
|
+
"commander": "^14.0.3",
|
|
48
|
+
"jsdom": "^29.1.1",
|
|
49
|
+
"valibot": "^1.4.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
53
|
+
"@types/jsdom": "^28.0.3",
|
|
54
|
+
"@types/node": "^25.7.0",
|
|
55
|
+
"oxfmt": "^0.49.0",
|
|
56
|
+
"oxlint": "^1.64.0",
|
|
57
|
+
"typescript": "^6.0.3"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=22"
|
|
61
|
+
}
|
|
62
|
+
}
|