@mindtnv/todoist-cli 0.3.1 → 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
@@ -10216,223 +10216,589 @@ function registerFilterCommand(program) {
10216
10216
  // src/cli/plugin.ts
10217
10217
  import chalk25 from "chalk";
10218
10218
 
10219
- // src/plugins/installer.ts
10219
+ // src/plugins/marketplace.ts
10220
10220
  init_config();
10221
- import { join as join6, resolve } from "path";
10222
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, rmSync, symlinkSync, lstatSync } from "fs";
10221
+ import { join as join6 } from "path";
10223
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";
10224
10230
  import { execSync } from "child_process";
10225
- var PLUGINS_DIR2 = join6(homedir4(), ".config", "todoist-cli", "plugins");
10226
- function ensurePluginsDir() {
10227
- if (!existsSync6(PLUGINS_DIR2)) {
10228
- mkdirSync4(PLUGINS_DIR2, { recursive: true });
10229
- }
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] };
10230
10248
  }
10231
- function derivePluginName(source) {
10249
+ function deriveNameFromSource(source) {
10232
10250
  if (source.startsWith("github:")) {
10233
10251
  const parts = source.replace("github:", "").split("/");
10234
10252
  return parts[parts.length - 1] ?? source;
10235
10253
  }
10236
- if (source.startsWith("npm:")) {
10237
- 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;
10238
10260
  }
10239
- return source.split("/").pop() ?? source;
10240
10261
  }
10241
- async function installPlugin(source) {
10242
- ensurePluginsDir();
10243
- let name = derivePluginName(source);
10244
- const targetDir = join6(PLUGINS_DIR2, name);
10245
- if (existsSync6(targetDir)) {
10246
- 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
+ }
10247
10284
  }
10248
- if (source.startsWith("github:")) {
10249
- const repo = source.replace("github:", "");
10250
- execSync(`git clone https://github.com/${repo}.git "${targetDir}"`, { stdio: "pipe" });
10251
- } else if (source.startsWith("npm:")) {
10252
- const pkg = source.replace("npm:", "");
10253
- mkdirSync4(targetDir, { recursive: true });
10254
- 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);
10255
10394
  } else {
10256
- const resolvedPath = resolve(process.cwd(), source);
10257
- if (!existsSync6(resolvedPath))
10258
- throw new Error(`Local path not found: ${resolvedPath}`);
10259
- 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
+ }
10260
10416
  }
10261
- const isSymlink = lstatSync(targetDir).isSymbolicLink();
10262
- if (!isSymlink && existsSync6(join6(targetDir, "package.json"))) {
10417
+ if (existsSync6(join6(targetDir, "package.json"))) {
10263
10418
  try {
10264
10419
  execSync(`cd "${targetDir}" && bun install`, { stdio: "pipe" });
10265
10420
  } catch {
10266
- execSync(`cd "${targetDir}" && npm install`, { stdio: "pipe" });
10421
+ try {
10422
+ execSync(`cd "${targetDir}" && npm install`, { stdio: "pipe" });
10423
+ } catch {}
10267
10424
  }
10268
10425
  }
10269
- const manifestPath = join6(targetDir, "plugin.json");
10270
- let manifest = null;
10271
- if (existsSync6(manifestPath)) {
10272
- manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
10273
- name = manifest.name;
10274
- }
10275
- setPluginEntry(name, { source });
10426
+ setPluginEntry(pluginName, {
10427
+ source: `${pluginName}@${plugin.marketplace}`,
10428
+ enabled: true
10429
+ });
10276
10430
  return {
10277
- name,
10278
- version: manifest?.version ?? "unknown",
10279
- description: manifest?.description,
10280
- permissions: manifest?.permissions
10431
+ name: pluginName,
10432
+ version: plugin.version ?? "unknown",
10433
+ marketplace: plugin.marketplace,
10434
+ description: plugin.description
10281
10435
  };
10282
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
+ }
10283
10476
  function removePlugin(name) {
10284
10477
  const targetDir = join6(PLUGINS_DIR2, name);
10285
- if (!existsSync6(targetDir)) {
10286
- throw new Error(`Plugin "${name}" is not installed.`);
10287
- }
10288
- if (lstatSync(targetDir).isSymbolicLink()) {
10289
- rmSync(targetDir);
10290
- } else {
10478
+ if (existsSync6(targetDir)) {
10291
10479
  rmSync(targetDir, { recursive: true, force: true });
10292
10480
  }
10293
10481
  removePluginEntry(name);
10294
10482
  }
10295
- function enablePlugin(name) {
10483
+ async function updatePlugin(name) {
10296
10484
  const config = getConfig();
10297
10485
  const plugins = config.plugins;
10298
10486
  if (!plugins?.[name]) {
10299
10487
  throw new Error(`Plugin "${name}" is not installed.`);
10300
10488
  }
10301
- const { enabled, ...rest } = plugins[name];
10302
- setPluginEntry(name, rest);
10303
- }
10304
- function disablePlugin(name) {
10305
- const config = getConfig();
10306
- const plugins = config.plugins;
10307
- if (!plugins?.[name]) {
10308
- 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" };
10309
10493
  }
10310
- setPluginEntry(name, { ...plugins[name], enabled: false });
10311
- }
10312
- function updatePlugin(name) {
10313
- const config = getConfig();
10314
- const plugins = config.plugins;
10315
- if (!plugins?.[name]) {
10316
- 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" };
10317
10498
  }
10318
- const targetDir = join6(PLUGINS_DIR2, name);
10319
- if (!existsSync6(targetDir)) {
10320
- 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` };
10321
10504
  }
10322
- const source = plugins[name].source;
10323
- const isSymlink = lstatSync(targetDir).isSymbolicLink();
10324
- if (isSymlink) {
10325
- 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}"` };
10326
10510
  }
10327
- if (source.startsWith("github:")) {
10328
- execSync(`cd "${targetDir}" && git pull`, { stdio: "pipe" });
10329
- if (existsSync6(join6(targetDir, "package.json"))) {
10330
- try {
10331
- execSync(`cd "${targetDir}" && bun install`, { stdio: "pipe" });
10332
- } catch {
10333
- 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 });
10334
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" });
10335
10539
  }
10336
- return { name, updated: true, message: "Pulled latest from git" };
10337
10540
  }
10338
- if (source.startsWith("npm:")) {
10339
- const pkg = source.replace("npm:", "");
10340
- execSync(`cd "${targetDir}" && npm update ${pkg}`, { stdio: "pipe" });
10341
- 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
+ }
10342
10549
  }
10343
- 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
+ };
10344
10562
  }
10345
- function listPlugins() {
10563
+ async function updateAllPlugins() {
10346
10564
  const config = getConfig();
10347
10565
  const plugins = config.plugins;
10348
10566
  if (!plugins)
10349
10567
  return [];
10350
- return Object.entries(plugins).map(([name, cfg]) => ({
10351
- name,
10352
- source: cfg.source,
10353
- enabled: cfg.enabled !== false
10354
- }));
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 });
10355
10619
  }
10356
10620
 
10357
10621
  // src/cli/plugin.ts
10622
+ init_config();
10358
10623
  function registerPluginCommand(program) {
10359
- const plugin = program.command("plugin").description("Manage plugins");
10360
- 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(() => {
10361
10626
  try {
10362
- console.log(chalk25.dim(`Installing plugin from ${source}...`));
10363
- const result = await installPlugin(source);
10364
- console.log(chalk25.green(`✓ Installed ${result.name} v${result.version}`));
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 () => {
10650
+ try {
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}`));
10365
10698
  if (result.description) {
10366
10699
  console.log(chalk25.dim(` ${result.description}`));
10367
10700
  }
10368
- if (result.permissions?.length) {
10369
- console.log(chalk25.dim(` Permissions: ${result.permissions.join(", ")}`));
10370
- }
10371
10701
  } catch (err) {
10372
- 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)}`));
10373
10703
  process.exit(1);
10374
10704
  }
10375
10705
  });
10376
10706
  plugin.command("remove").description("Remove an installed plugin").argument("<name>", "Plugin name").action((name) => {
10377
10707
  try {
10378
10708
  removePlugin(name);
10379
- console.log(chalk25.green(`✓ Removed ${name}`));
10709
+ console.log(chalk25.green(`Removed ${name}`));
10380
10710
  } catch (err) {
10381
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10711
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10382
10712
  process.exit(1);
10383
10713
  }
10384
10714
  });
10385
- plugin.command("list").description("List installed plugins").action(() => {
10386
- const plugins = listPlugins();
10387
- if (plugins.length === 0) {
10388
- console.log(chalk25.dim("No plugins installed."));
10389
- console.log(chalk25.dim("Install one with: todoist plugin add github:user/repo"));
10390
- return;
10391
- }
10392
- for (const p of plugins) {
10393
- const status = p.enabled ? chalk25.green("●") : chalk25.red("○");
10394
- 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);
10395
10742
  }
10396
10743
  });
10397
10744
  plugin.command("enable").description("Enable a disabled plugin").argument("<name>", "Plugin name").action((name) => {
10398
10745
  try {
10399
10746
  enablePlugin(name);
10400
- console.log(chalk25.green(`✓ Enabled ${name}`));
10747
+ console.log(chalk25.green(`Enabled ${name}`));
10401
10748
  } catch (err) {
10402
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10749
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10403
10750
  process.exit(1);
10404
10751
  }
10405
10752
  });
10406
10753
  plugin.command("disable").description("Disable a plugin without removing it").argument("<name>", "Plugin name").action((name) => {
10407
10754
  try {
10408
10755
  disablePlugin(name);
10409
- console.log(chalk25.yellow(`○ Disabled ${name}`));
10756
+ console.log(chalk25.yellow(`Disabled ${name}`));
10410
10757
  } catch (err) {
10411
- console.error(chalk25.red(err instanceof Error ? err.message : String(err)));
10758
+ console.error(chalk25.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
10412
10759
  process.exit(1);
10413
10760
  }
10414
10761
  });
10415
- 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(() => {
10416
10764
  try {
10417
- const plugins = name ? [{ name }] : listPlugins();
10418
- if (plugins.length === 0) {
10419
- console.log(chalk25.dim("No plugins installed."));
10420
- return;
10421
- }
10422
- for (const p of plugins) {
10423
- try {
10424
- const result = updatePlugin(p.name);
10425
- if (result.updated) {
10426
- console.log(chalk25.green(`✓ ${result.name}: ${result.message}`));
10427
- } else {
10428
- console.log(chalk25.dim(` ${result.name}: ${result.message}`));
10429
- }
10430
- } catch (err) {
10431
- console.error(chalk25.red(`✗ ${p.name}: ${err instanceof Error ? err.message : err}`));
10432
- }
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}`);
10433
10771
  }
10434
10772
  } catch (err) {
10435
- 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)}`));
10436
10802
  process.exit(1);
10437
10803
  }
10438
10804
  });
@@ -10517,7 +10883,7 @@ async function runWithWatch(interval, fn) {
10517
10883
  await new Promise((r) => setTimeout(r, interval * 1000));
10518
10884
  }
10519
10885
  }
10520
- 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", () => {
10521
10887
  if (program.opts().debug) {
10522
10888
  setDebug(true);
10523
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.1",
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",