@tomkapa/tayto 0.5.4 → 0.6.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
@@ -33,7 +33,9 @@ function loadConfig() {
33
33
  dbPath: process.env["TASK_DB_PATH"] ?? join(dataDir, "data.db"),
34
34
  logDir,
35
35
  logLevel: process.env["TASK_LOG_LEVEL"] ?? "info",
36
- otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]
36
+ otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
37
+ updateCachePath: join(dataDir, "update-check.json"),
38
+ noUpdateCheck: process.env["TAYTO_NO_UPDATE_CHECK"] === "1"
37
39
  };
38
40
  }
39
41
 
@@ -109,6 +111,10 @@ async function shutdownTelemetry() {
109
111
  }
110
112
  }
111
113
 
114
+ // src/cli/container.ts
115
+ import { join as join3 } from "path";
116
+ import { tmpdir } from "os";
117
+
112
118
  // src/errors/app-error.ts
113
119
  var AppError = class extends Error {
114
120
  constructor(code, message, cause) {
@@ -120,6 +126,9 @@ var AppError = class extends Error {
120
126
  code;
121
127
  cause;
122
128
  };
129
+ function toMessage(e) {
130
+ return e instanceof Error ? e.message : String(e);
131
+ }
123
132
 
124
133
  // src/repository/project.repository.ts
125
134
  import { ulid } from "ulid";
@@ -1997,8 +2006,141 @@ var PortabilityServiceImpl = class {
1997
2006
  }
1998
2007
  };
1999
2008
 
2009
+ // src/service/update.service.ts
2010
+ import { readFileSync as readFileSync2, writeFileSync } from "fs";
2011
+ import { execFileSync } from "child_process";
2012
+ import { trace } from "@opentelemetry/api";
2013
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@tomkapa/tayto/latest";
2014
+ var PACKAGE_NAME = "@tomkapa/tayto";
2015
+ var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
2016
+ function isValidCache(value) {
2017
+ if (typeof value !== "object" || value === null) return false;
2018
+ const obj = value;
2019
+ return typeof obj["checkedAt"] === "number" && typeof obj["latestVersion"] === "string";
2020
+ }
2021
+ function isNewerVersion(a, b) {
2022
+ const parse = (v) => {
2023
+ const parts = v.replace(/^v/, "").split(".").map(Number);
2024
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
2025
+ };
2026
+ const [aMaj, aMin, aPatch] = parse(a);
2027
+ const [bMaj, bMin, bPatch] = parse(b);
2028
+ if (aMaj !== bMaj) return aMaj > bMaj;
2029
+ if (aMin !== bMin) return aMin > bMin;
2030
+ return aPatch > bPatch;
2031
+ }
2032
+ function defaultExec(cmd, args) {
2033
+ execFileSync(cmd, args, { stdio: "inherit", timeout: 6e4 });
2034
+ }
2035
+ var tracer = trace.getTracer("task");
2036
+ var UpdateServiceImpl = class {
2037
+ constructor(cachePath, fetchImpl = globalThis.fetch, execImpl = defaultExec) {
2038
+ this.cachePath = cachePath;
2039
+ this.fetchImpl = fetchImpl;
2040
+ this.execImpl = execImpl;
2041
+ }
2042
+ cachePath;
2043
+ fetchImpl;
2044
+ execImpl;
2045
+ async checkForUpdate(currentVersion) {
2046
+ return tracer.startActiveSpan("UpdateService.checkForUpdate", async (span) => {
2047
+ try {
2048
+ span.setAttribute("update.current_version", currentVersion);
2049
+ const cached = this.readCache();
2050
+ if (cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
2051
+ span.setAttribute("update.cache_hit", true);
2052
+ const updateAvailable2 = isNewerVersion(cached.latestVersion, currentVersion);
2053
+ span.setAttribute("update.latest_version", cached.latestVersion);
2054
+ span.setAttribute("update.available", updateAvailable2);
2055
+ return ok({ currentVersion, latestVersion: cached.latestVersion, updateAvailable: updateAvailable2 });
2056
+ }
2057
+ span.setAttribute("update.cache_hit", false);
2058
+ let latestVersion;
2059
+ const controller = new AbortController();
2060
+ const timeout = setTimeout(() => {
2061
+ controller.abort();
2062
+ }, 5e3);
2063
+ try {
2064
+ const response = await this.fetchImpl(NPM_REGISTRY_URL, { signal: controller.signal });
2065
+ if (!response.ok) {
2066
+ logger.warn("Update check: registry returned non-OK status", {
2067
+ status: response.status
2068
+ });
2069
+ return err(
2070
+ new AppError(
2071
+ "UPGRADE_CHECK",
2072
+ `npm registry returned status ${String(response.status)}`
2073
+ )
2074
+ );
2075
+ }
2076
+ const body = await response.json();
2077
+ if (typeof body["version"] !== "string") {
2078
+ return err(
2079
+ new AppError("UPGRADE_CHECK", "Unexpected response format from npm registry")
2080
+ );
2081
+ }
2082
+ latestVersion = body["version"];
2083
+ } catch (e) {
2084
+ logger.warn("Update check: fetch failed", { error: toMessage(e) });
2085
+ return err(
2086
+ new AppError("UPGRADE_CHECK", `Failed to check for updates: ${toMessage(e)}`, e)
2087
+ );
2088
+ } finally {
2089
+ clearTimeout(timeout);
2090
+ }
2091
+ this.writeCache({ checkedAt: Date.now(), latestVersion });
2092
+ const updateAvailable = isNewerVersion(latestVersion, currentVersion);
2093
+ span.setAttribute("update.latest_version", latestVersion);
2094
+ span.setAttribute("update.available", updateAvailable);
2095
+ return ok({ currentVersion, latestVersion, updateAvailable });
2096
+ } finally {
2097
+ span.end();
2098
+ }
2099
+ });
2100
+ }
2101
+ performUpgrade(currentVersion) {
2102
+ return logger.startSpan("UpdateService.performUpgrade", (span) => {
2103
+ span.setAttribute("update.previous_version", currentVersion);
2104
+ try {
2105
+ this.execImpl("npm", ["install", "-g", `${PACKAGE_NAME}@latest`]);
2106
+ } catch (e) {
2107
+ logger.error("Upgrade failed", e, { command: `npm install -g ${PACKAGE_NAME}@latest` });
2108
+ span.setAttribute("update.success", false);
2109
+ return err(new AppError("UPGRADE_CHECK", `Upgrade failed: ${toMessage(e)}`, e));
2110
+ }
2111
+ const cached = this.readCache();
2112
+ const installedVersion = cached?.latestVersion ?? "unknown";
2113
+ span.setAttribute("update.success", true);
2114
+ span.setAttribute("update.installed_version", installedVersion);
2115
+ this.writeCache({ checkedAt: 0, latestVersion: installedVersion });
2116
+ return ok({ installedVersion });
2117
+ });
2118
+ }
2119
+ readCache() {
2120
+ try {
2121
+ const raw = readFileSync2(this.cachePath, "utf-8");
2122
+ const parsed = JSON.parse(raw);
2123
+ return isValidCache(parsed) ? parsed : null;
2124
+ } catch (e) {
2125
+ logger.info("Update cache not available", {
2126
+ error: toMessage(e)
2127
+ });
2128
+ return null;
2129
+ }
2130
+ }
2131
+ writeCache(entry) {
2132
+ try {
2133
+ writeFileSync(this.cachePath, JSON.stringify(entry), "utf-8");
2134
+ } catch (e) {
2135
+ logger.warn("Failed to write update cache", {
2136
+ error: toMessage(e)
2137
+ });
2138
+ }
2139
+ }
2140
+ };
2141
+
2000
2142
  // src/cli/container.ts
2001
- function createContainer(db, dbPath, detectGitRemote2) {
2143
+ function createContainer(db, dbPath, detectGitRemote2, updateCachePath) {
2002
2144
  const projectRepo = new SqliteProjectRepository(db);
2003
2145
  const taskRepo = new SqliteTaskRepository(db);
2004
2146
  const depRepo = new SqliteDependencyRepository(db);
@@ -2006,12 +2148,25 @@ function createContainer(db, dbPath, detectGitRemote2) {
2006
2148
  const dependencyService = new DependencyServiceImpl(depRepo, taskRepo);
2007
2149
  const taskService = new TaskServiceImpl(taskRepo, projectService, () => dependencyService);
2008
2150
  const portabilityService = new PortabilityServiceImpl(taskService, dependencyService);
2009
- return { dbPath, projectService, taskService, dependencyService, portabilityService };
2151
+ const updateService = new UpdateServiceImpl(
2152
+ updateCachePath ?? join3(tmpdir(), "tayto-update-check.json")
2153
+ );
2154
+ return {
2155
+ dbPath,
2156
+ projectService,
2157
+ taskService,
2158
+ dependencyService,
2159
+ portabilityService,
2160
+ updateService
2161
+ };
2010
2162
  }
2011
2163
 
2012
2164
  // src/cli/index.ts
2013
2165
  import { Command } from "commander";
2014
2166
 
2167
+ // src/version.ts
2168
+ var APP_VERSION = true ? "0.6.0" : "0.0.0-dev";
2169
+
2015
2170
  // src/cli/output.ts
2016
2171
  function printSuccess(data) {
2017
2172
  process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n");
@@ -2204,12 +2359,12 @@ function registerTaskDelete(parent, container) {
2204
2359
  }
2205
2360
 
2206
2361
  // src/cli/commands/task/breakdown.ts
2207
- import { readFileSync as readFileSync2 } from "fs";
2362
+ import { readFileSync as readFileSync3 } from "fs";
2208
2363
  function registerTaskBreakdown(parent, container) {
2209
2364
  parent.command("breakdown <parentId>").description("Create subtasks from a JSON file").requiredOption("-f, --file <path>", "JSON file with array of subtask definitions").action((parentId, opts) => {
2210
2365
  let content;
2211
2366
  try {
2212
- content = readFileSync2(opts.file, "utf-8");
2367
+ content = readFileSync3(opts.file, "utf-8");
2213
2368
  } catch (e) {
2214
2369
  return printError(
2215
2370
  new AppError("VALIDATION", `Failed to read subtasks file: ${opts.file}`, e)
@@ -2263,7 +2418,7 @@ function registerTaskSearch(parent, container) {
2263
2418
  }
2264
2419
 
2265
2420
  // src/cli/commands/task/export.ts
2266
- import { writeFileSync } from "fs";
2421
+ import { writeFileSync as writeFileSync2 } from "fs";
2267
2422
  function registerTaskExport(parent, container) {
2268
2423
  parent.command("export").description("Export tasks to JSON file").option("-p, --project <project>", "Project id or name").option("-o, --output <file>", "Output file path (defaults to stdout)").action((opts) => {
2269
2424
  const projectResult = withProject(container, opts.project);
@@ -2274,7 +2429,7 @@ function registerTaskExport(parent, container) {
2274
2429
  }
2275
2430
  if (opts.output) {
2276
2431
  try {
2277
- writeFileSync(opts.output, JSON.stringify(result.value, null, 2) + "\n", "utf-8");
2432
+ writeFileSync2(opts.output, JSON.stringify(result.value, null, 2) + "\n", "utf-8");
2278
2433
  } catch (e) {
2279
2434
  return printError(new AppError("UNKNOWN", `Failed to write file: ${opts.output}`, e));
2280
2435
  }
@@ -2290,7 +2445,7 @@ function registerTaskExport(parent, container) {
2290
2445
  }
2291
2446
 
2292
2447
  // src/cli/commands/task/import.ts
2293
- import { readFileSync as readFileSync3 } from "fs";
2448
+ import { readFileSync as readFileSync4 } from "fs";
2294
2449
  function registerTaskImport(parent, container) {
2295
2450
  parent.command("import").description("Import tasks from JSON file").requiredOption("-f, --file <file>", "Input JSON file path").option("-p, --project <project>", "Target project id or name").option(
2296
2451
  "--map <mapping>",
@@ -2298,7 +2453,7 @@ function registerTaskImport(parent, container) {
2298
2453
  ).action((opts) => {
2299
2454
  let fileData;
2300
2455
  try {
2301
- const raw = readFileSync3(opts.file, "utf-8");
2456
+ const raw = readFileSync4(opts.file, "utf-8");
2302
2457
  fileData = JSON.parse(raw);
2303
2458
  } catch (e) {
2304
2459
  return printError(
@@ -2372,10 +2527,42 @@ function registerDepGraph(parent, container) {
2372
2527
  });
2373
2528
  }
2374
2529
 
2530
+ // src/cli/commands/upgrade.ts
2531
+ function registerUpgrade(program, container) {
2532
+ program.command("upgrade").description("Check for updates and upgrade to the latest version").action(async () => {
2533
+ const checkResult = await container.updateService.checkForUpdate(APP_VERSION);
2534
+ if (!checkResult.ok) {
2535
+ printError(checkResult.error);
2536
+ return;
2537
+ }
2538
+ if (!checkResult.value.updateAvailable) {
2539
+ printSuccess({
2540
+ message: "Already up to date",
2541
+ currentVersion: checkResult.value.currentVersion
2542
+ });
2543
+ return;
2544
+ }
2545
+ process.stderr.write(
2546
+ `Upgrading from ${checkResult.value.currentVersion} to ${checkResult.value.latestVersion}...
2547
+ `
2548
+ );
2549
+ const upgradeResult = container.updateService.performUpgrade(APP_VERSION);
2550
+ if (!upgradeResult.ok) {
2551
+ printError(upgradeResult.error);
2552
+ return;
2553
+ }
2554
+ printSuccess({
2555
+ message: "Upgrade complete",
2556
+ previousVersion: checkResult.value.currentVersion,
2557
+ installedVersion: upgradeResult.value.installedVersion
2558
+ });
2559
+ });
2560
+ }
2561
+
2375
2562
  // src/cli/index.ts
2376
2563
  function buildCLI(container) {
2377
2564
  const program = new Command();
2378
- program.name("tayto").description("CLI task management for solo devs and AI agents").version("0.1.0");
2565
+ program.name("tayto").description("CLI task management for solo devs and AI agents").version(APP_VERSION);
2379
2566
  const project = program.command("project").description("Manage projects");
2380
2567
  registerProjectCreate(project, container);
2381
2568
  registerProjectList(project, container);
@@ -2400,26 +2587,63 @@ function buildCLI(container) {
2400
2587
  registerDepRemove(dep, container);
2401
2588
  registerDepList(dep, container);
2402
2589
  registerDepGraph(dep, container);
2590
+ registerUpgrade(program, container);
2403
2591
  program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
2404
- const { launchTUI } = await import("./tui-24ZW56Q6.js");
2592
+ const { launchTUI } = await import("./tui-WMESKCRD.js");
2405
2593
  await launchTUI(container, opts.project);
2406
2594
  });
2407
2595
  return program;
2408
2596
  }
2409
2597
 
2410
2598
  // src/index.ts
2599
+ async function checkForUpdateQuietly(container, currentVersion) {
2600
+ let timerId;
2601
+ try {
2602
+ const result = await Promise.race([
2603
+ container.updateService.checkForUpdate(currentVersion),
2604
+ new Promise((resolve) => {
2605
+ timerId = setTimeout(() => {
2606
+ resolve(null);
2607
+ }, 2e3);
2608
+ })
2609
+ ]);
2610
+ clearTimeout(timerId);
2611
+ if (result !== null && result.ok) {
2612
+ return result.value;
2613
+ }
2614
+ return null;
2615
+ } catch {
2616
+ clearTimeout(timerId);
2617
+ return null;
2618
+ }
2619
+ }
2411
2620
  async function main() {
2412
2621
  const config = loadConfig();
2413
2622
  logger.init(config.logDir);
2414
2623
  initTelemetry(config);
2415
2624
  const db = createDatabase(config.dbPath);
2416
2625
  runMigrations(db);
2417
- const container = createContainer(db, config.dbPath);
2626
+ const container = createContainer(db, config.dbPath, void 0, config.updateCachePath);
2418
2627
  const args = process.argv.slice(2);
2628
+ const isUpgradeCommand = args[0] === "upgrade";
2629
+ let updateCheck = null;
2630
+ if (!config.noUpdateCheck && !isUpgradeCommand) {
2631
+ updateCheck = await checkForUpdateQuietly(container, APP_VERSION);
2632
+ }
2419
2633
  if (args.length === 0) {
2420
- const { launchTUI } = await import("./tui-24ZW56Q6.js");
2421
- await launchTUI(container);
2634
+ const { launchTUI } = await import("./tui-WMESKCRD.js");
2635
+ await launchTUI(
2636
+ container,
2637
+ void 0,
2638
+ updateCheck?.updateAvailable ? updateCheck.latestVersion : void 0
2639
+ );
2422
2640
  } else {
2641
+ if (updateCheck?.updateAvailable) {
2642
+ process.stderr.write(
2643
+ `\x1B[33m[tayto]\x1B[0m Update available: ${updateCheck.currentVersion} \u2192 ${updateCheck.latestVersion}. Run: tayto upgrade
2644
+ `
2645
+ );
2646
+ }
2423
2647
  const program = buildCLI(container);
2424
2648
  await program.parseAsync(process.argv);
2425
2649
  }