@hasna/connectors 0.2.4 → 0.2.5
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/bin/index.js +298 -9
- package/bin/mcp.js +1 -1
- package/package.json +2 -2
package/bin/index.js
CHANGED
|
@@ -6496,14 +6496,15 @@ function App({ initialConnectors, overwrite = false }) {
|
|
|
6496
6496
|
init_registry();
|
|
6497
6497
|
init_installer();
|
|
6498
6498
|
init_auth();
|
|
6499
|
-
import { readdirSync as readdirSync4, statSync as statSync3 } from "fs";
|
|
6499
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5, statSync as statSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
6500
|
+
import { homedir as homedir3 } from "os";
|
|
6500
6501
|
import { join as join5, relative } from "path";
|
|
6501
6502
|
import { createInterface } from "readline";
|
|
6502
6503
|
import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
6503
6504
|
loadConnectorVersions();
|
|
6504
6505
|
var isTTY = process.stdout.isTTY ?? false;
|
|
6505
6506
|
var program2 = new Command;
|
|
6506
|
-
program2.name("connectors").description("Install API connectors for your project").version("0.2.
|
|
6507
|
+
program2.name("connectors").description("Install API connectors for your project").version("0.2.5");
|
|
6507
6508
|
program2.command("interactive", { isDefault: true }).alias("i").description("Interactive connector browser").action(() => {
|
|
6508
6509
|
if (!isTTY) {
|
|
6509
6510
|
console.log(`Non-interactive environment detected. Use a subcommand:
|
|
@@ -6533,7 +6534,22 @@ function listFilesRecursive(dir, base = dir) {
|
|
|
6533
6534
|
}
|
|
6534
6535
|
return files;
|
|
6535
6536
|
}
|
|
6536
|
-
program2.command("install").alias("add").argument("[connectors...]", "Connectors to install").option("-o, --overwrite", "Overwrite existing connectors", false).option("-d, --dry-run", "Preview what would be installed without making changes", false).option("--json", "Output results as JSON", false).description("Install one or more connectors").action((connectors, options) => {
|
|
6537
|
+
program2.command("install").alias("add").argument("[connectors...]", "Connectors to install").option("-o, --overwrite", "Overwrite existing connectors", false).option("-d, --dry-run", "Preview what would be installed without making changes", false).option("-c, --category <category>", "Install all connectors in a category").option("--json", "Output results as JSON", false).description("Install one or more connectors").action((connectors, options) => {
|
|
6538
|
+
if (options.category) {
|
|
6539
|
+
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
6540
|
+
if (!category) {
|
|
6541
|
+
if (options.json) {
|
|
6542
|
+
console.log(JSON.stringify({ error: `Unknown category: ${options.category}. Available: ${CATEGORIES.join(", ")}` }));
|
|
6543
|
+
} else {
|
|
6544
|
+
console.log(chalk2.red(`Unknown category: ${options.category}`));
|
|
6545
|
+
console.log(chalk2.dim(`Available: ${CATEGORIES.join(", ")}`));
|
|
6546
|
+
}
|
|
6547
|
+
process.exit(1);
|
|
6548
|
+
return;
|
|
6549
|
+
}
|
|
6550
|
+
const categoryConnectors = getConnectorsByCategory(category).map((c) => c.name);
|
|
6551
|
+
connectors.push(...categoryConnectors);
|
|
6552
|
+
}
|
|
6537
6553
|
if (connectors.length === 0) {
|
|
6538
6554
|
if (!isTTY) {
|
|
6539
6555
|
console.error("Error: specify connectors to install. Example: connectors install figma stripe");
|
|
@@ -6665,7 +6681,41 @@ Next steps:`));
|
|
|
6665
6681
|
}
|
|
6666
6682
|
process.exit(results.every((r) => r.success) ? 0 : 1);
|
|
6667
6683
|
});
|
|
6668
|
-
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available connectors", false).option("-i, --installed", "Show only installed connectors", false).option("--json", "Output as JSON", false).description("List available or installed connectors").action((options) => {
|
|
6684
|
+
program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available connectors", false).option("-i, --installed", "Show only installed connectors", false).option("-b, --brief", "Output only connector names", false).option("--json", "Output as JSON", false).description("List available or installed connectors").action((options) => {
|
|
6685
|
+
if (options.brief) {
|
|
6686
|
+
if (options.installed) {
|
|
6687
|
+
const installed = getInstalledConnectors();
|
|
6688
|
+
if (options.json) {
|
|
6689
|
+
console.log(JSON.stringify(installed));
|
|
6690
|
+
} else {
|
|
6691
|
+
for (const name of installed)
|
|
6692
|
+
console.log(name);
|
|
6693
|
+
}
|
|
6694
|
+
} else if (options.category) {
|
|
6695
|
+
const category = CATEGORIES.find((c) => c.toLowerCase() === options.category.toLowerCase());
|
|
6696
|
+
if (!category) {
|
|
6697
|
+
console.error(`Unknown category: ${options.category}`);
|
|
6698
|
+
process.exit(1);
|
|
6699
|
+
return;
|
|
6700
|
+
}
|
|
6701
|
+
const names = getConnectorsByCategory(category).map((c) => c.name);
|
|
6702
|
+
if (options.json) {
|
|
6703
|
+
console.log(JSON.stringify(names));
|
|
6704
|
+
} else {
|
|
6705
|
+
for (const n of names)
|
|
6706
|
+
console.log(n);
|
|
6707
|
+
}
|
|
6708
|
+
} else {
|
|
6709
|
+
const names = CONNECTORS.map((c) => c.name);
|
|
6710
|
+
if (options.json) {
|
|
6711
|
+
console.log(JSON.stringify(names));
|
|
6712
|
+
} else {
|
|
6713
|
+
for (const n of names)
|
|
6714
|
+
console.log(n);
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
return;
|
|
6718
|
+
}
|
|
6669
6719
|
if (options.installed) {
|
|
6670
6720
|
const installed = getInstalledConnectors();
|
|
6671
6721
|
if (installed.length === 0) {
|
|
@@ -7284,13 +7334,10 @@ Open this URL to authenticate:
|
|
|
7284
7334
|
connector,
|
|
7285
7335
|
authType,
|
|
7286
7336
|
configured: statusAfter2.configured,
|
|
7287
|
-
field: options.field ||
|
|
7337
|
+
field: options.field || "apiKey"
|
|
7288
7338
|
}));
|
|
7289
7339
|
} else {
|
|
7290
|
-
console.log(chalk2.green(`\u2713
|
|
7291
|
-
if (options.field) {
|
|
7292
|
-
console.log(chalk2.dim(` Field: ${options.field}`));
|
|
7293
|
-
}
|
|
7340
|
+
console.log(chalk2.green(`\u2713 Saved ${options.field || "apiKey"} for ${meta.displayName}`));
|
|
7294
7341
|
}
|
|
7295
7342
|
process.exit(0);
|
|
7296
7343
|
return;
|
|
@@ -7503,4 +7550,246 @@ Next steps:
|
|
|
7503
7550
|
console.log();
|
|
7504
7551
|
process.exit(results.every((r) => r.success) ? 0 : 1);
|
|
7505
7552
|
});
|
|
7553
|
+
program2.command("export").option("-o, --output <file>", "Write to file instead of stdout").description("Export all connector credentials as JSON backup").action((options) => {
|
|
7554
|
+
const connectDir = join5(homedir3(), ".connectors");
|
|
7555
|
+
const result = {};
|
|
7556
|
+
if (existsSync5(connectDir)) {
|
|
7557
|
+
for (const entry of readdirSync4(connectDir)) {
|
|
7558
|
+
const entryPath = join5(connectDir, entry);
|
|
7559
|
+
if (!statSync3(entryPath).isDirectory() || !entry.startsWith("connect-"))
|
|
7560
|
+
continue;
|
|
7561
|
+
const connectorName = entry.replace(/^connect-/, "");
|
|
7562
|
+
const profilesDir = join5(entryPath, "profiles");
|
|
7563
|
+
if (!existsSync5(profilesDir))
|
|
7564
|
+
continue;
|
|
7565
|
+
const profiles = {};
|
|
7566
|
+
for (const pEntry of readdirSync4(profilesDir)) {
|
|
7567
|
+
const pPath = join5(profilesDir, pEntry);
|
|
7568
|
+
if (statSync3(pPath).isFile() && pEntry.endsWith(".json")) {
|
|
7569
|
+
try {
|
|
7570
|
+
profiles[pEntry.replace(/\.json$/, "")] = JSON.parse(readFileSync5(pPath, "utf-8"));
|
|
7571
|
+
} catch {}
|
|
7572
|
+
} else if (statSync3(pPath).isDirectory()) {
|
|
7573
|
+
const configPath = join5(pPath, "config.json");
|
|
7574
|
+
if (existsSync5(configPath)) {
|
|
7575
|
+
try {
|
|
7576
|
+
profiles[pEntry] = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
7577
|
+
} catch {}
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
if (Object.keys(profiles).length > 0)
|
|
7582
|
+
result[connectorName] = { profiles };
|
|
7583
|
+
}
|
|
7584
|
+
}
|
|
7585
|
+
const exportData = JSON.stringify({ connectors: result, exportedAt: new Date().toISOString() }, null, 2);
|
|
7586
|
+
if (options.output) {
|
|
7587
|
+
writeFileSync4(options.output, exportData);
|
|
7588
|
+
console.log(chalk2.green(`\u2713 Exported to ${options.output}`));
|
|
7589
|
+
} else {
|
|
7590
|
+
console.log(exportData);
|
|
7591
|
+
}
|
|
7592
|
+
});
|
|
7593
|
+
program2.command("import").argument("<file>", "JSON backup file to import (use - for stdin)").option("--json", "Output as JSON", false).description("Import connector credentials from a JSON backup").action(async (file, options) => {
|
|
7594
|
+
let raw;
|
|
7595
|
+
if (file === "-") {
|
|
7596
|
+
const chunks = [];
|
|
7597
|
+
for await (const chunk of process.stdin)
|
|
7598
|
+
chunks.push(chunk.toString());
|
|
7599
|
+
raw = chunks.join("");
|
|
7600
|
+
} else {
|
|
7601
|
+
if (!existsSync5(file)) {
|
|
7602
|
+
if (options.json) {
|
|
7603
|
+
console.log(JSON.stringify({ error: `File not found: ${file}` }));
|
|
7604
|
+
} else {
|
|
7605
|
+
console.log(chalk2.red(`File not found: ${file}`));
|
|
7606
|
+
}
|
|
7607
|
+
process.exit(1);
|
|
7608
|
+
return;
|
|
7609
|
+
}
|
|
7610
|
+
raw = readFileSync5(file, "utf-8");
|
|
7611
|
+
}
|
|
7612
|
+
let data;
|
|
7613
|
+
try {
|
|
7614
|
+
data = JSON.parse(raw);
|
|
7615
|
+
} catch {
|
|
7616
|
+
if (options.json) {
|
|
7617
|
+
console.log(JSON.stringify({ error: "Invalid JSON" }));
|
|
7618
|
+
} else {
|
|
7619
|
+
console.log(chalk2.red("Invalid JSON in import file"));
|
|
7620
|
+
}
|
|
7621
|
+
process.exit(1);
|
|
7622
|
+
return;
|
|
7623
|
+
}
|
|
7624
|
+
if (!data.connectors || typeof data.connectors !== "object") {
|
|
7625
|
+
if (options.json) {
|
|
7626
|
+
console.log(JSON.stringify({ error: "Invalid format: missing 'connectors' object" }));
|
|
7627
|
+
} else {
|
|
7628
|
+
console.log(chalk2.red("Invalid format: missing 'connectors' object"));
|
|
7629
|
+
}
|
|
7630
|
+
process.exit(1);
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
const connectDir = join5(homedir3(), ".connectors");
|
|
7634
|
+
let imported = 0;
|
|
7635
|
+
for (const [connectorName, connData] of Object.entries(data.connectors)) {
|
|
7636
|
+
if (!/^[a-z0-9-]+$/.test(connectorName))
|
|
7637
|
+
continue;
|
|
7638
|
+
if (!connData.profiles || typeof connData.profiles !== "object")
|
|
7639
|
+
continue;
|
|
7640
|
+
const profilesDir = join5(connectDir, `connect-${connectorName}`, "profiles");
|
|
7641
|
+
for (const [profileName, config] of Object.entries(connData.profiles)) {
|
|
7642
|
+
if (!config || typeof config !== "object")
|
|
7643
|
+
continue;
|
|
7644
|
+
mkdirSync4(profilesDir, { recursive: true });
|
|
7645
|
+
writeFileSync4(join5(profilesDir, `${profileName}.json`), JSON.stringify(config, null, 2));
|
|
7646
|
+
imported++;
|
|
7647
|
+
}
|
|
7648
|
+
}
|
|
7649
|
+
if (options.json) {
|
|
7650
|
+
console.log(JSON.stringify({ success: true, imported }));
|
|
7651
|
+
} else {
|
|
7652
|
+
console.log(chalk2.green(`\u2713 Imported ${imported} profile(s)`));
|
|
7653
|
+
}
|
|
7654
|
+
});
|
|
7655
|
+
program2.command("upgrade").alias("self-update").option("--check", "Only check for updates, don't install", false).option("--json", "Output as JSON", false).description("Check for updates and upgrade to the latest version").action(async (options) => {
|
|
7656
|
+
const currentVersion = "0.2.5";
|
|
7657
|
+
try {
|
|
7658
|
+
const res = await fetch("https://registry.npmjs.org/@hasna/connectors/latest");
|
|
7659
|
+
if (!res.ok)
|
|
7660
|
+
throw new Error(`npm registry returned ${res.status}`);
|
|
7661
|
+
const data = await res.json();
|
|
7662
|
+
const latestVersion = data.version;
|
|
7663
|
+
const isUpToDate = currentVersion === latestVersion;
|
|
7664
|
+
if (options.json) {
|
|
7665
|
+
console.log(JSON.stringify({ current: currentVersion, latest: latestVersion, upToDate: isUpToDate }));
|
|
7666
|
+
if (options.check) {
|
|
7667
|
+
process.exit(isUpToDate ? 0 : 1);
|
|
7668
|
+
return;
|
|
7669
|
+
}
|
|
7670
|
+
} else {
|
|
7671
|
+
console.log(`
|
|
7672
|
+
Current: ${chalk2.cyan(currentVersion)}`);
|
|
7673
|
+
console.log(` Latest: ${chalk2.cyan(latestVersion)}`);
|
|
7674
|
+
if (isUpToDate) {
|
|
7675
|
+
console.log(chalk2.green(`
|
|
7676
|
+
Already up to date!
|
|
7677
|
+
`));
|
|
7678
|
+
process.exit(0);
|
|
7679
|
+
return;
|
|
7680
|
+
}
|
|
7681
|
+
console.log(chalk2.yellow(`
|
|
7682
|
+
Update available: ${currentVersion} \u2192 ${latestVersion}`));
|
|
7683
|
+
}
|
|
7684
|
+
if (options.check) {
|
|
7685
|
+
if (!options.json)
|
|
7686
|
+
console.log(chalk2.dim(`
|
|
7687
|
+
Run 'connectors upgrade' to install.
|
|
7688
|
+
`));
|
|
7689
|
+
process.exit(isUpToDate ? 0 : 1);
|
|
7690
|
+
return;
|
|
7691
|
+
}
|
|
7692
|
+
if (!options.json)
|
|
7693
|
+
console.log(chalk2.dim(`
|
|
7694
|
+
Upgrading...`));
|
|
7695
|
+
const { execSync } = await import("child_process");
|
|
7696
|
+
try {
|
|
7697
|
+
execSync(`bun install -g @hasna/connectors@${latestVersion}`, { stdio: options.json ? "pipe" : "inherit" });
|
|
7698
|
+
} catch {
|
|
7699
|
+
try {
|
|
7700
|
+
execSync(`npm install -g @hasna/connectors@${latestVersion}`, { stdio: options.json ? "pipe" : "inherit" });
|
|
7701
|
+
} catch (e) {
|
|
7702
|
+
if (options.json) {
|
|
7703
|
+
console.log(JSON.stringify({ error: "Failed to upgrade. Try manually: bun install -g @hasna/connectors@latest" }));
|
|
7704
|
+
} else {
|
|
7705
|
+
console.log(chalk2.red(`
|
|
7706
|
+
Failed to upgrade. Try manually:`));
|
|
7707
|
+
console.log(chalk2.dim(` bun install -g @hasna/connectors@latest
|
|
7708
|
+
`));
|
|
7709
|
+
}
|
|
7710
|
+
process.exit(1);
|
|
7711
|
+
return;
|
|
7712
|
+
}
|
|
7713
|
+
}
|
|
7714
|
+
if (options.json) {
|
|
7715
|
+
console.log(JSON.stringify({ upgraded: true, from: currentVersion, to: latestVersion }));
|
|
7716
|
+
} else {
|
|
7717
|
+
console.log(chalk2.green(`
|
|
7718
|
+
Upgraded to ${latestVersion}!
|
|
7719
|
+
`));
|
|
7720
|
+
}
|
|
7721
|
+
} catch (e) {
|
|
7722
|
+
if (options.json) {
|
|
7723
|
+
console.log(JSON.stringify({ error: e instanceof Error ? e.message : "Failed to check for updates" }));
|
|
7724
|
+
} else {
|
|
7725
|
+
console.log(chalk2.red(`
|
|
7726
|
+
Failed to check for updates: ${e instanceof Error ? e.message : e}
|
|
7727
|
+
`));
|
|
7728
|
+
}
|
|
7729
|
+
process.exit(1);
|
|
7730
|
+
}
|
|
7731
|
+
});
|
|
7732
|
+
program2.command("completions").argument("<shell>", "Shell type: bash, zsh, or fish").description("Output shell completion script").action((shell) => {
|
|
7733
|
+
const commands = ["interactive", "install", "list", "search", "info", "docs", "remove", "categories", "serve", "update", "status", "doctor", "auth", "init", "export", "import", "upgrade", "completions"];
|
|
7734
|
+
const connectorNames = CONNECTORS.map((c) => c.name);
|
|
7735
|
+
const categoryNames = CATEGORIES.map((c) => `"${c}"`);
|
|
7736
|
+
if (shell === "zsh") {
|
|
7737
|
+
console.log(`#compdef connectors
|
|
7738
|
+
_connectors() {
|
|
7739
|
+
local -a commands connectors categories
|
|
7740
|
+
commands=(${commands.join(" ")})
|
|
7741
|
+
connectors=(${connectorNames.join(" ")})
|
|
7742
|
+
categories=(${categoryNames.map((c) => c.replace(/"/g, "\\\"")).join(" ")})
|
|
7743
|
+
|
|
7744
|
+
if (( CURRENT == 2 )); then
|
|
7745
|
+
_describe 'command' commands
|
|
7746
|
+
elif (( CURRENT == 3 )); then
|
|
7747
|
+
case "\${words[2]}" in
|
|
7748
|
+
install|add|info|docs|remove|rm|auth)
|
|
7749
|
+
_describe 'connector' connectors ;;
|
|
7750
|
+
search) _message 'search query' ;;
|
|
7751
|
+
list|ls) _arguments '--category[Filter by category]:category:(${CATEGORIES.join(" ").replace(/&/g, "\\&")})' '--installed' '--json' '--brief' ;;
|
|
7752
|
+
*) ;;
|
|
7753
|
+
esac
|
|
7754
|
+
fi
|
|
7755
|
+
}
|
|
7756
|
+
compdef _connectors connectors`);
|
|
7757
|
+
} else if (shell === "bash") {
|
|
7758
|
+
console.log(`_connectors() {
|
|
7759
|
+
local cur prev commands connectors
|
|
7760
|
+
COMPREPLY=()
|
|
7761
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
7762
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
7763
|
+
commands="${commands.join(" ")}"
|
|
7764
|
+
connectors="${connectorNames.join(" ")}"
|
|
7765
|
+
|
|
7766
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
7767
|
+
COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
|
|
7768
|
+
elif [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
7769
|
+
case "\${prev}" in
|
|
7770
|
+
install|add|info|docs|remove|rm|auth)
|
|
7771
|
+
COMPREPLY=( $(compgen -W "\${connectors}" -- "\${cur}") ) ;;
|
|
7772
|
+
esac
|
|
7773
|
+
fi
|
|
7774
|
+
}
|
|
7775
|
+
complete -F _connectors connectors`);
|
|
7776
|
+
} else if (shell === "fish") {
|
|
7777
|
+
let script = `# Fish completions for connectors
|
|
7778
|
+
`;
|
|
7779
|
+
for (const cmd of commands) {
|
|
7780
|
+
script += `complete -c connectors -n "__fish_use_subcommand" -a "${cmd}"
|
|
7781
|
+
`;
|
|
7782
|
+
}
|
|
7783
|
+
script += `# Connector names for install/info/docs/remove/auth
|
|
7784
|
+
`;
|
|
7785
|
+
for (const name of connectorNames) {
|
|
7786
|
+
script += `complete -c connectors -n "__fish_seen_subcommand_from install add info docs remove rm auth" -a "${name}"
|
|
7787
|
+
`;
|
|
7788
|
+
}
|
|
7789
|
+
console.log(script);
|
|
7790
|
+
} else {
|
|
7791
|
+
console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
|
|
7792
|
+
process.exit(1);
|
|
7793
|
+
}
|
|
7794
|
+
});
|
|
7506
7795
|
program2.parse();
|
package/bin/mcp.js
CHANGED
|
@@ -20275,7 +20275,7 @@ function guessKeyField(name) {
|
|
|
20275
20275
|
loadConnectorVersions();
|
|
20276
20276
|
var server = new McpServer({
|
|
20277
20277
|
name: "connectors",
|
|
20278
|
-
version: "0.2.
|
|
20278
|
+
version: "0.2.5"
|
|
20279
20279
|
});
|
|
20280
20280
|
server.registerTool("search_connectors", {
|
|
20281
20281
|
title: "Search Connectors",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/connectors",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Open source connector library - Install API connectors with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "cd dashboard && bun run build && cd .. && bun build ./src/cli/index.tsx --outdir ./bin --target bun --external ink --external react --external chalk --external conf && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/serve.js --target bun && bun build ./src/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --outDir ./dist",
|
|
28
28
|
"build:dashboard": "cd dashboard && bun run build",
|
|
29
|
-
"postinstall": "cd dashboard && bun install",
|
|
29
|
+
"postinstall": "[ \"$SKIP_DASHBOARD\" = \"1\" ] || [ -d dashboard/node_modules ] || (cd dashboard && bun install)",
|
|
30
30
|
"dev": "bun run ./src/cli/index.tsx",
|
|
31
31
|
"typecheck": "tsc --noEmit",
|
|
32
32
|
"test": "bun test",
|