@tomkapa/tayto 0.5.4 → 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.
- package/dist/{chunk-74Q55TOV.js → chunk-5V4TBQ5S.js} +38 -4
- package/dist/chunk-5V4TBQ5S.js.map +1 -0
- package/dist/index.js +262 -30
- package/dist/index.js.map +1 -1
- package/dist/{tui-24ZW56Q6.js → tui-NCL4RFFD.js} +579 -215
- package/dist/tui-NCL4RFFD.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-74Q55TOV.js.map +0 -1
- package/dist/tui-24ZW56Q6.js.map +0 -1
|
@@ -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
|
|
170
|
-
if (!
|
|
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
|
-
|
|
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-
|
|
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-
|
|
19
|
+
} from "./chunk-5V4TBQ5S.js";
|
|
19
20
|
|
|
20
21
|
// src/config/index.ts
|
|
21
22
|
import { mkdirSync } from "fs";
|
|
@@ -33,7 +34,10 @@ function loadConfig() {
|
|
|
33
34
|
dbPath: process.env["TASK_DB_PATH"] ?? join(dataDir, "data.db"),
|
|
34
35
|
logDir,
|
|
35
36
|
logLevel: process.env["TASK_LOG_LEVEL"] ?? "info",
|
|
36
|
-
otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"]
|
|
37
|
+
otelEndpoint: process.env["OTEL_EXPORTER_OTLP_ENDPOINT"],
|
|
38
|
+
updateCachePath: join(dataDir, "update-check.json"),
|
|
39
|
+
dismissedGitRemotesPath: join(dataDir, "dismissed-git-remotes.json"),
|
|
40
|
+
noUpdateCheck: process.env["TAYTO_NO_UPDATE_CHECK"] === "1"
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -109,6 +113,10 @@ async function shutdownTelemetry() {
|
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
// src/cli/container.ts
|
|
117
|
+
import { join as join3 } from "path";
|
|
118
|
+
import { tmpdir } from "os";
|
|
119
|
+
|
|
112
120
|
// src/errors/app-error.ts
|
|
113
121
|
var AppError = class extends Error {
|
|
114
122
|
constructor(code, message, cause) {
|
|
@@ -120,6 +128,9 @@ var AppError = class extends Error {
|
|
|
120
128
|
code;
|
|
121
129
|
cause;
|
|
122
130
|
};
|
|
131
|
+
function toMessage(e) {
|
|
132
|
+
return e instanceof Error ? e.message : String(e);
|
|
133
|
+
}
|
|
123
134
|
|
|
124
135
|
// src/repository/project.repository.ts
|
|
125
136
|
import { ulid } from "ulid";
|
|
@@ -151,7 +162,7 @@ function rowToProject(row) {
|
|
|
151
162
|
name: row.name,
|
|
152
163
|
description: row.description,
|
|
153
164
|
isDefault: row.is_default === 1,
|
|
154
|
-
gitRemote: row.git_remote,
|
|
165
|
+
gitRemote: row.git_remote ? GitRemote.parse(row.git_remote) : null,
|
|
155
166
|
createdAt: row.created_at,
|
|
156
167
|
updatedAt: row.updated_at
|
|
157
168
|
};
|
|
@@ -178,7 +189,7 @@ var SqliteProjectRepository = class {
|
|
|
178
189
|
input.name,
|
|
179
190
|
input.description ?? "",
|
|
180
191
|
input.isDefault ? 1 : 0,
|
|
181
|
-
input.gitRemote ?? null,
|
|
192
|
+
input.gitRemote?.value ?? null,
|
|
182
193
|
now,
|
|
183
194
|
now
|
|
184
195
|
);
|
|
@@ -193,7 +204,7 @@ var SqliteProjectRepository = class {
|
|
|
193
204
|
return err(
|
|
194
205
|
new AppError(
|
|
195
206
|
"DUPLICATE",
|
|
196
|
-
`Git remote already linked to another project: ${input.gitRemote}`,
|
|
207
|
+
`Git remote already linked to another project: ${input.gitRemote?.value}`,
|
|
197
208
|
e
|
|
198
209
|
)
|
|
199
210
|
);
|
|
@@ -230,7 +241,7 @@ var SqliteProjectRepository = class {
|
|
|
230
241
|
}
|
|
231
242
|
findByGitRemote(remote) {
|
|
232
243
|
try {
|
|
233
|
-
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);
|
|
234
245
|
return ok(row ? rowToProject(row) : null);
|
|
235
246
|
} catch (e) {
|
|
236
247
|
return err(new AppError("DB_ERROR", "Failed to find project by git remote", e));
|
|
@@ -271,7 +282,7 @@ var SqliteProjectRepository = class {
|
|
|
271
282
|
input.name ?? existing.name,
|
|
272
283
|
input.description ?? existing.description,
|
|
273
284
|
input.isDefault !== void 0 ? input.isDefault ? 1 : 0 : existing.is_default,
|
|
274
|
-
input.gitRemote !== void 0 ? input.gitRemote : existing.git_remote,
|
|
285
|
+
input.gitRemote !== void 0 ? input.gitRemote?.value ?? null : existing.git_remote,
|
|
275
286
|
now,
|
|
276
287
|
id
|
|
277
288
|
);
|
|
@@ -958,18 +969,19 @@ var SqliteDependencyRepository = class {
|
|
|
958
969
|
|
|
959
970
|
// src/types/project.ts
|
|
960
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();
|
|
961
973
|
var CreateProjectSchema = z.object({
|
|
962
974
|
name: z.string().min(1, "Project name is required").max(255),
|
|
963
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(),
|
|
964
976
|
description: z.string().max(5e3).optional(),
|
|
965
977
|
isDefault: z.boolean().optional(),
|
|
966
|
-
gitRemote:
|
|
978
|
+
gitRemote: gitRemoteField
|
|
967
979
|
});
|
|
968
980
|
var UpdateProjectSchema = z.object({
|
|
969
981
|
name: z.string().min(1).max(255).optional(),
|
|
970
982
|
description: z.string().max(5e3).optional(),
|
|
971
983
|
isDefault: z.boolean().optional(),
|
|
972
|
-
gitRemote:
|
|
984
|
+
gitRemote: gitRemoteField
|
|
973
985
|
});
|
|
974
986
|
|
|
975
987
|
// src/service/project.service.ts
|
|
@@ -1074,8 +1086,10 @@ var ProjectServiceImpl = class {
|
|
|
1074
1086
|
return logger.startSpan("ProjectService.linkGitRemote", () => {
|
|
1075
1087
|
const resolved = this.resolveProject(idOrName);
|
|
1076
1088
|
if (!resolved.ok) return resolved;
|
|
1077
|
-
let
|
|
1078
|
-
if (
|
|
1089
|
+
let gitRemote;
|
|
1090
|
+
if (remote) {
|
|
1091
|
+
gitRemote = GitRemote.parse(remote);
|
|
1092
|
+
} else {
|
|
1079
1093
|
const detected = this.detectRemote();
|
|
1080
1094
|
if (!detected.ok) return detected;
|
|
1081
1095
|
if (!detected.value) {
|
|
@@ -1086,9 +1100,9 @@ var ProjectServiceImpl = class {
|
|
|
1086
1100
|
)
|
|
1087
1101
|
);
|
|
1088
1102
|
}
|
|
1089
|
-
|
|
1103
|
+
gitRemote = detected.value;
|
|
1090
1104
|
}
|
|
1091
|
-
return this.repo.update(resolved.value.id, { gitRemote
|
|
1105
|
+
return this.repo.update(resolved.value.id, { gitRemote });
|
|
1092
1106
|
});
|
|
1093
1107
|
}
|
|
1094
1108
|
unlinkGitRemote(idOrName) {
|
|
@@ -1997,8 +2011,141 @@ var PortabilityServiceImpl = class {
|
|
|
1997
2011
|
}
|
|
1998
2012
|
};
|
|
1999
2013
|
|
|
2014
|
+
// src/service/update.service.ts
|
|
2015
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
2016
|
+
import { execFileSync } from "child_process";
|
|
2017
|
+
import { trace } from "@opentelemetry/api";
|
|
2018
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org/@tomkapa/tayto/latest";
|
|
2019
|
+
var PACKAGE_NAME = "@tomkapa/tayto";
|
|
2020
|
+
var CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2021
|
+
function isValidCache(value) {
|
|
2022
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2023
|
+
const obj = value;
|
|
2024
|
+
return typeof obj["checkedAt"] === "number" && typeof obj["latestVersion"] === "string";
|
|
2025
|
+
}
|
|
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
|
+
function defaultExec(cmd, args) {
|
|
2038
|
+
execFileSync(cmd, args, { stdio: "inherit", timeout: 6e4 });
|
|
2039
|
+
}
|
|
2040
|
+
var tracer = trace.getTracer("task");
|
|
2041
|
+
var UpdateServiceImpl = class {
|
|
2042
|
+
constructor(cachePath, fetchImpl = globalThis.fetch, execImpl = defaultExec) {
|
|
2043
|
+
this.cachePath = cachePath;
|
|
2044
|
+
this.fetchImpl = fetchImpl;
|
|
2045
|
+
this.execImpl = execImpl;
|
|
2046
|
+
}
|
|
2047
|
+
cachePath;
|
|
2048
|
+
fetchImpl;
|
|
2049
|
+
execImpl;
|
|
2050
|
+
async checkForUpdate(currentVersion) {
|
|
2051
|
+
return tracer.startActiveSpan("UpdateService.checkForUpdate", async (span) => {
|
|
2052
|
+
try {
|
|
2053
|
+
span.setAttribute("update.current_version", currentVersion);
|
|
2054
|
+
const cached = this.readCache();
|
|
2055
|
+
if (cached && Date.now() - cached.checkedAt < CHECK_TTL_MS) {
|
|
2056
|
+
span.setAttribute("update.cache_hit", true);
|
|
2057
|
+
const updateAvailable2 = isNewerVersion(cached.latestVersion, currentVersion);
|
|
2058
|
+
span.setAttribute("update.latest_version", cached.latestVersion);
|
|
2059
|
+
span.setAttribute("update.available", updateAvailable2);
|
|
2060
|
+
return ok({ currentVersion, latestVersion: cached.latestVersion, updateAvailable: updateAvailable2 });
|
|
2061
|
+
}
|
|
2062
|
+
span.setAttribute("update.cache_hit", false);
|
|
2063
|
+
let latestVersion;
|
|
2064
|
+
const controller = new AbortController();
|
|
2065
|
+
const timeout = setTimeout(() => {
|
|
2066
|
+
controller.abort();
|
|
2067
|
+
}, 5e3);
|
|
2068
|
+
try {
|
|
2069
|
+
const response = await this.fetchImpl(NPM_REGISTRY_URL, { signal: controller.signal });
|
|
2070
|
+
if (!response.ok) {
|
|
2071
|
+
logger.warn("Update check: registry returned non-OK status", {
|
|
2072
|
+
status: response.status
|
|
2073
|
+
});
|
|
2074
|
+
return err(
|
|
2075
|
+
new AppError(
|
|
2076
|
+
"UPGRADE_CHECK",
|
|
2077
|
+
`npm registry returned status ${String(response.status)}`
|
|
2078
|
+
)
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
const body = await response.json();
|
|
2082
|
+
if (typeof body["version"] !== "string") {
|
|
2083
|
+
return err(
|
|
2084
|
+
new AppError("UPGRADE_CHECK", "Unexpected response format from npm registry")
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
latestVersion = body["version"];
|
|
2088
|
+
} catch (e) {
|
|
2089
|
+
logger.warn("Update check: fetch failed", { error: toMessage(e) });
|
|
2090
|
+
return err(
|
|
2091
|
+
new AppError("UPGRADE_CHECK", `Failed to check for updates: ${toMessage(e)}`, e)
|
|
2092
|
+
);
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(timeout);
|
|
2095
|
+
}
|
|
2096
|
+
this.writeCache({ checkedAt: Date.now(), latestVersion });
|
|
2097
|
+
const updateAvailable = isNewerVersion(latestVersion, currentVersion);
|
|
2098
|
+
span.setAttribute("update.latest_version", latestVersion);
|
|
2099
|
+
span.setAttribute("update.available", updateAvailable);
|
|
2100
|
+
return ok({ currentVersion, latestVersion, updateAvailable });
|
|
2101
|
+
} finally {
|
|
2102
|
+
span.end();
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
performUpgrade(currentVersion) {
|
|
2107
|
+
return logger.startSpan("UpdateService.performUpgrade", (span) => {
|
|
2108
|
+
span.setAttribute("update.previous_version", currentVersion);
|
|
2109
|
+
try {
|
|
2110
|
+
this.execImpl("npm", ["install", "-g", `${PACKAGE_NAME}@latest`]);
|
|
2111
|
+
} catch (e) {
|
|
2112
|
+
logger.error("Upgrade failed", e, { command: `npm install -g ${PACKAGE_NAME}@latest` });
|
|
2113
|
+
span.setAttribute("update.success", false);
|
|
2114
|
+
return err(new AppError("UPGRADE_CHECK", `Upgrade failed: ${toMessage(e)}`, e));
|
|
2115
|
+
}
|
|
2116
|
+
const cached = this.readCache();
|
|
2117
|
+
const installedVersion = cached?.latestVersion ?? "unknown";
|
|
2118
|
+
span.setAttribute("update.success", true);
|
|
2119
|
+
span.setAttribute("update.installed_version", installedVersion);
|
|
2120
|
+
this.writeCache({ checkedAt: 0, latestVersion: installedVersion });
|
|
2121
|
+
return ok({ installedVersion });
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
readCache() {
|
|
2125
|
+
try {
|
|
2126
|
+
const raw = readFileSync2(this.cachePath, "utf-8");
|
|
2127
|
+
const parsed = JSON.parse(raw);
|
|
2128
|
+
return isValidCache(parsed) ? parsed : null;
|
|
2129
|
+
} catch (e) {
|
|
2130
|
+
logger.info("Update cache not available", {
|
|
2131
|
+
error: toMessage(e)
|
|
2132
|
+
});
|
|
2133
|
+
return null;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
writeCache(entry) {
|
|
2137
|
+
try {
|
|
2138
|
+
writeFileSync(this.cachePath, JSON.stringify(entry), "utf-8");
|
|
2139
|
+
} catch (e) {
|
|
2140
|
+
logger.warn("Failed to write update cache", {
|
|
2141
|
+
error: toMessage(e)
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
|
|
2000
2147
|
// src/cli/container.ts
|
|
2001
|
-
function createContainer(db, dbPath, detectGitRemote2) {
|
|
2148
|
+
function createContainer(db, dbPath, detectGitRemote2, updateCachePath, dismissedGitRemotesPath) {
|
|
2002
2149
|
const projectRepo = new SqliteProjectRepository(db);
|
|
2003
2150
|
const taskRepo = new SqliteTaskRepository(db);
|
|
2004
2151
|
const depRepo = new SqliteDependencyRepository(db);
|
|
@@ -2006,12 +2153,26 @@ function createContainer(db, dbPath, detectGitRemote2) {
|
|
|
2006
2153
|
const dependencyService = new DependencyServiceImpl(depRepo, taskRepo);
|
|
2007
2154
|
const taskService = new TaskServiceImpl(taskRepo, projectService, () => dependencyService);
|
|
2008
2155
|
const portabilityService = new PortabilityServiceImpl(taskService, dependencyService);
|
|
2009
|
-
|
|
2156
|
+
const updateService = new UpdateServiceImpl(
|
|
2157
|
+
updateCachePath ?? join3(tmpdir(), "tayto-update-check.json")
|
|
2158
|
+
);
|
|
2159
|
+
return {
|
|
2160
|
+
dbPath,
|
|
2161
|
+
dismissedGitRemotesPath: dismissedGitRemotesPath ?? join3(tmpdir(), "tayto-dismissed-git-remotes.json"),
|
|
2162
|
+
projectService,
|
|
2163
|
+
taskService,
|
|
2164
|
+
dependencyService,
|
|
2165
|
+
portabilityService,
|
|
2166
|
+
updateService
|
|
2167
|
+
};
|
|
2010
2168
|
}
|
|
2011
2169
|
|
|
2012
2170
|
// src/cli/index.ts
|
|
2013
2171
|
import { Command } from "commander";
|
|
2014
2172
|
|
|
2173
|
+
// src/version.ts
|
|
2174
|
+
var APP_VERSION = true ? "0.7.0" : "0.0.0-dev";
|
|
2175
|
+
|
|
2015
2176
|
// src/cli/output.ts
|
|
2016
2177
|
function printSuccess(data) {
|
|
2017
2178
|
process.stdout.write(JSON.stringify({ ok: true, data }, null, 2) + "\n");
|
|
@@ -2059,18 +2220,20 @@ function registerProjectList(parent, container) {
|
|
|
2059
2220
|
|
|
2060
2221
|
// src/cli/commands/project/update.ts
|
|
2061
2222
|
function registerProjectUpdate(parent, container) {
|
|
2062
|
-
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(
|
|
2063
2224
|
(idOrKeyOrName, opts) => {
|
|
2064
2225
|
const resolved = container.projectService.resolveProject(idOrKeyOrName);
|
|
2065
2226
|
if (!resolved.ok) {
|
|
2066
2227
|
handleResult(resolved);
|
|
2067
2228
|
return;
|
|
2068
2229
|
}
|
|
2069
|
-
const
|
|
2230
|
+
const updateInput = {
|
|
2070
2231
|
name: opts.name,
|
|
2071
2232
|
description: opts.description,
|
|
2072
|
-
isDefault: opts.default
|
|
2073
|
-
|
|
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);
|
|
2074
2237
|
handleResult(result);
|
|
2075
2238
|
}
|
|
2076
2239
|
);
|
|
@@ -2204,12 +2367,12 @@ function registerTaskDelete(parent, container) {
|
|
|
2204
2367
|
}
|
|
2205
2368
|
|
|
2206
2369
|
// src/cli/commands/task/breakdown.ts
|
|
2207
|
-
import { readFileSync as
|
|
2370
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2208
2371
|
function registerTaskBreakdown(parent, container) {
|
|
2209
2372
|
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
2373
|
let content;
|
|
2211
2374
|
try {
|
|
2212
|
-
content =
|
|
2375
|
+
content = readFileSync3(opts.file, "utf-8");
|
|
2213
2376
|
} catch (e) {
|
|
2214
2377
|
return printError(
|
|
2215
2378
|
new AppError("VALIDATION", `Failed to read subtasks file: ${opts.file}`, e)
|
|
@@ -2263,7 +2426,7 @@ function registerTaskSearch(parent, container) {
|
|
|
2263
2426
|
}
|
|
2264
2427
|
|
|
2265
2428
|
// src/cli/commands/task/export.ts
|
|
2266
|
-
import { writeFileSync } from "fs";
|
|
2429
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2267
2430
|
function registerTaskExport(parent, container) {
|
|
2268
2431
|
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
2432
|
const projectResult = withProject(container, opts.project);
|
|
@@ -2274,7 +2437,7 @@ function registerTaskExport(parent, container) {
|
|
|
2274
2437
|
}
|
|
2275
2438
|
if (opts.output) {
|
|
2276
2439
|
try {
|
|
2277
|
-
|
|
2440
|
+
writeFileSync2(opts.output, JSON.stringify(result.value, null, 2) + "\n", "utf-8");
|
|
2278
2441
|
} catch (e) {
|
|
2279
2442
|
return printError(new AppError("UNKNOWN", `Failed to write file: ${opts.output}`, e));
|
|
2280
2443
|
}
|
|
@@ -2290,7 +2453,7 @@ function registerTaskExport(parent, container) {
|
|
|
2290
2453
|
}
|
|
2291
2454
|
|
|
2292
2455
|
// src/cli/commands/task/import.ts
|
|
2293
|
-
import { readFileSync as
|
|
2456
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2294
2457
|
function registerTaskImport(parent, container) {
|
|
2295
2458
|
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
2459
|
"--map <mapping>",
|
|
@@ -2298,7 +2461,7 @@ function registerTaskImport(parent, container) {
|
|
|
2298
2461
|
).action((opts) => {
|
|
2299
2462
|
let fileData;
|
|
2300
2463
|
try {
|
|
2301
|
-
const raw =
|
|
2464
|
+
const raw = readFileSync4(opts.file, "utf-8");
|
|
2302
2465
|
fileData = JSON.parse(raw);
|
|
2303
2466
|
} catch (e) {
|
|
2304
2467
|
return printError(
|
|
@@ -2372,10 +2535,42 @@ function registerDepGraph(parent, container) {
|
|
|
2372
2535
|
});
|
|
2373
2536
|
}
|
|
2374
2537
|
|
|
2538
|
+
// src/cli/commands/upgrade.ts
|
|
2539
|
+
function registerUpgrade(program, container) {
|
|
2540
|
+
program.command("upgrade").description("Check for updates and upgrade to the latest version").action(async () => {
|
|
2541
|
+
const checkResult = await container.updateService.checkForUpdate(APP_VERSION);
|
|
2542
|
+
if (!checkResult.ok) {
|
|
2543
|
+
printError(checkResult.error);
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (!checkResult.value.updateAvailable) {
|
|
2547
|
+
printSuccess({
|
|
2548
|
+
message: "Already up to date",
|
|
2549
|
+
currentVersion: checkResult.value.currentVersion
|
|
2550
|
+
});
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
process.stderr.write(
|
|
2554
|
+
`Upgrading from ${checkResult.value.currentVersion} to ${checkResult.value.latestVersion}...
|
|
2555
|
+
`
|
|
2556
|
+
);
|
|
2557
|
+
const upgradeResult = container.updateService.performUpgrade(APP_VERSION);
|
|
2558
|
+
if (!upgradeResult.ok) {
|
|
2559
|
+
printError(upgradeResult.error);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
printSuccess({
|
|
2563
|
+
message: "Upgrade complete",
|
|
2564
|
+
previousVersion: checkResult.value.currentVersion,
|
|
2565
|
+
installedVersion: upgradeResult.value.installedVersion
|
|
2566
|
+
});
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2375
2570
|
// src/cli/index.ts
|
|
2376
2571
|
function buildCLI(container) {
|
|
2377
2572
|
const program = new Command();
|
|
2378
|
-
program.name("tayto").description("CLI task management for solo devs and AI agents").version(
|
|
2573
|
+
program.name("tayto").description("CLI task management for solo devs and AI agents").version(APP_VERSION);
|
|
2379
2574
|
const project = program.command("project").description("Manage projects");
|
|
2380
2575
|
registerProjectCreate(project, container);
|
|
2381
2576
|
registerProjectList(project, container);
|
|
@@ -2400,26 +2595,63 @@ function buildCLI(container) {
|
|
|
2400
2595
|
registerDepRemove(dep, container);
|
|
2401
2596
|
registerDepList(dep, container);
|
|
2402
2597
|
registerDepGraph(dep, container);
|
|
2598
|
+
registerUpgrade(program, container);
|
|
2403
2599
|
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-
|
|
2600
|
+
const { launchTUI } = await import("./tui-NCL4RFFD.js");
|
|
2405
2601
|
await launchTUI(container, opts.project);
|
|
2406
2602
|
});
|
|
2407
2603
|
return program;
|
|
2408
2604
|
}
|
|
2409
2605
|
|
|
2410
2606
|
// src/index.ts
|
|
2607
|
+
async function checkForUpdateQuietly(container, currentVersion) {
|
|
2608
|
+
let timerId;
|
|
2609
|
+
try {
|
|
2610
|
+
const result = await Promise.race([
|
|
2611
|
+
container.updateService.checkForUpdate(currentVersion),
|
|
2612
|
+
new Promise((resolve) => {
|
|
2613
|
+
timerId = setTimeout(() => {
|
|
2614
|
+
resolve(null);
|
|
2615
|
+
}, 2e3);
|
|
2616
|
+
})
|
|
2617
|
+
]);
|
|
2618
|
+
clearTimeout(timerId);
|
|
2619
|
+
if (result !== null && result.ok) {
|
|
2620
|
+
return result.value;
|
|
2621
|
+
}
|
|
2622
|
+
return null;
|
|
2623
|
+
} catch {
|
|
2624
|
+
clearTimeout(timerId);
|
|
2625
|
+
return null;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2411
2628
|
async function main() {
|
|
2412
2629
|
const config = loadConfig();
|
|
2413
2630
|
logger.init(config.logDir);
|
|
2414
2631
|
initTelemetry(config);
|
|
2415
2632
|
const db = createDatabase(config.dbPath);
|
|
2416
2633
|
runMigrations(db);
|
|
2417
|
-
const container = createContainer(db, config.dbPath);
|
|
2634
|
+
const container = createContainer(db, config.dbPath, void 0, config.updateCachePath, config.dismissedGitRemotesPath);
|
|
2418
2635
|
const args = process.argv.slice(2);
|
|
2636
|
+
const isUpgradeCommand = args[0] === "upgrade";
|
|
2637
|
+
let updateCheck = null;
|
|
2638
|
+
if (!config.noUpdateCheck && !isUpgradeCommand) {
|
|
2639
|
+
updateCheck = await checkForUpdateQuietly(container, APP_VERSION);
|
|
2640
|
+
}
|
|
2419
2641
|
if (args.length === 0) {
|
|
2420
|
-
const { launchTUI } = await import("./tui-
|
|
2421
|
-
await launchTUI(
|
|
2642
|
+
const { launchTUI } = await import("./tui-NCL4RFFD.js");
|
|
2643
|
+
await launchTUI(
|
|
2644
|
+
container,
|
|
2645
|
+
void 0,
|
|
2646
|
+
updateCheck?.updateAvailable ? updateCheck.latestVersion : void 0
|
|
2647
|
+
);
|
|
2422
2648
|
} else {
|
|
2649
|
+
if (updateCheck?.updateAvailable) {
|
|
2650
|
+
process.stderr.write(
|
|
2651
|
+
`\x1B[33m[tayto]\x1B[0m Update available: ${updateCheck.currentVersion} \u2192 ${updateCheck.latestVersion}. Run: tayto upgrade
|
|
2652
|
+
`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2423
2655
|
const program = buildCLI(container);
|
|
2424
2656
|
await program.parseAsync(process.argv);
|
|
2425
2657
|
}
|