@tomkapa/tayto 0.6.0 → 0.7.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.
@@ -154,6 +154,38 @@ function err(error) {
154
154
  return { ok: false, error };
155
155
  }
156
156
 
157
+ // src/types/git-remote.ts
158
+ var GitRemote = class _GitRemote {
159
+ value;
160
+ constructor(normalized) {
161
+ this.value = normalized;
162
+ }
163
+ static parse(raw) {
164
+ return new _GitRemote(_GitRemote.normalize(raw));
165
+ }
166
+ static normalize(url) {
167
+ const trimmed = url.trim();
168
+ const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
169
+ if (sshMatch) {
170
+ const [, host, path] = sshMatch;
171
+ return `${host}/${path}`.toLowerCase();
172
+ }
173
+ try {
174
+ const parsed = new URL(trimmed);
175
+ const path = parsed.pathname.replace(/\.git$/, "").replace(/^\//, "");
176
+ return `${parsed.host}/${path}`.toLowerCase();
177
+ } catch {
178
+ return trimmed.toLowerCase();
179
+ }
180
+ }
181
+ equals(other) {
182
+ return this.value === other.value;
183
+ }
184
+ toString() {
185
+ return this.value;
186
+ }
187
+ };
188
+
157
189
  // src/utils/git.ts
158
190
  function detectGitRemote(cwd) {
159
191
  try {
@@ -166,12 +198,13 @@ function detectGitRemote(cwd) {
166
198
  logger.info("detectGitRemote: no git remote found (non-zero exit or error)");
167
199
  return ok(null);
168
200
  }
169
- const remote = result.stdout.trim();
170
- if (!remote) {
201
+ const raw = result.stdout.trim();
202
+ if (!raw) {
171
203
  logger.info("detectGitRemote: empty stdout from git remote get-url");
172
204
  return ok(null);
173
205
  }
174
- logger.info(`detectGitRemote: found remote=${remote}`);
206
+ const remote = GitRemote.parse(raw);
207
+ logger.info(`detectGitRemote: found remote=${remote.value}`);
175
208
  return ok(remote);
176
209
  } catch (e) {
177
210
  logger.info(`detectGitRemote: exception during git detection: ${String(e)}`);
@@ -183,6 +216,7 @@ export {
183
216
  logger,
184
217
  ok,
185
218
  err,
219
+ GitRemote,
186
220
  TaskStatus,
187
221
  TaskType,
188
222
  TaskLevel,
@@ -196,4 +230,4 @@ export {
196
230
  isTerminalStatus,
197
231
  detectGitRemote
198
232
  };
199
- //# sourceMappingURL=chunk-74Q55TOV.js.map
233
+ //# sourceMappingURL=chunk-5V4TBQ5S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/logging/logger.ts","../src/types/enums.ts","../src/utils/git.ts","../src/types/common.ts","../src/types/git-remote.ts"],"sourcesContent":["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","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","/**\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"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DependencyType,
4
+ GitRemote,
4
5
  RANK_GAP,
5
6
  TERMINAL_STATUSES,
6
7
  TaskLevel,
@@ -15,7 +16,7 @@ import {
15
16
  logger,
16
17
  midpoint,
17
18
  ok
18
- } from "./chunk-74Q55TOV.js";
19
+ } from "./chunk-5V4TBQ5S.js";
19
20
 
20
21
  // src/config/index.ts
21
22
  import { mkdirSync } from "fs";
@@ -35,6 +36,7 @@ function loadConfig() {
35
36
  logLevel: process.env["TASK_LOG_LEVEL"] ?? "info",
36
37
  otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
37
38
  updateCachePath: join(dataDir, "update-check.json"),
39
+ dismissedGitRemotesPath: join(dataDir, "dismissed-git-remotes.json"),
38
40
  noUpdateCheck: process.env["TAYTO_NO_UPDATE_CHECK"] === "1"
39
41
  };
40
42
  }
@@ -160,7 +162,7 @@ function rowToProject(row) {
160
162
  name: row.name,
161
163
  description: row.description,
162
164
  isDefault: row.is_default === 1,
163
- gitRemote: row.git_remote,
165
+ gitRemote: row.git_remote ? GitRemote.parse(row.git_remote) : null,
164
166
  createdAt: row.created_at,
165
167
  updatedAt: row.updated_at
166
168
  };
@@ -187,7 +189,7 @@ var SqliteProjectRepository = class {
187
189
  input.name,
188
190
  input.description ?? "",
189
191
  input.isDefault ? 1 : 0,
190
- input.gitRemote ?? null,
192
+ input.gitRemote?.value ?? null,
191
193
  now,
192
194
  now
193
195
  );
@@ -202,7 +204,7 @@ var SqliteProjectRepository = class {
202
204
  return err(
203
205
  new AppError(
204
206
  "DUPLICATE",
205
- `Git remote already linked to another project: ${input.gitRemote}`,
207
+ `Git remote already linked to another project: ${input.gitRemote?.value}`,
206
208
  e
207
209
  )
208
210
  );
@@ -239,7 +241,7 @@ var SqliteProjectRepository = class {
239
241
  }
240
242
  findByGitRemote(remote) {
241
243
  try {
242
- const row = this.db.prepare(`SELECT * FROM projects WHERE git_remote = ? AND ${NOT_DELETED}`).get(remote);
244
+ const row = this.db.prepare(`SELECT * FROM projects WHERE git_remote = ? AND ${NOT_DELETED}`).get(remote.value);
243
245
  return ok(row ? rowToProject(row) : null);
244
246
  } catch (e) {
245
247
  return err(new AppError("DB_ERROR", "Failed to find project by git remote", e));
@@ -280,7 +282,7 @@ var SqliteProjectRepository = class {
280
282
  input.name ?? existing.name,
281
283
  input.description ?? existing.description,
282
284
  input.isDefault !== void 0 ? input.isDefault ? 1 : 0 : existing.is_default,
283
- input.gitRemote !== void 0 ? input.gitRemote : existing.git_remote,
285
+ input.gitRemote !== void 0 ? input.gitRemote?.value ?? null : existing.git_remote,
284
286
  now,
285
287
  id
286
288
  );
@@ -967,18 +969,19 @@ var SqliteDependencyRepository = class {
967
969
 
968
970
  // src/types/project.ts
969
971
  import { z } from "zod/v4";
972
+ var gitRemoteField = z.string().min(1, "Git remote URL must not be empty").transform((v) => GitRemote.parse(v)).nullable().optional();
970
973
  var CreateProjectSchema = z.object({
971
974
  name: z.string().min(1, "Project name is required").max(255),
972
975
  key: z.string().min(2, "Project key must be at least 2 characters").max(7, "Project key must be at most 7 characters").regex(/^[A-Za-z0-9]+$/, "Project key must contain only letters and digits").transform((v) => v.toUpperCase()).optional(),
973
976
  description: z.string().max(5e3).optional(),
974
977
  isDefault: z.boolean().optional(),
975
- gitRemote: z.string().min(1, "Git remote URL must not be empty").nullable().optional()
978
+ gitRemote: gitRemoteField
976
979
  });
977
980
  var UpdateProjectSchema = z.object({
978
981
  name: z.string().min(1).max(255).optional(),
979
982
  description: z.string().max(5e3).optional(),
980
983
  isDefault: z.boolean().optional(),
981
- gitRemote: z.string().min(1, "Git remote URL must not be empty").nullable().optional()
984
+ gitRemote: gitRemoteField
982
985
  });
983
986
 
984
987
  // src/service/project.service.ts
@@ -1083,8 +1086,10 @@ var ProjectServiceImpl = class {
1083
1086
  return logger.startSpan("ProjectService.linkGitRemote", () => {
1084
1087
  const resolved = this.resolveProject(idOrName);
1085
1088
  if (!resolved.ok) return resolved;
1086
- let url = remote;
1087
- if (!url) {
1089
+ let gitRemote;
1090
+ if (remote) {
1091
+ gitRemote = GitRemote.parse(remote);
1092
+ } else {
1088
1093
  const detected = this.detectRemote();
1089
1094
  if (!detected.ok) return detected;
1090
1095
  if (!detected.value) {
@@ -1095,9 +1100,9 @@ var ProjectServiceImpl = class {
1095
1100
  )
1096
1101
  );
1097
1102
  }
1098
- url = detected.value;
1103
+ gitRemote = detected.value;
1099
1104
  }
1100
- return this.repo.update(resolved.value.id, { gitRemote: url });
1105
+ return this.repo.update(resolved.value.id, { gitRemote });
1101
1106
  });
1102
1107
  }
1103
1108
  unlinkGitRemote(idOrName) {
@@ -2140,7 +2145,7 @@ var UpdateServiceImpl = class {
2140
2145
  };
2141
2146
 
2142
2147
  // src/cli/container.ts
2143
- function createContainer(db, dbPath, detectGitRemote2, updateCachePath) {
2148
+ function createContainer(db, dbPath, detectGitRemote2, updateCachePath, dismissedGitRemotesPath) {
2144
2149
  const projectRepo = new SqliteProjectRepository(db);
2145
2150
  const taskRepo = new SqliteTaskRepository(db);
2146
2151
  const depRepo = new SqliteDependencyRepository(db);
@@ -2153,6 +2158,7 @@ function createContainer(db, dbPath, detectGitRemote2, updateCachePath) {
2153
2158
  );
2154
2159
  return {
2155
2160
  dbPath,
2161
+ dismissedGitRemotesPath: dismissedGitRemotesPath ?? join3(tmpdir(), "tayto-dismissed-git-remotes.json"),
2156
2162
  projectService,
2157
2163
  taskService,
2158
2164
  dependencyService,
@@ -2165,7 +2171,7 @@ function createContainer(db, dbPath, detectGitRemote2, updateCachePath) {
2165
2171
  import { Command } from "commander";
2166
2172
 
2167
2173
  // src/version.ts
2168
- var APP_VERSION = true ? "0.6.0" : "0.0.0-dev";
2174
+ var APP_VERSION = true ? "0.7.0" : "0.0.0-dev";
2169
2175
 
2170
2176
  // src/cli/output.ts
2171
2177
  function printSuccess(data) {
@@ -2214,18 +2220,20 @@ function registerProjectList(parent, container) {
2214
2220
 
2215
2221
  // src/cli/commands/project/update.ts
2216
2222
  function registerProjectUpdate(parent, container) {
2217
- parent.command("update <idOrKeyOrName>").description("Update a project (lookup by id, key, or name)").option("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("--default", "Set as default project").action(
2223
+ parent.command("update <idOrKeyOrName>").description("Update a project (lookup by id, key, or name)").option("-n, --name <name>", "Project name").option("-d, --description <description>", "Project description").option("--default", "Set as default project").option("--git-remote <url>", "Git remote URL (use --no-git-remote to unlink)").option("--no-git-remote", "Unlink git remote").action(
2218
2224
  (idOrKeyOrName, opts) => {
2219
2225
  const resolved = container.projectService.resolveProject(idOrKeyOrName);
2220
2226
  if (!resolved.ok) {
2221
2227
  handleResult(resolved);
2222
2228
  return;
2223
2229
  }
2224
- const result = container.projectService.updateProject(resolved.value.id, {
2230
+ const updateInput = {
2225
2231
  name: opts.name,
2226
2232
  description: opts.description,
2227
- isDefault: opts.default
2228
- });
2233
+ isDefault: opts.default,
2234
+ ...opts.gitRemote === false ? { gitRemote: null } : typeof opts.gitRemote === "string" ? { gitRemote: opts.gitRemote } : {}
2235
+ };
2236
+ const result = container.projectService.updateProject(resolved.value.id, updateInput);
2229
2237
  handleResult(result);
2230
2238
  }
2231
2239
  );
@@ -2589,7 +2597,7 @@ function buildCLI(container) {
2589
2597
  registerDepGraph(dep, container);
2590
2598
  registerUpgrade(program, container);
2591
2599
  program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
2592
- const { launchTUI } = await import("./tui-WMESKCRD.js");
2600
+ const { launchTUI } = await import("./tui-NCL4RFFD.js");
2593
2601
  await launchTUI(container, opts.project);
2594
2602
  });
2595
2603
  return program;
@@ -2623,7 +2631,7 @@ async function main() {
2623
2631
  initTelemetry(config);
2624
2632
  const db = createDatabase(config.dbPath);
2625
2633
  runMigrations(db);
2626
- const container = createContainer(db, config.dbPath, void 0, config.updateCachePath);
2634
+ const container = createContainer(db, config.dbPath, void 0, config.updateCachePath, config.dismissedGitRemotesPath);
2627
2635
  const args = process.argv.slice(2);
2628
2636
  const isUpgradeCommand = args[0] === "upgrade";
2629
2637
  let updateCheck = null;
@@ -2631,7 +2639,7 @@ async function main() {
2631
2639
  updateCheck = await checkForUpdateQuietly(container, APP_VERSION);
2632
2640
  }
2633
2641
  if (args.length === 0) {
2634
- const { launchTUI } = await import("./tui-WMESKCRD.js");
2642
+ const { launchTUI } = await import("./tui-NCL4RFFD.js");
2635
2643
  await launchTUI(
2636
2644
  container,
2637
2645
  void 0,