@tomkapa/tayto 0.7.0 → 0.8.1

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.
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // <define:__CHANGELOG_ENTRIES__>
4
+ var define_CHANGELOG_ENTRIES_default = [{ version: "0.8.1", date: "2026-04-14", sections: [{ heading: "Changed", items: ["Lower minimum Node.js requirement from >=25 to >=22"] }] }, { version: "0.8.0", date: "2026-04-13", sections: [{ heading: "Added", items: ["What's new ticker and changelog dialog for post-upgrade visibility"] }] }, { version: "0.7.0", date: "2026-04-12", sections: [{ heading: "Added", items: ["Prompt to create project when unlinked git remote is detected on TUI startup", "Project edit support in TUI and `--git-remote` flag for CLI", "Git remote field to create project form", "Arrow key navigation and inline cursor movement in form fields"] }, { heading: "Changed", items: ["Introduce `GitRemote` value object to enforce URL normalization"] }, { heading: "Fixed", items: ["Restore terminal raw mode around external editor to prevent readonly mode"] }] }, { version: "0.6.0", date: "2026-04-12", sections: [{ heading: "Added", items: ["Auto-upgrade with npm registry version check and `tayto upgrade` command", "Contextual hints in TUI for discoverability", "Shift+Tab panel navigation in TUI"] }, { heading: "Changed", items: ["Improve TUI intuitiveness with better keyboard shortcut guidance"] }, { heading: "Chore", items: ["Add open-source project scaffolding and fix CI node version", "Add demo video to README"] }] }, { version: "0.5.0", date: "2026-04-10", sections: [{ heading: "Changed", items: ["Redesign TUI color theme around #9B9BA5 (logo helmet grey) for consistent palette", "Redesign header with 3-column layout: logo, product info, and shortcut hints", "Integrate Tayto pixel logo into TUI header bar"] }, { heading: "Improved", items: ["Consolidate logo assets into `src/tui/assets/` and `src/tui/components/Logo.tsx`", "Theme colors now import from logo PALETTE for single source of truth", "Memoize Logo component to avoid unnecessary re-renders"] }, { heading: "Removed", items: ["Standalone `tayto-logo/` directory (AGENT.md, preview PNGs)"] }] }, { version: "0.4.1", date: "2026-04-10", sections: [{ heading: "Fixed", items: ["Prevent rank collisions from floating-point precision collapse", "Scope `listTasks` and `searchTasks` to default project when none specified"] }, { heading: "Changed", items: ["Resolve project at CLI/TUI boundary; services now accept `Project` directly"] }] }, { version: "0.4.0", date: "2026-04-09", sections: [{ heading: "Added", items: ["Link projects to git remotes for automatic project selection", "Running `tayto` inside a git repo auto-selects the linked project"] }] }, { version: "0.3.0", date: null, sections: [{ heading: "Added", items: ["Dependency management (`tayto dep add/remove/list/graph`)", "Dependency types: `blocks`, `blocked-by`, `relates-to`, `duplicates`", "Task breakdown command to split tasks into subtasks"] }] }, { version: "0.2.0", date: null, sections: [{ heading: "Added", items: ["TUI (terminal UI) built with Ink/React", "Full-text search via SQLite FTS5", "Task export and import (JSON)", "OpenTelemetry tracing on every operation"] }] }, { version: "0.1.0", date: null, sections: [{ heading: "Added", items: ["Initial release", "CLI with Commander.js", "SQLite database backend with ULID identifiers", "Fractional ranking for O(1) reorders", "Task types: `story`, `bug`, `tech-debt`", "Task statuses: `backlog` \u2192 `todo` \u2192 `in-progress` \u2192 `review` \u2192 `done` / `cancelled`", "Project management with default project support", "Zod validation across all service layer inputs"] }] }];
5
+
3
6
  // src/logging/logger.ts
4
7
  import { appendFileSync, readdirSync, unlinkSync } from "fs";
5
8
  import { join } from "path";
@@ -88,6 +91,9 @@ var Logger = class {
88
91
  };
89
92
  var logger = new Logger();
90
93
 
94
+ // src/version.ts
95
+ var APP_VERSION = true ? "0.8.1" : "0.0.0-dev";
96
+
91
97
  // src/types/enums.ts
92
98
  var TaskStatus = {
93
99
  Backlog: "backlog",
@@ -212,7 +218,21 @@ function detectGitRemote(cwd) {
212
218
  }
213
219
  }
214
220
 
221
+ // src/utils/version.ts
222
+ function isNewerVersion(a, b) {
223
+ const parse = (v) => {
224
+ const parts = v.replace(/^v/, "").split(".").map(Number);
225
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
226
+ };
227
+ const [aMaj, aMin, aPatch] = parse(a);
228
+ const [bMaj, bMin, bPatch] = parse(b);
229
+ if (aMaj !== bMaj) return aMaj > bMaj;
230
+ if (aMin !== bMin) return aMin > bMin;
231
+ return aPatch > bPatch;
232
+ }
233
+
215
234
  export {
235
+ define_CHANGELOG_ENTRIES_default,
216
236
  logger,
217
237
  ok,
218
238
  err,
@@ -228,6 +248,8 @@ export {
228
248
  midpoint,
229
249
  TERMINAL_STATUSES,
230
250
  isTerminalStatus,
231
- detectGitRemote
251
+ detectGitRemote,
252
+ isNewerVersion,
253
+ APP_VERSION
232
254
  };
233
- //# sourceMappingURL=chunk-5V4TBQ5S.js.map
255
+ //# sourceMappingURL=chunk-6ZNCVCAS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["<define:__CHANGELOG_ENTRIES__>","../src/logging/logger.ts","../src/version.ts","../src/types/enums.ts","../src/utils/git.ts","../src/types/common.ts","../src/types/git-remote.ts","../src/utils/version.ts"],"sourcesContent":["[{\"version\":\"0.8.1\",\"date\":\"2026-04-14\",\"sections\":[{\"heading\":\"Changed\",\"items\":[\"Lower minimum Node.js requirement from >=25 to >=22\"]}]},{\"version\":\"0.8.0\",\"date\":\"2026-04-13\",\"sections\":[{\"heading\":\"Added\",\"items\":[\"What's new ticker and changelog dialog for post-upgrade visibility\"]}]},{\"version\":\"0.7.0\",\"date\":\"2026-04-12\",\"sections\":[{\"heading\":\"Added\",\"items\":[\"Prompt to create project when unlinked git remote is detected on TUI startup\",\"Project edit support in TUI and `--git-remote` flag for CLI\",\"Git remote field to create project form\",\"Arrow key navigation and inline cursor movement in form fields\"]},{\"heading\":\"Changed\",\"items\":[\"Introduce `GitRemote` value object to enforce URL normalization\"]},{\"heading\":\"Fixed\",\"items\":[\"Restore terminal raw mode around external editor to prevent readonly mode\"]}]},{\"version\":\"0.6.0\",\"date\":\"2026-04-12\",\"sections\":[{\"heading\":\"Added\",\"items\":[\"Auto-upgrade with npm registry version check and `tayto upgrade` command\",\"Contextual hints in TUI for discoverability\",\"Shift+Tab panel navigation in TUI\"]},{\"heading\":\"Changed\",\"items\":[\"Improve TUI intuitiveness with better keyboard shortcut guidance\"]},{\"heading\":\"Chore\",\"items\":[\"Add open-source project scaffolding and fix CI node version\",\"Add demo video to README\"]}]},{\"version\":\"0.5.0\",\"date\":\"2026-04-10\",\"sections\":[{\"heading\":\"Changed\",\"items\":[\"Redesign TUI color theme around #9B9BA5 (logo helmet grey) for consistent palette\",\"Redesign header with 3-column layout: logo, product info, and shortcut hints\",\"Integrate Tayto pixel logo into TUI header bar\"]},{\"heading\":\"Improved\",\"items\":[\"Consolidate logo assets into `src/tui/assets/` and `src/tui/components/Logo.tsx`\",\"Theme colors now import from logo PALETTE for single source of truth\",\"Memoize Logo component to avoid unnecessary re-renders\"]},{\"heading\":\"Removed\",\"items\":[\"Standalone `tayto-logo/` directory (AGENT.md, preview PNGs)\"]}]},{\"version\":\"0.4.1\",\"date\":\"2026-04-10\",\"sections\":[{\"heading\":\"Fixed\",\"items\":[\"Prevent rank collisions from floating-point precision collapse\",\"Scope `listTasks` and `searchTasks` to default project when none specified\"]},{\"heading\":\"Changed\",\"items\":[\"Resolve project at CLI/TUI boundary; services now accept `Project` directly\"]}]},{\"version\":\"0.4.0\",\"date\":\"2026-04-09\",\"sections\":[{\"heading\":\"Added\",\"items\":[\"Link projects to git remotes for automatic project selection\",\"Running `tayto` inside a git repo auto-selects the linked project\"]}]},{\"version\":\"0.3.0\",\"date\":null,\"sections\":[{\"heading\":\"Added\",\"items\":[\"Dependency management (`tayto dep add/remove/list/graph`)\",\"Dependency types: `blocks`, `blocked-by`, `relates-to`, `duplicates`\",\"Task breakdown command to split tasks into subtasks\"]}]},{\"version\":\"0.2.0\",\"date\":null,\"sections\":[{\"heading\":\"Added\",\"items\":[\"TUI (terminal UI) built with Ink/React\",\"Full-text search via SQLite FTS5\",\"Task export and import (JSON)\",\"OpenTelemetry tracing on every operation\"]}]},{\"version\":\"0.1.0\",\"date\":null,\"sections\":[{\"heading\":\"Added\",\"items\":[\"Initial release\",\"CLI with Commander.js\",\"SQLite database backend with ULID identifiers\",\"Fractional ranking for O(1) reorders\",\"Task types: `story`, `bug`, `tech-debt`\",\"Task statuses: `backlog` → `todo` → `in-progress` → `review` → `done` / `cancelled`\",\"Project management with default project support\",\"Zod validation across all service layer inputs\"]}]}]","import { appendFileSync, readdirSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { trace, type Span, SpanStatusCode } from '@opentelemetry/api';\n\nconst tracer = trace.getTracer('task');\nconst LOG_RETENTION_DAYS = 7;\n\nexport interface LogAttributes {\n [key: string]: string | number | boolean;\n}\n\ntype LogLevel = 'INFO' | 'WARN' | 'ERROR';\n\nfunction formatTimestamp(): string {\n return new Date().toISOString();\n}\n\nfunction formatAttrs(attrs?: LogAttributes): string {\n if (!attrs || Object.keys(attrs).length === 0) return '';\n return ' ' + JSON.stringify(attrs);\n}\n\nclass Logger {\n private logFilePath: string | null = null;\n\n init(logDir: string): void {\n const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD\n this.logFilePath = join(logDir, `task-${date}.log`);\n this.pruneOldLogs(logDir);\n }\n\n info(message: string, attrs?: LogAttributes): void {\n this.write('INFO', message, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(message, attrs);\n }\n }\n\n warn(message: string, attrs?: LogAttributes): void {\n this.write('WARN', message, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(`WARN: ${message}`, attrs);\n }\n }\n\n error(message: string, error?: unknown, attrs?: LogAttributes): void {\n const errorDetail = error instanceof Error ? ` | ${error.stack ?? error.message}` : '';\n this.write('ERROR', `${message}${errorDetail}`, attrs);\n const span = trace.getActiveSpan();\n if (span) {\n span.addEvent(`ERROR: ${message}`, attrs);\n if (error instanceof Error) {\n span.recordException(error);\n }\n span.setStatus({ code: SpanStatusCode.ERROR, message });\n }\n }\n\n startSpan<T>(name: string, fn: (span: Span) => T): T {\n return tracer.startActiveSpan(name, (span) => {\n try {\n const result = fn(span);\n span.end();\n return result;\n } catch (e) {\n if (e instanceof Error) {\n span.recordException(e);\n }\n span.setStatus({ code: SpanStatusCode.ERROR });\n span.end();\n throw e;\n }\n });\n }\n\n private write(level: LogLevel, message: string, attrs?: LogAttributes): void {\n if (!this.logFilePath) return;\n const line = `${formatTimestamp()} [${level}] ${message}${formatAttrs(attrs)}\\n`;\n try {\n appendFileSync(this.logFilePath, line);\n } catch {\n // Swallowing here is intentional: logging must never crash the app.\n // If the log file is unwritable, the OTel span still captures the event.\n }\n }\n\n private pruneOldLogs(logDir: string): void {\n try {\n const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;\n const files = readdirSync(logDir).filter((f) => f.startsWith('task-') && f.endsWith('.log'));\n for (const file of files) {\n const dateStr = file.slice('task-'.length, -'.log'.length);\n const fileDate = new Date(dateStr).getTime();\n if (!isNaN(fileDate) && fileDate < cutoff) {\n unlinkSync(join(logDir, file));\n }\n }\n } catch {\n // Best-effort cleanup — don't crash if pruning fails\n }\n }\n}\n\nexport const logger = new Logger();\n","declare const __APP_VERSION__: string;\n\nexport const APP_VERSION: string =\n typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0-dev';\n","export const TaskStatus = {\n Backlog: 'backlog',\n Todo: 'todo',\n InProgress: 'in-progress',\n Review: 'review',\n Done: 'done',\n Cancelled: 'cancelled',\n} as const;\nexport type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];\n\nexport const TaskType = {\n Epic: 'epic',\n Story: 'story',\n TechDebt: 'tech-debt',\n Bug: 'bug',\n} as const;\nexport type TaskType = (typeof TaskType)[keyof typeof TaskType];\n\n/**\n * Task level derived from type.\n * Level 1: epics (grouping/planning layer)\n * Level 2: stories, tech-debt, bugs (execution layer)\n */\nexport const TaskLevel = {\n Epic: 1,\n Work: 2,\n} as const;\nexport type TaskLevel = (typeof TaskLevel)[keyof typeof TaskLevel];\n\nconst TYPE_TO_LEVEL: Record<string, TaskLevel> = {\n [TaskType.Epic]: TaskLevel.Epic,\n [TaskType.Story]: TaskLevel.Work,\n [TaskType.TechDebt]: TaskLevel.Work,\n [TaskType.Bug]: TaskLevel.Work,\n};\n\nexport function getTaskLevel(type: string): TaskLevel {\n return TYPE_TO_LEVEL[type] ?? TaskLevel.Work;\n}\n\n/** Types that belong to the work (level 2) execution layer. */\nexport const WORK_TYPES: ReadonlySet<string> = new Set([\n TaskType.Story,\n TaskType.TechDebt,\n TaskType.Bug,\n]);\n\n/** Types stored in the database. */\nexport const DependencyType = {\n Blocks: 'blocks',\n RelatesTo: 'relates-to',\n Duplicates: 'duplicates',\n} as const;\nexport type DependencyType = (typeof DependencyType)[keyof typeof DependencyType];\n\n/**\n * UI-level dependency types — includes BlockedBy which is a reverse-Blocks\n * relationship resolved before persisting to the database.\n */\nexport const UIDependencyType = {\n ...DependencyType,\n BlockedBy: 'blocked-by',\n} as const;\nexport type UIDependencyType = (typeof UIDependencyType)[keyof typeof UIDependencyType];\n\n/** Gap between consecutive rank values, used for insertion between neighbors. */\nexport const RANK_GAP = 1000.0;\n\n/**\n * Collapse-safe midpoint between two rank values. Returns `null` when\n * IEEE 754 double precision cannot represent a strictly-between value —\n * callers use this as a signal to rebalance the rank grid and retry.\n * Returning a number unconditionally would silently collide with an\n * endpoint and corrupt the task ordering.\n */\nexport function midpoint(a: number, b: number): number | null {\n const m = (a + b) / 2;\n return m > a && m < b ? m : null;\n}\n\n/** Statuses that represent terminal/completed task states. */\nexport const TERMINAL_STATUSES: ReadonlySet<string> = new Set([\n TaskStatus.Done,\n TaskStatus.Cancelled,\n]);\n\nexport function isTerminalStatus(status: string): boolean {\n return TERMINAL_STATUSES.has(status);\n}\n","import { spawnSync } from 'node:child_process';\nimport type { Result } from '../types/common.js';\nimport { ok } from '../types/common.js';\nimport { GitRemote } from '../types/git-remote.js';\nimport { logger } from '../logging/logger.js';\n\n/**\n * Detect the git origin remote URL from the given directory.\n * Returns ok(GitRemote) if found, ok(null) if no git repo or no origin remote.\n * Never returns err() — all git failures are silent fallbacks.\n */\nexport function detectGitRemote(cwd?: string): Result<GitRemote | null> {\n try {\n const result = spawnSync('git', ['remote', 'get-url', 'origin'], {\n cwd: cwd ?? process.cwd(),\n encoding: 'utf-8',\n timeout: 5000,\n });\n\n if (result.status !== 0 || result.error) {\n logger.info('detectGitRemote: no git remote found (non-zero exit or error)');\n return ok(null);\n }\n\n const raw = result.stdout.trim();\n if (!raw) {\n logger.info('detectGitRemote: empty stdout from git remote get-url');\n return ok(null);\n }\n\n const remote = GitRemote.parse(raw);\n logger.info(`detectGitRemote: found remote=${remote.value}`);\n return ok(remote);\n } catch (e: unknown) {\n logger.info(`detectGitRemote: exception during git detection: ${String(e)}`);\n return ok(null);\n }\n}\n","import type { AppError } from '../errors/app-error.js';\n\nexport type Result<T, E = AppError> = { ok: true; value: T } | { ok: false; error: E };\n\nexport function ok<T>(value: T): Result<T, never> {\n return { ok: true, value };\n}\n\nexport function err<E>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\nexport interface CLIOutput<T> {\n ok: boolean;\n data?: T;\n error?: { code: string; message: string };\n}\n\nexport type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;\n","/**\n * Value object representing a normalized git remote URL.\n * Accepts any common format (SSH or HTTPS, with or without .git suffix)\n * and stores the canonical `host/owner/repo` form.\n */\nexport class GitRemote {\n readonly value: string;\n\n private constructor(normalized: string) {\n this.value = normalized;\n }\n\n static parse(raw: string): GitRemote {\n return new GitRemote(GitRemote.normalize(raw));\n }\n\n private static normalize(url: string): string {\n const trimmed = url.trim();\n\n // SSH format: git@github.com:owner/repo.git or git@github.com:owner/repo\n const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\\.git)?$/);\n if (sshMatch) {\n const [, host, path] = sshMatch;\n return `${host}/${path}`.toLowerCase();\n }\n\n // HTTPS/HTTP format: https://github.com/owner/repo.git or without .git\n try {\n const parsed = new URL(trimmed);\n const path = parsed.pathname.replace(/\\.git$/, '').replace(/^\\//, '');\n return `${parsed.host}/${path}`.toLowerCase();\n } catch {\n return trimmed.toLowerCase();\n }\n }\n\n equals(other: GitRemote): boolean {\n return this.value === other.value;\n }\n\n toString(): string {\n return this.value;\n }\n}\n","/**\n * Compare two semver strings (major.minor.patch).\n * Returns true if `a` is strictly newer than `b`.\n */\nexport function isNewerVersion(a: string, b: string): boolean {\n const parse = (v: string): [number, number, number] => {\n const parts = v.replace(/^v/, '').split('.').map(Number);\n return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];\n };\n const [aMaj, aMin, aPatch] = parse(a);\n const [bMaj, bMin, bPatch] = parse(b);\n if (aMaj !== bMaj) return aMaj > bMaj;\n if (aMin !== bMin) return aMin > bMin;\n return aPatch > bPatch;\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ APP_VERSION,
3
4
  DependencyType,
4
5
  GitRemote,
5
6
  RANK_GAP,
@@ -12,11 +13,12 @@ import {
12
13
  detectGitRemote,
13
14
  err,
14
15
  getTaskLevel,
16
+ isNewerVersion,
15
17
  isTerminalStatus,
16
18
  logger,
17
19
  midpoint,
18
20
  ok
19
- } from "./chunk-5V4TBQ5S.js";
21
+ } from "./chunk-6ZNCVCAS.js";
20
22
 
21
23
  // src/config/index.ts
22
24
  import { mkdirSync } from "fs";
@@ -37,6 +39,7 @@ function loadConfig() {
37
39
  otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
38
40
  updateCachePath: join(dataDir, "update-check.json"),
39
41
  dismissedGitRemotesPath: join(dataDir, "dismissed-git-remotes.json"),
42
+ telemetryStatePath: join(dataDir, "telemetry.json"),
40
43
  noUpdateCheck: process.env["TAYTO_NO_UPDATE_CHECK"] === "1"
41
44
  };
42
45
  }
@@ -2023,17 +2026,6 @@ function isValidCache(value) {
2023
2026
  const obj = value;
2024
2027
  return typeof obj["checkedAt"] === "number" && typeof obj["latestVersion"] === "string";
2025
2028
  }
2026
- function isNewerVersion(a, b) {
2027
- const parse = (v) => {
2028
- const parts = v.replace(/^v/, "").split(".").map(Number);
2029
- return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
2030
- };
2031
- const [aMaj, aMin, aPatch] = parse(a);
2032
- const [bMaj, bMin, bPatch] = parse(b);
2033
- if (aMaj !== bMaj) return aMaj > bMaj;
2034
- if (aMin !== bMin) return aMin > bMin;
2035
- return aPatch > bPatch;
2036
- }
2037
2029
  function defaultExec(cmd, args) {
2038
2030
  execFileSync(cmd, args, { stdio: "inherit", timeout: 6e4 });
2039
2031
  }
@@ -2135,7 +2127,16 @@ var UpdateServiceImpl = class {
2135
2127
  }
2136
2128
  writeCache(entry) {
2137
2129
  try {
2138
- writeFileSync(this.cachePath, JSON.stringify(entry), "utf-8");
2130
+ let existing = {};
2131
+ try {
2132
+ const raw = readFileSync2(this.cachePath, "utf-8");
2133
+ const parsed = JSON.parse(raw);
2134
+ if (typeof parsed === "object" && parsed !== null) {
2135
+ existing = parsed;
2136
+ }
2137
+ } catch {
2138
+ }
2139
+ writeFileSync(this.cachePath, JSON.stringify({ ...existing, ...entry }), "utf-8");
2139
2140
  } catch (e) {
2140
2141
  logger.warn("Failed to write update cache", {
2141
2142
  error: toMessage(e)
@@ -2153,11 +2154,11 @@ function createContainer(db, dbPath, detectGitRemote2, updateCachePath, dismisse
2153
2154
  const dependencyService = new DependencyServiceImpl(depRepo, taskRepo);
2154
2155
  const taskService = new TaskServiceImpl(taskRepo, projectService, () => dependencyService);
2155
2156
  const portabilityService = new PortabilityServiceImpl(taskService, dependencyService);
2156
- const updateService = new UpdateServiceImpl(
2157
- updateCachePath ?? join3(tmpdir(), "tayto-update-check.json")
2158
- );
2157
+ const resolvedUpdateCachePath = updateCachePath ?? join3(tmpdir(), "tayto-update-check.json");
2158
+ const updateService = new UpdateServiceImpl(resolvedUpdateCachePath);
2159
2159
  return {
2160
2160
  dbPath,
2161
+ updateCachePath: resolvedUpdateCachePath,
2161
2162
  dismissedGitRemotesPath: dismissedGitRemotesPath ?? join3(tmpdir(), "tayto-dismissed-git-remotes.json"),
2162
2163
  projectService,
2163
2164
  taskService,
@@ -2170,9 +2171,6 @@ function createContainer(db, dbPath, detectGitRemote2, updateCachePath, dismisse
2170
2171
  // src/cli/index.ts
2171
2172
  import { Command } from "commander";
2172
2173
 
2173
- // src/version.ts
2174
- var APP_VERSION = true ? "0.7.0" : "0.0.0-dev";
2175
-
2176
2174
  // src/cli/output.ts
2177
2175
  function printSuccess(data) {
2178
2176
  process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n");
@@ -2597,12 +2595,145 @@ function buildCLI(container) {
2597
2595
  registerDepGraph(dep, container);
2598
2596
  registerUpgrade(program, container);
2599
2597
  program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
2600
- const { launchTUI } = await import("./tui-NCL4RFFD.js");
2598
+ const { launchTUI } = await import("./tui-APMSKU4E.js");
2601
2599
  await launchTUI(container, opts.project);
2602
2600
  });
2603
2601
  return program;
2604
2602
  }
2605
2603
 
2604
+ // src/telemetry/heartbeat.ts
2605
+ import { readFile, writeFile } from "fs/promises";
2606
+
2607
+ // src/types/install-id.ts
2608
+ import { ulid as ulid2 } from "ulid";
2609
+ var ULID_REGEX = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
2610
+ var InstallId = class _InstallId {
2611
+ value;
2612
+ constructor(id) {
2613
+ this.value = id;
2614
+ }
2615
+ static generate() {
2616
+ return new _InstallId(ulid2());
2617
+ }
2618
+ static parse(raw) {
2619
+ const trimmed = raw.trim();
2620
+ if (!ULID_REGEX.test(trimmed)) {
2621
+ throw new Error(`Invalid InstallId: expected ULID format, got "${trimmed}"`);
2622
+ }
2623
+ return new _InstallId(trimmed.toUpperCase());
2624
+ }
2625
+ toString() {
2626
+ return this.value;
2627
+ }
2628
+ };
2629
+
2630
+ // src/utils/ci.ts
2631
+ var CI_ENV_VARS = [
2632
+ "CI",
2633
+ "GITHUB_ACTIONS",
2634
+ "JENKINS_URL",
2635
+ "GITLAB_CI",
2636
+ "CIRCLECI",
2637
+ "BUILDKITE",
2638
+ "TF_BUILD",
2639
+ "CODEBUILD_BUILD_ID"
2640
+ ];
2641
+ function isCI(env = process.env) {
2642
+ return CI_ENV_VARS.some((key) => {
2643
+ const val = env[key];
2644
+ return val !== void 0 && val !== "";
2645
+ });
2646
+ }
2647
+
2648
+ // src/telemetry/heartbeat.ts
2649
+ var HEARTBEAT_URL = "https://tayto-telemetry.tomkapa.workers.dev/api/heartbeat";
2650
+ var TIMEOUT_MS = 3e3;
2651
+ function isValidRawState(value) {
2652
+ if (typeof value !== "object" || value === null) return false;
2653
+ const obj = value;
2654
+ return typeof obj["installId"] === "string" && typeof obj["lastPingDate"] === "string";
2655
+ }
2656
+ function todayUTC() {
2657
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2658
+ }
2659
+ async function readState(statePath) {
2660
+ let raw;
2661
+ try {
2662
+ raw = await readFile(statePath, "utf-8");
2663
+ } catch (e) {
2664
+ if (e.code !== "ENOENT") {
2665
+ logger.warn("telemetry: failed to read state", { error: toMessage(e), path: statePath });
2666
+ }
2667
+ return null;
2668
+ }
2669
+ try {
2670
+ const parsed = JSON.parse(raw);
2671
+ if (!isValidRawState(parsed)) return null;
2672
+ return { installId: InstallId.parse(parsed.installId), lastPingDate: parsed.lastPingDate };
2673
+ } catch (e) {
2674
+ logger.warn("telemetry: corrupt state file, will regenerate", {
2675
+ error: toMessage(e),
2676
+ path: statePath
2677
+ });
2678
+ return null;
2679
+ }
2680
+ }
2681
+ async function writeState(statePath, state) {
2682
+ try {
2683
+ await writeFile(statePath, JSON.stringify(state), "utf-8");
2684
+ } catch (e) {
2685
+ logger.warn("telemetry: failed to write state", { error: toMessage(e), path: statePath });
2686
+ }
2687
+ }
2688
+ function maybeSendHeartbeat(deps) {
2689
+ const { statePath, version, fetchImpl = globalThis.fetch, env = process.env } = deps;
2690
+ void (async () => {
2691
+ try {
2692
+ if (env["TASKCLI_TELEMETRY_DISABLED"] === "1") {
2693
+ logger.info("telemetry: disabled via TASKCLI_TELEMETRY_DISABLED");
2694
+ return;
2695
+ }
2696
+ if (isCI(env)) {
2697
+ logger.info("telemetry: skipped in CI environment");
2698
+ return;
2699
+ }
2700
+ const today = todayUTC();
2701
+ const state = await readState(statePath);
2702
+ if (state && state.lastPingDate === today) {
2703
+ return;
2704
+ }
2705
+ const installId = state ? state.installId : InstallId.generate();
2706
+ await writeState(statePath, { installId: installId.toString(), lastPingDate: today });
2707
+ const controller = new AbortController();
2708
+ const timeout = setTimeout(() => {
2709
+ controller.abort();
2710
+ }, TIMEOUT_MS);
2711
+ timeout.unref();
2712
+ const payload = {
2713
+ installId: installId.toString(),
2714
+ version,
2715
+ os: process.platform,
2716
+ nodeVersion: process.version,
2717
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2718
+ };
2719
+ fetchImpl(HEARTBEAT_URL, {
2720
+ method: "POST",
2721
+ headers: { "Content-Type": "application/json" },
2722
+ body: JSON.stringify(payload),
2723
+ signal: controller.signal
2724
+ }).then(() => {
2725
+ clearTimeout(timeout);
2726
+ logger.info("telemetry: heartbeat sent", { installId: installId.toString() });
2727
+ }).catch((e) => {
2728
+ clearTimeout(timeout);
2729
+ logger.info("telemetry: heartbeat failed (non-blocking)", { error: toMessage(e) });
2730
+ });
2731
+ } catch (e) {
2732
+ logger.warn("telemetry: unexpected error in maybeSendHeartbeat", { error: toMessage(e) });
2733
+ }
2734
+ })();
2735
+ }
2736
+
2606
2737
  // src/index.ts
2607
2738
  async function checkForUpdateQuietly(container, currentVersion) {
2608
2739
  let timerId;
@@ -2629,9 +2760,16 @@ async function main() {
2629
2760
  const config = loadConfig();
2630
2761
  logger.init(config.logDir);
2631
2762
  initTelemetry(config);
2763
+ maybeSendHeartbeat({ statePath: config.telemetryStatePath, version: APP_VERSION });
2632
2764
  const db = createDatabase(config.dbPath);
2633
2765
  runMigrations(db);
2634
- const container = createContainer(db, config.dbPath, void 0, config.updateCachePath, config.dismissedGitRemotesPath);
2766
+ const container = createContainer(
2767
+ db,
2768
+ config.dbPath,
2769
+ void 0,
2770
+ config.updateCachePath,
2771
+ config.dismissedGitRemotesPath
2772
+ );
2635
2773
  const args = process.argv.slice(2);
2636
2774
  const isUpgradeCommand = args[0] === "upgrade";
2637
2775
  let updateCheck = null;
@@ -2639,7 +2777,7 @@ async function main() {
2639
2777
  updateCheck = await checkForUpdateQuietly(container, APP_VERSION);
2640
2778
  }
2641
2779
  if (args.length === 0) {
2642
- const { launchTUI } = await import("./tui-NCL4RFFD.js");
2780
+ const { launchTUI } = await import("./tui-APMSKU4E.js");
2643
2781
  await launchTUI(
2644
2782
  container,
2645
2783
  void 0,