🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

subsc-cli

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

subsc-cli - npm Package Compare versions

Comparing version
0.3.3
to
1.0.0
+5
pnpm-workspace.yaml
allowBuilds:
better-sqlite3: true
esbuild: true
onlyBuiltDependencies:
- better-sqlite3
+133
-34

@@ -5,31 +5,126 @@ #!/usr/bin/env node

import Table from "cli-table3";
import consola, { consola as consola$1 } from "consola";
import { mkdirSync, readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import path from "path";
import { consola } from "consola";
import Database from "better-sqlite3";
import { mkdirSync } from "node:fs";
import path from "node:path";
import { homedir } from "node:os";
//#region src/basefs.ts
const dir = path.join(homedir(), ".config", "subscription-cli", "subscription.json");
let _db = null;
function getDbDir() {
return process.env.SUBSC_CLI_DB_DIR ?? path.join(homedir(), ".config", "subsc-cli");
}
function getDb() {
if (_db) return _db;
const dbdir = getDbDir();
mkdirSync(dbdir, { recursive: true });
_db = new Database(path.join(dbdir, "subscriptions.db"));
_db.pragma("journal_mode = WAL");
_db.pragma("foreign_keys = ON");
_db.exec(`
CREATE TABLE IF NOT EXISTS subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price INTEGER NOT NULL,
currency TEXT NOT NULL,
cycle TEXT NOT NULL
);
`);
_db.exec(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
`);
_db.exec(`
CREATE TABLE IF NOT EXISTS subscription_tags (
subscription_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (subscription_id, tag_id),
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
`);
return _db;
}
function mapTags(subs) {
if (subs.length === 0) return subs;
const getTags = getDb().prepare(`
SELECT tags.name FROM tags
JOIN subscription_tags ON subscription_tags.tag_id = tags.id
WHERE subscription_tags.subscription_id = ?
`);
for (const sub of subs) sub.tags = getTags.all(sub.id).map((r) => r.name);
return subs;
}
const getSubscriptions = () => {
try {
const result = readFileSync(dir, "utf-8");
return JSON.parse(result).filter(Boolean);
} catch (err) {
return [];
return mapTags(getDb().prepare("SELECT id, name, price, currency, cycle FROM subscriptions ORDER BY id").all());
} catch (error) {
consola.error("Failed to fetch subscriptions:", error);
throw error;
}
};
const writeSubscription = (SharedArgs) => {
mkdirSync(path.dirname(dir), { recursive: true });
const json = JSON.stringify(SharedArgs, null, 2);
const writeSubscription = (data) => {
try {
writeFileSync(dir, json, "utf-8");
consola.success("done add subscription");
const db = getDb();
const uniqueTags = Array.from(new Set(data.tags));
db.transaction(() => {
const result = db.prepare(`
INSERT INTO subscriptions (name, price, currency, cycle)
VALUES (?, ?, ?, ?)
`).run(data.name, data.price, data.currency, data.cycle);
const subscriptionId = Number(result.lastInsertRowid);
const insertTag = db.prepare(`
INSERT OR IGNORE INTO tags (name)
VALUES (?)
`);
const getTagId = db.prepare(`
SELECT id FROM tags WHERE name = ?
`);
const insertRel = db.prepare(`
INSERT INTO subscription_tags (subscription_id, tag_id)
VALUES (?, ?)
`);
for (const t of uniqueTags) {
insertTag.run(t);
const tagRow = getTagId.get(t);
if (tagRow) insertRel.run(subscriptionId, tagRow.id);
}
})();
} catch (error) {
consola.error("ファイルを作成することができませんでした");
consola.error("Failed to add subscription:", error);
throw error;
}
};
const deleteSubscription = (id) => {
writeSubscription(getSubscriptions().filter((n) => n.id !== id));
try {
if (getDb().prepare("DELETE FROM subscriptions WHERE id = ?").run(id).changes === 0) consola.warn(`No subscription found with id ${id}`);
} catch (error) {
consola.error("Failed to delete subscription:", error);
throw error;
}
};
const tagsSubscription = (tags) => {
return getSubscriptions().filter((item) => tags.every((n) => item.tags.includes(n)));
const tagsSubscription = (tag) => {
try {
const db = getDb();
const tags = Array.from(new Set(Array.isArray(tag) ? tag : [tag]));
if (tags.length === 0) return [];
const placeholders = tags.map(() => "?").join(",");
const ids = db.prepare(`
SELECT subscription_tags.subscription_id
FROM subscription_tags
JOIN tags ON tags.id = subscription_tags.tag_id
WHERE tags.name IN (${placeholders})
GROUP BY subscription_tags.subscription_id
HAVING COUNT(DISTINCT tags.name) = ?
`).all(...tags, tags.length).map((r) => r.subscription_id);
if (ids.length === 0) return [];
return mapTags(db.prepare(`
SELECT id, name, price, currency, cycle FROM subscriptions
WHERE id IN (${ids.map(() => "?").join(",")})
`).all(...ids));
} catch (error) {
consola.error("Failed to filter by tags:", error);
throw error;
}
};

@@ -41,3 +136,3 @@ //#endregion

if (list.length === 0) {
consola$1.info("No subscriptions found");
consola.info("No subscriptions found");
return;

@@ -54,4 +149,4 @@ }

String(sub.cycle),
String(sub.tags.join(" | ")),
sub.currency === "USD" ? String(`$${sub.price}`) : String(`\\${sub.price}`)
sub.tags.length > 0 ? sub.tags.join(", ") : "-",
sub.currency === "USD" ? String(`$${sub.price}`) : String(`¥${sub.price}`)
]);

@@ -64,3 +159,3 @@ const usdtotal = list.filter((n) => n.currency === "USD").reduce((sum, n) => sum + n.price, 0);

"JPY TOTAL",
`\\${jpytotal}`
`¥${jpytotal}`
], [

@@ -72,3 +167,3 @@ "",

]);
consola$1.log(table.toString());
consola.log(table.toString());
};

@@ -85,10 +180,17 @@ //#endregion

const name = await input({ message: "subscription name" });
const price = await input({ message: "payment subscribe service" });
const price = await input({
message: "monthly payment amount",
validate: (value) => {
if (value.trim() === "") return "Please enter a valid number";
if (isNaN(Number(value)) || Number(value) < 0) return "Please enter a valid non-negative number";
return true;
}
});
const currency = await select({
message: "currency",
choices: [{
label: "JPY",
name: "JPY",
value: "JPY"
}, {
label: "USD",
name: "USD",
value: "USD"

@@ -100,6 +202,6 @@ }]

choices: [{
label: "monthly",
name: "monthly",
value: "monthly"
}, {
label: "yearly",
name: "yearly",
value: "yearly"

@@ -109,5 +211,3 @@ }]

const tag = (await input({ message: "tags" })).split(",").map((tag) => tag.trim()).filter(Boolean);
const get = getSubscriptions();
get.push({
id: Math.max(0, ...get.map((s) => s.id)) + 1,
writeSubscription({
name,

@@ -119,3 +219,2 @@ price: Number(price),

});
writeSubscription(get);
});

@@ -125,4 +224,4 @@ program.command("delete").argument("<number>").action((number) => {

});
program.command("tags").argument("<...text>").action((text) => {
spreadSubscription(tagsSubscription(text));
program.command("tags").argument("<taglist...>").action((taglist) => {
spreadSubscription(tagsSubscription(taglist));
});

@@ -129,0 +228,0 @@ program.parse();

{
"name": "subsc-cli",
"version": "0.3.3",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"packageManager": "pnpm@11.5.1",
"bin": {

@@ -16,16 +17,22 @@ "subsc-cli": "./dist/index.mjs"

},
"devDependencies": {
"@types/bun": "latest",
"tsdown": "^0.22.2"
"scripts": {
"build": "tsdown",
"start": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@inquirer/prompts": "^8.5.2",
"better-sqlite3": "^11.0.0",
"cli-table3": "^0.6.5",
"commander": "^15.0.0",
"consola": "^3.4.2",
"table": "^6.9.0"
"consola": "^3.4.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.12",
"tsdown": "^0.22.2",
"tsx": "^4.19.0",
"typescript": "^5",
"vitest": "^3.0.0"
}
}

@@ -28,3 +28,3 @@ # subsc-cli

> [!TIP]
> The binary name is `sb`. Run via `bunx sb <command>` after `bun install`.
> The binary name is `subsc-cli`. Run via `bunx subsc-cli <command>` after `bun install`.

@@ -31,0 +31,0 @@ ## Data

{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["bun"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}