@mindtnv/todoist-cli 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -657,66 +657,59 @@ function createPaletteRegistry() {
657
657
  }
658
658
 
659
659
  // src/plugins/storage.ts
660
- import { Database } from "bun:sqlite";
661
- import { mkdirSync as mkdirSync3, existsSync as existsSync3 } from "fs";
660
+ import { mkdirSync as mkdirSync3, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
662
661
  import { join as join3 } from "path";
662
+ function loadJson(path, fallback) {
663
+ if (!existsSync3(path))
664
+ return fallback;
665
+ try {
666
+ return JSON.parse(readFileSync3(path, "utf-8"));
667
+ } catch {
668
+ return fallback;
669
+ }
670
+ }
671
+ function saveJson(path, data) {
672
+ writeFileSync3(path, JSON.stringify(data, null, 2) + `
673
+ `);
674
+ }
663
675
  function createPluginStorage(dataDir) {
664
676
  if (!existsSync3(dataDir)) {
665
677
  mkdirSync3(dataDir, { recursive: true });
666
678
  }
667
- const dbPath = join3(dataDir, "data.db");
668
- const db = new Database(dbPath);
669
- db.run("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)");
670
- db.run("CREATE TABLE IF NOT EXISTS task_data (task_id TEXT, key TEXT, value TEXT, PRIMARY KEY (task_id, key))");
671
- const getStmt = db.prepare("SELECT value FROM kv WHERE key = ?");
672
- const setStmt = db.prepare("INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)");
673
- const delStmt = db.prepare("DELETE FROM kv WHERE key = ?");
674
- const listStmt = db.prepare("SELECT key FROM kv WHERE key LIKE ? ESCAPE '\\'");
675
- const getTaskStmt = db.prepare("SELECT value FROM task_data WHERE task_id = ? AND key = ?");
676
- const setTaskStmt = db.prepare("INSERT OR REPLACE INTO task_data (task_id, key, value) VALUES (?, ?, ?)");
679
+ const kvPath = join3(dataDir, "kv.json");
680
+ const taskDataPath = join3(dataDir, "task-data.json");
681
+ let kv = loadJson(kvPath, {});
682
+ let taskData = loadJson(taskDataPath, {});
677
683
  return {
678
684
  async get(key) {
679
- const row = getStmt.get(key);
680
- if (!row)
681
- return null;
682
- try {
683
- return JSON.parse(row.value);
684
- } catch {
685
- console.warn("[plugin-storage] Corrupted data for key:", key);
686
- return null;
687
- }
685
+ const value = kv[key];
686
+ return value !== undefined ? value : null;
688
687
  },
689
688
  async set(key, value) {
690
- const serialized = JSON.stringify(value);
691
- setStmt.run(key, serialized);
689
+ kv[key] = value;
690
+ saveJson(kvPath, kv);
692
691
  },
693
692
  async delete(key) {
694
- delStmt.run(key);
693
+ delete kv[key];
694
+ saveJson(kvPath, kv);
695
695
  },
696
696
  async list(prefix) {
697
- const escapedPrefix = prefix ? prefix.replace(/[%_\\]/g, (ch) => `\\${ch}`) : "";
698
- const pattern = prefix ? `${escapedPrefix}%` : "%";
699
- const rows = listStmt.all(pattern);
700
- return rows.map((r) => r.key);
697
+ const keys = Object.keys(kv);
698
+ if (!prefix)
699
+ return keys;
700
+ return keys.filter((k) => k.startsWith(prefix));
701
701
  },
702
702
  async getTaskData(taskId, key) {
703
- const row = getTaskStmt.get(taskId, key);
704
- if (!row)
705
- return null;
706
- try {
707
- return JSON.parse(row.value);
708
- } catch {
709
- console.warn("[plugin-storage] Corrupted data for key:", `${taskId}:${key}`);
710
- return null;
711
- }
703
+ const value = taskData[taskId]?.[key];
704
+ return value !== undefined ? value : null;
712
705
  },
713
706
  async setTaskData(taskId, key, value) {
714
- const serialized = JSON.stringify(value);
715
- setTaskStmt.run(taskId, key, serialized);
707
+ if (!taskData[taskId])
708
+ taskData[taskId] = {};
709
+ taskData[taskId][key] = value;
710
+ saveJson(taskDataPath, taskData);
716
711
  },
717
- close() {
718
- db.close();
719
- }
712
+ close() {}
720
713
  };
721
714
  }
722
715
  var init_storage = () => {};
@@ -795,7 +788,7 @@ var init_api_proxy = __esm(() => {
795
788
 
796
789
  // src/plugins/loader.ts
797
790
  import { join as join4 } from "path";
798
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
791
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
799
792
  import { homedir as homedir3 } from "os";
800
793
  function createLogger(pluginName) {
801
794
  return {
@@ -843,7 +836,7 @@ async function loadPlugins(hooks, views, extensions, palette) {
843
836
  let manifest = null;
844
837
  if (existsSync4(manifestPath)) {
845
838
  try {
846
- manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
839
+ manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
847
840
  } catch (parseErr) {
848
841
  console.warn(`[plugins] Invalid plugin.json for "${name}":`, parseErr instanceof Error ? parseErr.message : parseErr);
849
842
  }
@@ -8482,13 +8475,13 @@ Examples:
8482
8475
  }
8483
8476
  let description = opts.description;
8484
8477
  if (opts.editor) {
8485
- const { writeFileSync: writeFileSync3, readFileSync: readFileSync4 } = await import("node:fs");
8478
+ const { writeFileSync: writeFileSync4, readFileSync: readFileSync5 } = await import("node:fs");
8486
8479
  const { spawnSync } = await import("node:child_process");
8487
8480
  const tmpFile = `/tmp/todoist-desc-${Date.now()}.md`;
8488
- writeFileSync3(tmpFile, description ?? "");
8481
+ writeFileSync4(tmpFile, description ?? "");
8489
8482
  const editor = process.env.EDITOR || process.env.VISUAL || "vi";
8490
8483
  spawnSync(editor, [tmpFile], { stdio: "inherit" });
8491
- description = readFileSync4(tmpFile, "utf-8");
8484
+ description = readFileSync5(tmpFile, "utf-8");
8492
8485
  if (description.trim() === "")
8493
8486
  description = undefined;
8494
8487
  }
@@ -9465,12 +9458,12 @@ function registerSectionCommand(program) {
9465
9458
 
9466
9459
  // src/cli/completion.ts
9467
9460
  init_config();
9468
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
9461
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
9469
9462
  import { join as join5 } from "path";
9470
9463
  import chalk18 from "chalk";
9471
9464
  var COMPLETION_CACHE_PATH = join5(CONFIG_DIR, ".completion-cache.json");
9472
9465
  function saveCompletionCache(cache) {
9473
- writeFileSync3(COMPLETION_CACHE_PATH, JSON.stringify(cache, null, 2), "utf-8");
9466
+ writeFileSync4(COMPLETION_CACHE_PATH, JSON.stringify(cache, null, 2), "utf-8");
9474
9467
  }
9475
9468
  var BASH_COMPLETION = `#!/usr/bin/env bash
9476
9469
  # todoist CLI bash completion
@@ -10223,223 +10216,589 @@ function registerFilterCommand(program) {
10223
10216
  // src/cli/plugin.ts
10224
10217
  import chalk25 from "chalk";
10225
10218
 
10226
- // src/plugins/installer.ts
10219
+ // src/plugins/marketplace.ts
10227
10220
  init_config();
10228
- import { join as join6, resolve } from "path";
10229
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync, symlinkSync, lstatSync } from "fs";
10221
+ import { join as join6 } from "path";
10230
10222
  import { homedir as homedir4 } from "os";
10223
+ import {
10224
+ existsSync as existsSync6,
10225
+ mkdirSync as mkdirSync4,
10226
+ readFileSync as readFileSync6,
10227
+ rmSync,
10228
+ cpSync
10229
+ } from "fs";
10231
10230
  import { execSync } from "child_process";
10232
- var PLUGINS_DIR2 = join6(homedir4(), ".config", "todoist-cli", "plugins");
10233
- function ensurePluginsDir() {
10234
- if (!existsSync6(PLUGINS_DIR2)) {
10235
- mkdirSync4(PLUGINS_DIR2, { recursive: true });
10236
- }
10231
+ var CONFIG_DIR3 = join6(homedir4(), ".config", "todoist-cli");
10232
+ var MARKETPLACE_CACHE_DIR = join6(CONFIG_DIR3, "marketplace-cache");
10233
+ var PLUGINS_DIR2 = join6(CONFIG_DIR3, "plugins");
10234
+ var DEFAULT_MARKETPLACE = "github:mindtnv/todoist-cli";
10235
+ var DEFAULT_MARKETPLACE_NAME = "todoist-cli-official";
10236
+ function ensureDir(dir) {
10237
+ if (!existsSync6(dir)) {
10238
+ mkdirSync4(dir, { recursive: true });
10239
+ }
10240
+ }
10241
+ function parseGitHubSource(source) {
10242
+ if (!source.startsWith("github:"))
10243
+ return null;
10244
+ const parts = source.replace("github:", "").split("/");
10245
+ if (parts.length < 2 || !parts[0] || !parts[1])
10246
+ return null;
10247
+ return { user: parts[0], repo: parts[1] };
10237
10248
  }
10238
- function derivePluginName(source) {
10249
+ function deriveNameFromSource(source) {
10239
10250
  if (source.startsWith("github:")) {
10240
10251
  const parts = source.replace("github:", "").split("/");
10241
10252
  return parts[parts.length - 1] ?? source;
10242
10253
  }
10243
- if (source.startsWith("npm:")) {
10244
- return source.replace("npm:", "").replace(/^@[^/]+\//, "");
10254
+ try {
10255
+ const url = new URL(source);
10256
+ const segments = url.pathname.split("/").filter(Boolean);
10257
+ return segments[segments.length - 1] ?? source;
10258
+ } catch {
10259
+ return source.split("/").pop() ?? source;
10245
10260
  }
10246
- return source.split("/").pop() ?? source;
10247
10261
  }
10248
- async function installPlugin(source) {
10249
- ensurePluginsDir();
10250
- let name = derivePluginName(source);
10251
- const targetDir = join6(PLUGINS_DIR2, name);
10252
- if (existsSync6(targetDir)) {
10253
- throw new Error(`Plugin "${name}" is already installed. Use "todoist plugin remove ${name}" first.`);
10262
+ function isExternalSource(source) {
10263
+ return typeof source === "object" && source !== null && "type" in source;
10264
+ }
10265
+ function getRegisteredMarketplaces() {
10266
+ const config = getConfig();
10267
+ const marketplaces = [];
10268
+ marketplaces.push({
10269
+ name: DEFAULT_MARKETPLACE_NAME,
10270
+ source: DEFAULT_MARKETPLACE,
10271
+ autoUpdate: true
10272
+ });
10273
+ const configMarketplaces = config.marketplaces;
10274
+ if (configMarketplaces) {
10275
+ for (const [name, entry] of Object.entries(configMarketplaces)) {
10276
+ if (name === DEFAULT_MARKETPLACE_NAME)
10277
+ continue;
10278
+ marketplaces.push({
10279
+ name,
10280
+ source: entry.source ?? "",
10281
+ autoUpdate: entry.autoUpdate ?? true
10282
+ });
10283
+ }
10254
10284
  }
10255
- if (source.startsWith("github:")) {
10256
- const repo = source.replace("github:", "");
10257
- execSync(`git clone https://github.com/${repo}.git "${targetDir}"`, { stdio: "pipe" });
10258
- } else if (source.startsWith("npm:")) {
10259
- const pkg = source.replace("npm:", "");
10260
- mkdirSync4(targetDir, { recursive: true });
10261
- execSync(`cd "${targetDir}" && npm init -y && npm install ${pkg}`, { stdio: "pipe" });
10285
+ return marketplaces;
10286
+ }
10287
+ function addMarketplace(source) {
10288
+ const name = deriveNameFromSource(source);
10289
+ if (name === DEFAULT_MARKETPLACE_NAME) {
10290
+ throw new Error(`Cannot add marketplace with reserved name "${DEFAULT_MARKETPLACE_NAME}".`);
10291
+ }
10292
+ const config = getConfig();
10293
+ const rawConfig = config;
10294
+ if (!rawConfig.marketplaces) {
10295
+ rawConfig.marketplaces = {};
10296
+ }
10297
+ const marketplaces = rawConfig.marketplaces;
10298
+ marketplaces[name] = { source, autoUpdate: true };
10299
+ saveConfig(config);
10300
+ return name;
10301
+ }
10302
+ function removeMarketplace(name) {
10303
+ if (name === DEFAULT_MARKETPLACE_NAME) {
10304
+ throw new Error(`Cannot remove the default marketplace "${DEFAULT_MARKETPLACE_NAME}".`);
10305
+ }
10306
+ const config = getConfig();
10307
+ const rawConfig = config;
10308
+ const marketplaces = rawConfig.marketplaces;
10309
+ if (!marketplaces || !(name in marketplaces)) {
10310
+ throw new Error(`Marketplace "${name}" is not registered.`);
10311
+ }
10312
+ delete marketplaces[name];
10313
+ saveConfig(config);
10314
+ const cacheDir = join6(MARKETPLACE_CACHE_DIR, name);
10315
+ if (existsSync6(cacheDir)) {
10316
+ rmSync(cacheDir, { recursive: true, force: true });
10317
+ }
10318
+ }
10319
+ async function fetchMarketplaceManifest(config) {
10320
+ ensureDir(MARKETPLACE_CACHE_DIR);
10321
+ const github = parseGitHubSource(config.source);
10322
+ if (github) {
10323
+ const cacheDir = join6(MARKETPLACE_CACHE_DIR, config.name);
10324
+ if (existsSync6(cacheDir)) {
10325
+ try {
10326
+ execSync(`git -C "${cacheDir}" pull`, { stdio: "pipe" });
10327
+ } catch {}
10328
+ } else {
10329
+ execSync(`git clone https://github.com/${github.user}/${github.repo}.git "${cacheDir}"`, { stdio: "pipe" });
10330
+ }
10331
+ const manifestPath2 = join6(cacheDir, "marketplace.json");
10332
+ if (!existsSync6(manifestPath2)) {
10333
+ throw new Error(`Marketplace "${config.name}" does not contain a marketplace.json file.`);
10334
+ }
10335
+ const raw2 = readFileSync6(manifestPath2, "utf-8");
10336
+ return JSON.parse(raw2);
10337
+ }
10338
+ if (config.source.startsWith("http://") || config.source.startsWith("https://")) {
10339
+ const response = await fetch(config.source);
10340
+ if (!response.ok) {
10341
+ throw new Error(`Failed to fetch marketplace manifest from ${config.source}: ${response.statusText}`);
10342
+ }
10343
+ return await response.json();
10344
+ }
10345
+ const manifestPath = join6(config.source, "marketplace.json");
10346
+ if (!existsSync6(manifestPath)) {
10347
+ throw new Error(`Marketplace at "${config.source}" does not contain a marketplace.json file.`);
10348
+ }
10349
+ const raw = readFileSync6(manifestPath, "utf-8");
10350
+ return JSON.parse(raw);
10351
+ }
10352
+ async function discoverPlugins() {
10353
+ const marketplaces = getRegisteredMarketplaces();
10354
+ const discovered = [];
10355
+ const config = getConfig();
10356
+ const installedPlugins = config.plugins ?? {};
10357
+ for (const marketplace of marketplaces) {
10358
+ try {
10359
+ const manifest = await fetchMarketplaceManifest(marketplace);
10360
+ for (const plugin of manifest.plugins) {
10361
+ const isInstalled = plugin.name in installedPlugins;
10362
+ const pluginConfig = installedPlugins[plugin.name];
10363
+ const isEnabled = isInstalled ? pluginConfig?.enabled !== false : false;
10364
+ discovered.push({
10365
+ ...plugin,
10366
+ marketplace: marketplace.name,
10367
+ installed: isInstalled,
10368
+ enabled: isEnabled
10369
+ });
10370
+ }
10371
+ } catch {
10372
+ continue;
10373
+ }
10374
+ }
10375
+ return discovered;
10376
+ }
10377
+ async function installPlugin(pluginName, marketplaceName) {
10378
+ ensureDir(PLUGINS_DIR2);
10379
+ const allPlugins = await discoverPlugins();
10380
+ let candidates = allPlugins.filter((p) => p.name === pluginName);
10381
+ if (marketplaceName) {
10382
+ candidates = candidates.filter((p) => p.marketplace === marketplaceName);
10383
+ }
10384
+ if (candidates.length === 0) {
10385
+ throw new Error(`Plugin "${pluginName}" not found${marketplaceName ? ` in marketplace "${marketplaceName}"` : ""}.`);
10386
+ }
10387
+ const plugin = candidates[0];
10388
+ if (plugin.installed) {
10389
+ throw new Error(`Plugin "${pluginName}" is already installed. Use "todoist plugin remove ${pluginName}" first.`);
10390
+ }
10391
+ const targetDir = join6(PLUGINS_DIR2, pluginName);
10392
+ if (isExternalSource(plugin.source)) {
10393
+ resolveExternalSource(plugin.source, targetDir);
10262
10394
  } else {
10263
- const resolvedPath = resolve(process.cwd(), source);
10264
- if (!existsSync6(resolvedPath))
10265
- throw new Error(`Local path not found: ${resolvedPath}`);
10266
- symlinkSync(resolvedPath, targetDir, "dir");
10395
+ const source = plugin.source;
10396
+ if (source.startsWith("./") || source.startsWith("../")) {
10397
+ const cacheDir = join6(MARKETPLACE_CACHE_DIR, plugin.marketplace);
10398
+ const sourcePath = join6(cacheDir, source);
10399
+ if (!existsSync6(sourcePath)) {
10400
+ throw new Error(`Plugin source path not found: ${sourcePath}`);
10401
+ }
10402
+ ensureDir(targetDir);
10403
+ cpSync(sourcePath, targetDir, { recursive: true });
10404
+ } else if (source.startsWith("github:")) {
10405
+ const github = parseGitHubSource(source);
10406
+ if (!github)
10407
+ throw new Error(`Invalid GitHub source: ${source}`);
10408
+ execSync(`git clone https://github.com/${github.user}/${github.repo}.git "${targetDir}"`, { stdio: "pipe" });
10409
+ } else {
10410
+ if (!existsSync6(source)) {
10411
+ throw new Error(`Plugin source path not found: ${source}`);
10412
+ }
10413
+ ensureDir(targetDir);
10414
+ cpSync(source, targetDir, { recursive: true });
10415
+ }
10267
10416
  }
10268
- const isSymlink = lstatSync(targetDir).isSymbolicLink();
10269
- if (!isSymlink && existsSync6(join6(targetDir, "package.json"))) {
10417
+ if (existsSync6(join6(targetDir, "package.json"))) {
10270
10418
  try {
10271
10419
  execSync(`cd "${targetDir}" && bun install`, { stdio: "pipe" });
10272
10420
  } catch {
10273
- execSync(`cd "${targetDir}" && npm install`, { stdio: "pipe" });
10421
+ try {
10422
+ execSync(`cd "${targetDir}" && npm install`, { stdio: "pipe" });
10423
+ } catch {}
10274
10424
  }
10275
10425
  }
10276
- const manifestPath = join6(targetDir, "plugin.json");
10277
- let manifest = null;
10278
- if (existsSync6(manifestPath)) {
10279
- manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
10280
- name = manifest.name;
10281
- }
10282
- setPluginEntry(name, { source });
10426
+ setPluginEntry(pluginName, {
10427
+ source: `${pluginName}@${plugin.marketplace}`,
10428
+ enabled: true
10429
+ });
10283
10430
  return {
10284
- name,
10285
- version: manifest?.version ?? "unknown",
10286
- description: manifest?.description,
10287
- permissions: manifest?.permissions
10431
+ name: pluginName,
10432
+ version: plugin.version ?? "unknown",
10433
+ marketplace: plugin.marketplace,
10434
+ description: plugin.description
10288
10435
  };
10289
10436
  }
10437
+ function resolveExternalSource(source, targetDir) {
10438
+ switch (source.type) {
10439
+ case "github": {
10440
+ if (!source.repo)
10441
+ throw new Error("GitHub source requires a 'repo' field.");
10442
+ const ref = source.ref ? ` --branch ${source.ref}` : "";
10443
+ execSync(`git clone https://github.com/${source.repo}.git${ref} "${targetDir}"`, { stdio: "pipe" });
10444
+ if (source.sha) {
10445
+ execSync(`git -C "${targetDir}" checkout ${source.sha}`, {
10446
+ stdio: "pipe"
10447
+ });
10448
+ }
10449
+ break;
10450
+ }
10451
+ case "git": {
10452
+ if (!source.url)
10453
+ throw new Error("Git source requires a 'url' field.");
10454
+ const ref = source.ref ? ` --branch ${source.ref}` : "";
10455
+ execSync(`git clone ${source.url}${ref} "${targetDir}"`, {
10456
+ stdio: "pipe"
10457
+ });
10458
+ if (source.sha) {
10459
+ execSync(`git -C "${targetDir}" checkout ${source.sha}`, {
10460
+ stdio: "pipe"
10461
+ });
10462
+ }
10463
+ break;
10464
+ }
10465
+ case "npm": {
10466
+ if (!source.package)
10467
+ throw new Error("npm source requires a 'package' field.");
10468
+ mkdirSync4(targetDir, { recursive: true });
10469
+ execSync(`cd "${targetDir}" && npm init -y && npm install ${source.package}`, { stdio: "pipe" });
10470
+ break;
10471
+ }
10472
+ default:
10473
+ throw new Error(`Unknown source type: ${source.type}`);
10474
+ }
10475
+ }
10290
10476
  function removePlugin(name) {
10291
10477
  const targetDir = join6(PLUGINS_DIR2, name);
10292
- if (!existsSync6(targetDir)) {
10293
- throw new Error(`Plugin "${name}" is not installed.`);
10294
- }
10295
- if (lstatSync(targetDir).isSymbolicLink()) {
10296
- rmSync(targetDir);
10297
- } else {
10478
+ if (existsSync6(targetDir)) {
10298
10479
  rmSync(targetDir, { recursive: true, force: true });
10299
10480
  }
10300
10481
  removePluginEntry(name);
10301
10482
  }
10302
- function enablePlugin(name) {
10483
+ async function updatePlugin(name) {
10303
10484
  const config = getConfig();
10304
10485
  const plugins = config.plugins;
10305
10486
  if (!plugins?.[name]) {
10306
10487
  throw new Error(`Plugin "${name}" is not installed.`);
10307
10488
  }
10308
- const { enabled, ...rest } = plugins[name];
10309
- setPluginEntry(name, rest);
10310
- }
10311
- function disablePlugin(name) {
10312
- const config = getConfig();
10313
- const plugins = config.plugins;
10314
- if (!plugins?.[name]) {
10315
- throw new Error(`Plugin "${name}" is not installed.`);
10489
+ const pluginEntry = plugins[name];
10490
+ const sourceStr = pluginEntry.source;
10491
+ if (!sourceStr) {
10492
+ return { name, updated: false, message: "No source information found" };
10316
10493
  }
10317
- setPluginEntry(name, { ...plugins[name], enabled: false });
10318
- }
10319
- function updatePlugin(name) {
10320
- const config = getConfig();
10321
- const plugins = config.plugins;
10322
- if (!plugins?.[name]) {
10323
- throw new Error(`Plugin "${name}" is not installed.`);
10494
+ const atIndex = sourceStr.lastIndexOf("@");
10495
+ const marketplaceName = atIndex > 0 ? sourceStr.substring(atIndex + 1) : undefined;
10496
+ if (!marketplaceName) {
10497
+ return { name, updated: false, message: "Cannot determine marketplace for plugin" };
10324
10498
  }
10325
- const targetDir = join6(PLUGINS_DIR2, name);
10326
- if (!existsSync6(targetDir)) {
10327
- throw new Error(`Plugin directory not found for "${name}".`);
10499
+ await refreshMarketplaceCache(marketplaceName);
10500
+ const marketplaces = getRegisteredMarketplaces();
10501
+ const marketplace = marketplaces.find((m) => m.name === marketplaceName);
10502
+ if (!marketplace) {
10503
+ return { name, updated: false, message: `Marketplace "${marketplaceName}" not found` };
10328
10504
  }
10329
- const source = plugins[name].source;
10330
- const isSymlink = lstatSync(targetDir).isSymbolicLink();
10331
- if (isSymlink) {
10332
- return { name, updated: false, message: "Local symlink — already up to date" };
10505
+ let manifest;
10506
+ try {
10507
+ manifest = await fetchMarketplaceManifest(marketplace);
10508
+ } catch {
10509
+ return { name, updated: false, message: `Failed to fetch marketplace "${marketplaceName}"` };
10333
10510
  }
10334
- if (source.startsWith("github:")) {
10335
- execSync(`cd "${targetDir}" && git pull`, { stdio: "pipe" });
10336
- if (existsSync6(join6(targetDir, "package.json"))) {
10337
- try {
10338
- execSync(`cd "${targetDir}" && bun install`, { stdio: "pipe" });
10339
- } catch {
10340
- execSync(`cd "${targetDir}" && npm install`, { stdio: "pipe" });
10511
+ const pluginManifest = manifest.plugins.find((p) => p.name === name);
10512
+ if (!pluginManifest) {
10513
+ return { name, updated: false, message: `Plugin "${name}" no longer in marketplace` };
10514
+ }
10515
+ const oldVersion = pluginEntry.version ?? "unknown";
10516
+ const newVersion = pluginManifest.version ?? "unknown";
10517
+ const targetDir = join6(PLUGINS_DIR2, name);
10518
+ if (isExternalSource(pluginManifest.source)) {
10519
+ if (pluginManifest.source.type === "github" && existsSync6(join6(targetDir, ".git"))) {
10520
+ execSync(`git -C "${targetDir}" pull`, { stdio: "pipe" });
10521
+ } else {
10522
+ if (existsSync6(targetDir)) {
10523
+ rmSync(targetDir, { recursive: true, force: true });
10524
+ }
10525
+ resolveExternalSource(pluginManifest.source, targetDir);
10526
+ }
10527
+ } else {
10528
+ const source = pluginManifest.source;
10529
+ if (source.startsWith("./") || source.startsWith("../")) {
10530
+ const cacheDir = join6(MARKETPLACE_CACHE_DIR, marketplaceName);
10531
+ const sourcePath = join6(cacheDir, source);
10532
+ if (existsSync6(targetDir)) {
10533
+ rmSync(targetDir, { recursive: true, force: true });
10341
10534
  }
10535
+ ensureDir(targetDir);
10536
+ cpSync(sourcePath, targetDir, { recursive: true });
10537
+ } else if (existsSync6(join6(targetDir, ".git"))) {
10538
+ execSync(`git -C "${targetDir}" pull`, { stdio: "pipe" });
10342
10539
  }
10343
- return { name, updated: true, message: "Pulled latest from git" };
10344
10540
  }
10345
- if (source.startsWith("npm:")) {
10346
- const pkg = source.replace("npm:", "");
10347
- execSync(`cd "${targetDir}" && npm update ${pkg}`, { stdio: "pipe" });
10348
- return { name, updated: true, message: "Updated npm package" };
10541
+ if (existsSync6(join6(targetDir, "package.json"))) {
10542
+ try {
10543
+ execSync(`cd "${targetDir}" && bun install`, { stdio: "pipe" });
10544
+ } catch {
10545
+ try {
10546
+ execSync(`cd "${targetDir}" && npm install`, { stdio: "pipe" });
10547
+ } catch {}
10548
+ }
10349
10549
  }
10350
- return { name, updated: false, message: "Local plugin — update manually" };
10550
+ setPluginEntry(name, {
10551
+ ...pluginEntry,
10552
+ version: newVersion
10553
+ });
10554
+ const updated = oldVersion !== newVersion;
10555
+ return {
10556
+ name,
10557
+ updated,
10558
+ oldVersion,
10559
+ newVersion,
10560
+ message: updated ? `Updated from ${oldVersion} to ${newVersion}` : "Already at latest version"
10561
+ };
10351
10562
  }
10352
- function listPlugins() {
10563
+ async function updateAllPlugins() {
10353
10564
  const config = getConfig();
10354
10565
  const plugins = config.plugins;
10355
10566
  if (!plugins)
10356
10567
  return [];
10357
- return Object.entries(plugins).map(([name, cfg]) => ({
10358
- name,
10359
- source: cfg.source,
10360
- enabled: cfg.enabled !== false
10361
- }));
10568
+ const results = [];
10569
+ for (const name of Object.keys(plugins)) {
10570
+ try {
10571
+ const result = await updatePlugin(name);
10572
+ results.push(result);
10573
+ } catch (err) {
10574
+ results.push({
10575
+ name,
10576
+ updated: false,
10577
+ message: err instanceof Error ? err.message : "Unknown error"
10578
+ });
10579
+ }
10580
+ }
10581
+ return results;
10582
+ }
10583
+ async function refreshMarketplaceCache(name) {
10584
+ const marketplaces = getRegisteredMarketplaces();
10585
+ const targets = name ? marketplaces.filter((m) => m.name === name) : marketplaces;
10586
+ for (const marketplace of targets) {
10587
+ const github = parseGitHubSource(marketplace.source);
10588
+ if (!github)
10589
+ continue;
10590
+ const cacheDir = join6(MARKETPLACE_CACHE_DIR, marketplace.name);
10591
+ if (existsSync6(cacheDir)) {
10592
+ try {
10593
+ execSync(`git -C "${cacheDir}" pull`, { stdio: "pipe" });
10594
+ } catch {}
10595
+ } else {
10596
+ try {
10597
+ execSync(`git clone https://github.com/${github.user}/${github.repo}.git "${cacheDir}"`, { stdio: "pipe" });
10598
+ } catch {}
10599
+ }
10600
+ }
10601
+ }
10602
+ function enablePlugin(name) {
10603
+ const config = getConfig();
10604
+ const plugins = config.plugins;
10605
+ if (!plugins?.[name]) {
10606
+ throw new Error(`Plugin "${name}" is not installed.`);
10607
+ }
10608
+ const entry = plugins[name];
10609
+ const { enabled: _enabled, ...rest } = entry;
10610
+ setPluginEntry(name, rest);
10611
+ }
10612
+ function disablePlugin(name) {
10613
+ const config = getConfig();
10614
+ const plugins = config.plugins;
10615
+ if (!plugins?.[name]) {
10616
+ throw new Error(`Plugin "${name}" is not installed.`);
10617
+ }
10618
+ setPluginEntry(name, { ...plugins[name], enabled: false });
10362
10619
  }
10363
10620
 
10364
10621
  // src/cli/plugin.ts
10622
+ init_config();
10365
10623
  function registerPluginCommand(program) {
10366
- const plugin = program.command("plugin").description("Manage plugins");
10367
- plugin.command("add").description("Install a plugin (github:user/repo, npm:package, or local path)").argument("<source>", "Plugin source").action(async (source) => {
10624
+ const plugin = program.command("plugin").description("Manage plugins and marketplaces");
10625
+ plugin.command("list").description("List installed plugins").action(() => {
10626
+ try {
10627
+ const config = getConfig();
10628
+ const plugins = config.plugins;
10629
+ if (!plugins || Object.keys(plugins).length === 0) {
10630
+ console.log(chalk25.dim("No plugins installed."));
10631
+ console.log(chalk25.dim("Discover plugins with: todoist plugin discover"));
10632
+ return;
10633
+ }
10634
+ console.log(chalk25.bold("Installed Plugins"));
10635
+ console.log("");
10636
+ for (const [name, entry] of Object.entries(plugins)) {
10637
+ const isEnabled = entry.enabled !== false;
10638
+ const status = isEnabled ? chalk25.green("●") : chalk25.yellow("○");
10639
+ const statusLabel = isEnabled ? chalk25.green("enabled") : chalk25.yellow("disabled");
10640
+ const version = entry.version ?? "unknown";
10641
+ const source = entry.source ?? "";
10642
+ console.log(` ${status} ${chalk25.bold(name)} ${chalk25.cyan("v" + version)} ${statusLabel} ${chalk25.dim(source)}`);
10643
+ }
10644
+ } catch (err) {
10645
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10646
+ process.exit(1);
10647
+ }
10648
+ });
10649
+ plugin.command("discover").description("Browse available plugins from all marketplaces").action(async () => {
10368
10650
  try {
10369
- console.log(chalk25.dim(`Installing plugin from ${source}...`));
10370
- const result = await installPlugin(source);
10371
- console.log(chalk25.green(`✓ Installed ${result.name} v${result.version}`));
10651
+ console.log(chalk25.dim("Fetching plugins from marketplaces..."));
10652
+ const discovered = await discoverPlugins();
10653
+ if (discovered.length === 0) {
10654
+ console.log(chalk25.dim("No plugins found in any marketplace."));
10655
+ return;
10656
+ }
10657
+ const grouped = new Map;
10658
+ for (const plugin2 of discovered) {
10659
+ const group = grouped.get(plugin2.marketplace) ?? [];
10660
+ group.push(plugin2);
10661
+ grouped.set(plugin2.marketplace, group);
10662
+ }
10663
+ for (const [marketplace2, plugins] of grouped) {
10664
+ console.log("");
10665
+ console.log(chalk25.bold.underline(marketplace2));
10666
+ console.log("");
10667
+ for (const p of plugins) {
10668
+ let indicator;
10669
+ if (p.installed && p.enabled) {
10670
+ indicator = chalk25.green("●");
10671
+ } else if (p.installed && !p.enabled) {
10672
+ indicator = chalk25.yellow("◐");
10673
+ } else {
10674
+ indicator = chalk25.dim("○");
10675
+ }
10676
+ const version = p.version ? chalk25.cyan("v" + p.version) : "";
10677
+ const description = p.description ? chalk25.dim(p.description) : "";
10678
+ console.log(` ${indicator} ${chalk25.bold(p.name)} ${version} ${description}`);
10679
+ }
10680
+ }
10681
+ } catch (err) {
10682
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10683
+ process.exit(1);
10684
+ }
10685
+ });
10686
+ plugin.command("install").description("Install a plugin (optionally name@marketplace)").argument("<name>", "Plugin name (or name@marketplace)").action(async (nameArg) => {
10687
+ try {
10688
+ let pluginName = nameArg;
10689
+ let marketplaceName;
10690
+ const atIndex = nameArg.lastIndexOf("@");
10691
+ if (atIndex > 0) {
10692
+ pluginName = nameArg.substring(0, atIndex);
10693
+ marketplaceName = nameArg.substring(atIndex + 1);
10694
+ }
10695
+ console.log(chalk25.dim(`Installing plugin "${pluginName}"...`));
10696
+ const result = await installPlugin(pluginName, marketplaceName);
10697
+ console.log(chalk25.green(`Installed ${result.name} v${result.version} from ${result.marketplace}`));
10372
10698
  if (result.description) {
10373
10699
  console.log(chalk25.dim(` ${result.description}`));
10374
10700
  }
10375
- if (result.permissions?.length) {
10376
- console.log(chalk25.dim(` Permissions: ${result.permissions.join(", ")}`));
10377
- }
10378
10701
  } catch (err) {
10379
- console.error(chalk25.red(`Failed to install: ${err instanceof Error ? err.message : err}`));
10702
+ console.error(chalk25.red(`Failed to install: ${err instanceof Error ? err.message : String(err)}`));
10380
10703
  process.exit(1);
10381
10704
  }
10382
10705
  });
10383
10706
  plugin.command("remove").description("Remove an installed plugin").argument("<name>", "Plugin name").action((name) => {
10384
10707
  try {
10385
10708
  removePlugin(name);
10386
- console.log(chalk25.green(`✓ Removed ${name}`));
10709
+ console.log(chalk25.green(`Removed ${name}`));
10387
10710
  } catch (err) {
10388
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10711
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10389
10712
  process.exit(1);
10390
10713
  }
10391
10714
  });
10392
- plugin.command("list").description("List installed plugins").action(() => {
10393
- const plugins = listPlugins();
10394
- if (plugins.length === 0) {
10395
- console.log(chalk25.dim("No plugins installed."));
10396
- console.log(chalk25.dim("Install one with: todoist plugin add github:user/repo"));
10397
- return;
10398
- }
10399
- for (const p of plugins) {
10400
- const status = p.enabled ? chalk25.green("●") : chalk25.red("○");
10401
- console.log(` ${status} ${chalk25.bold(p.name)} ${chalk25.dim(`(${p.source})`)}`);
10715
+ plugin.command("update").description("Update a specific plugin or all plugins").argument("[name]", "Plugin name (omit to update all)").action(async (name) => {
10716
+ try {
10717
+ if (name) {
10718
+ const result = await updatePlugin(name);
10719
+ if (result.updated) {
10720
+ console.log(chalk25.green(`${result.name}: ${result.message}`));
10721
+ } else {
10722
+ console.log(chalk25.dim(`${result.name}: ${result.message}`));
10723
+ }
10724
+ } else {
10725
+ console.log(chalk25.dim("Updating all plugins..."));
10726
+ const results = await updateAllPlugins();
10727
+ if (results.length === 0) {
10728
+ console.log(chalk25.dim("No plugins installed."));
10729
+ return;
10730
+ }
10731
+ for (const result of results) {
10732
+ if (result.updated) {
10733
+ console.log(chalk25.green(` ${result.name}: ${result.message}`));
10734
+ } else {
10735
+ console.log(chalk25.dim(` ${result.name}: ${result.message}`));
10736
+ }
10737
+ }
10738
+ }
10739
+ } catch (err) {
10740
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10741
+ process.exit(1);
10402
10742
  }
10403
10743
  });
10404
10744
  plugin.command("enable").description("Enable a disabled plugin").argument("<name>", "Plugin name").action((name) => {
10405
10745
  try {
10406
10746
  enablePlugin(name);
10407
- console.log(chalk25.green(`✓ Enabled ${name}`));
10747
+ console.log(chalk25.green(`Enabled ${name}`));
10408
10748
  } catch (err) {
10409
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10749
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10410
10750
  process.exit(1);
10411
10751
  }
10412
10752
  });
10413
10753
  plugin.command("disable").description("Disable a plugin without removing it").argument("<name>", "Plugin name").action((name) => {
10414
10754
  try {
10415
10755
  disablePlugin(name);
10416
- console.log(chalk25.yellow(`○ Disabled ${name}`));
10756
+ console.log(chalk25.yellow(`Disabled ${name}`));
10417
10757
  } catch (err) {
10418
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10758
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10419
10759
  process.exit(1);
10420
10760
  }
10421
10761
  });
10422
- plugin.command("update").description("Update a plugin (or all plugins)").argument("[name]", "Plugin name (omit to update all)").action((name) => {
10762
+ const marketplace = plugin.command("marketplace").description("Manage plugin marketplaces");
10763
+ marketplace.command("list").description("List registered marketplaces").action(() => {
10423
10764
  try {
10424
- const plugins = name ? [{ name }] : listPlugins();
10425
- if (plugins.length === 0) {
10426
- console.log(chalk25.dim("No plugins installed."));
10427
- return;
10428
- }
10429
- for (const p of plugins) {
10430
- try {
10431
- const result = updatePlugin(p.name);
10432
- if (result.updated) {
10433
- console.log(chalk25.green(`✓ ${result.name}: ${result.message}`));
10434
- } else {
10435
- console.log(chalk25.dim(` ${result.name}: ${result.message}`));
10436
- }
10437
- } catch (err) {
10438
- console.error(chalk25.red(`✗ ${p.name}: ${err instanceof Error ? err.message : err}`));
10439
- }
10765
+ const marketplaces = getRegisteredMarketplaces();
10766
+ console.log(chalk25.bold("Registered Marketplaces"));
10767
+ console.log("");
10768
+ for (const m of marketplaces) {
10769
+ const autoUpdate = m.autoUpdate ? chalk25.green("auto-update") : chalk25.dim("manual");
10770
+ console.log(` ${chalk25.bold(m.name)} ${chalk25.dim(m.source)} ${autoUpdate}`);
10440
10771
  }
10441
10772
  } catch (err) {
10442
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10773
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10774
+ process.exit(1);
10775
+ }
10776
+ });
10777
+ marketplace.command("add").description("Add a marketplace (e.g. github:user/repo)").argument("<source>", "Marketplace source (github:user/repo)").action((source) => {
10778
+ try {
10779
+ const name = addMarketplace(source);
10780
+ console.log(chalk25.green(`Added marketplace "${name}" from ${source}`));
10781
+ } catch (err) {
10782
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10783
+ process.exit(1);
10784
+ }
10785
+ });
10786
+ marketplace.command("remove").description("Remove a registered marketplace").argument("<name>", "Marketplace name").action((name) => {
10787
+ try {
10788
+ removeMarketplace(name);
10789
+ console.log(chalk25.green(`Removed marketplace "${name}"`));
10790
+ } catch (err) {
10791
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10792
+ process.exit(1);
10793
+ }
10794
+ });
10795
+ marketplace.command("refresh").description("Refresh marketplace cache").argument("[name]", "Marketplace name (omit to refresh all)").action(async (name) => {
10796
+ try {
10797
+ console.log(chalk25.dim(`Refreshing ${name ? `"${name}"` : "all marketplaces"}...`));
10798
+ await refreshMarketplaceCache(name);
10799
+ console.log(chalk25.green(`Marketplace cache refreshed.`));
10800
+ } catch (err) {
10801
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10443
10802
  process.exit(1);
10444
10803
  }
10445
10804
  });
@@ -10524,7 +10883,7 @@ async function runWithWatch(interval, fn) {
10524
10883
  await new Promise((r) => setTimeout(r, interval * 1000));
10525
10884
  }
10526
10885
  }
10527
- program.name("todoist").description("CLI tool for managing Todoist tasks").version("0.2.0").option("--debug", "Enable debug output").hook("preAction", () => {
10886
+ program.name("todoist").description("CLI tool for managing Todoist tasks").version("0.4.0").option("--debug", "Enable debug output").hook("preAction", () => {
10528
10887
  if (program.opts().debug) {
10529
10888
  setDebug(true);
10530
10889
  debug("Debug mode enabled");
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "todoist-cli-official",
3
+ "description": "Official todoist-cli plugin marketplace",
4
+ "version": "1.0.0",
5
+ "plugins": [
6
+ {
7
+ "name": "time-tracking",
8
+ "source": "./plugins/time-tracking",
9
+ "version": "1.0.0",
10
+ "description": "Track time spent on tasks",
11
+ "author": "todoist-cli",
12
+ "category": "productivity",
13
+ "keywords": ["time", "tracking", "timer", "report"]
14
+ }
15
+ ]
16
+ }
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@mindtnv/todoist-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A fast, keyboard-driven Todoist client for the terminal — interactive TUI and scriptable CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "todoist": "dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ "marketplace.json"
11
12
  ],
12
13
  "scripts": {
13
14
  "dev": "bun run src/cli/index.ts",