@tomkapa/tayto 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-STYT4TGJ.js → chunk-74Q55TOV.js} +41 -2
- package/dist/chunk-74Q55TOV.js.map +1 -0
- package/dist/index.js +136 -43
- package/dist/index.js.map +1 -1
- package/dist/migrations/005_project_git_remote.sql +5 -0
- package/dist/{tui-JEP3F4JS.js → tui-4GNIGMCK.js} +287 -130
- package/dist/tui-4GNIGMCK.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-STYT4TGJ.js.map +0 -1
- package/dist/tui-JEP3F4JS.js.map +0 -1
|
@@ -143,8 +143,46 @@ function isTerminalStatus(status) {
|
|
|
143
143
|
return TERMINAL_STATUSES.has(status);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// src/utils/git.ts
|
|
147
|
+
import { spawnSync } from "child_process";
|
|
148
|
+
|
|
149
|
+
// src/types/common.ts
|
|
150
|
+
function ok(value) {
|
|
151
|
+
return { ok: true, value };
|
|
152
|
+
}
|
|
153
|
+
function err(error) {
|
|
154
|
+
return { ok: false, error };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/utils/git.ts
|
|
158
|
+
function detectGitRemote(cwd) {
|
|
159
|
+
try {
|
|
160
|
+
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
161
|
+
cwd: cwd ?? process.cwd(),
|
|
162
|
+
encoding: "utf-8",
|
|
163
|
+
timeout: 5e3
|
|
164
|
+
});
|
|
165
|
+
if (result.status !== 0 || result.error) {
|
|
166
|
+
logger.info("detectGitRemote: no git remote found (non-zero exit or error)");
|
|
167
|
+
return ok(null);
|
|
168
|
+
}
|
|
169
|
+
const remote = result.stdout.trim();
|
|
170
|
+
if (!remote) {
|
|
171
|
+
logger.info("detectGitRemote: empty stdout from git remote get-url");
|
|
172
|
+
return ok(null);
|
|
173
|
+
}
|
|
174
|
+
logger.info(`detectGitRemote: found remote=${remote}`);
|
|
175
|
+
return ok(remote);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
logger.info(`detectGitRemote: exception during git detection: ${String(e)}`);
|
|
178
|
+
return ok(null);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
146
182
|
export {
|
|
147
183
|
logger,
|
|
184
|
+
ok,
|
|
185
|
+
err,
|
|
148
186
|
TaskStatus,
|
|
149
187
|
TaskType,
|
|
150
188
|
TaskLevel,
|
|
@@ -155,6 +193,7 @@ export {
|
|
|
155
193
|
RANK_GAP,
|
|
156
194
|
midpoint,
|
|
157
195
|
TERMINAL_STATUSES,
|
|
158
|
-
isTerminalStatus
|
|
196
|
+
isTerminalStatus,
|
|
197
|
+
detectGitRemote
|
|
159
198
|
};
|
|
160
|
-
//# sourceMappingURL=chunk-
|
|
199
|
+
//# sourceMappingURL=chunk-74Q55TOV.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"],"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 { logger } from '../logging/logger.js';\n\n/**\n * Detect the git origin remote URL from the given directory.\n * Returns ok(url) 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<string | 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 remote = result.stdout.trim();\n if (!remote) {\n logger.info('detectGitRemote: empty stdout from git remote get-url');\n return ok(null);\n }\n\n logger.info(`detectGitRemote: found remote=${remote}`);\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"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
TaskType,
|
|
9
9
|
UIDependencyType,
|
|
10
10
|
WORK_TYPES,
|
|
11
|
+
detectGitRemote,
|
|
12
|
+
err,
|
|
11
13
|
getTaskLevel,
|
|
12
14
|
isTerminalStatus,
|
|
13
15
|
logger,
|
|
14
|
-
midpoint
|
|
15
|
-
|
|
16
|
+
midpoint,
|
|
17
|
+
ok
|
|
18
|
+
} from "./chunk-74Q55TOV.js";
|
|
16
19
|
|
|
17
20
|
// src/config/index.ts
|
|
18
21
|
import { mkdirSync } from "fs";
|
|
@@ -106,14 +109,6 @@ async function shutdownTelemetry() {
|
|
|
106
109
|
}
|
|
107
110
|
}
|
|
108
111
|
|
|
109
|
-
// src/types/common.ts
|
|
110
|
-
function ok(value) {
|
|
111
|
-
return { ok: true, value };
|
|
112
|
-
}
|
|
113
|
-
function err(error) {
|
|
114
|
-
return { ok: false, error };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
112
|
// src/errors/app-error.ts
|
|
118
113
|
var AppError = class extends Error {
|
|
119
114
|
constructor(code, message, cause) {
|
|
@@ -156,6 +151,7 @@ function rowToProject(row) {
|
|
|
156
151
|
name: row.name,
|
|
157
152
|
description: row.description,
|
|
158
153
|
isDefault: row.is_default === 1,
|
|
154
|
+
gitRemote: row.git_remote,
|
|
159
155
|
createdAt: row.created_at,
|
|
160
156
|
updatedAt: row.updated_at
|
|
161
157
|
};
|
|
@@ -174,14 +170,15 @@ var SqliteProjectRepository = class {
|
|
|
174
170
|
this.db.prepare("UPDATE projects SET is_default = 0 WHERE is_default = 1").run();
|
|
175
171
|
}
|
|
176
172
|
this.db.prepare(
|
|
177
|
-
`INSERT INTO projects (id, key, name, description, is_default, created_at, updated_at)
|
|
178
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
173
|
+
`INSERT INTO projects (id, key, name, description, is_default, git_remote, created_at, updated_at)
|
|
174
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
179
175
|
).run(
|
|
180
176
|
id,
|
|
181
177
|
input.key,
|
|
182
178
|
input.name,
|
|
183
179
|
input.description ?? "",
|
|
184
180
|
input.isDefault ? 1 : 0,
|
|
181
|
+
input.gitRemote ?? null,
|
|
185
182
|
now,
|
|
186
183
|
now
|
|
187
184
|
);
|
|
@@ -192,6 +189,15 @@ var SqliteProjectRepository = class {
|
|
|
192
189
|
return ok(rowToProject(row));
|
|
193
190
|
} catch (e) {
|
|
194
191
|
if (e instanceof Error && e.message.includes("UNIQUE constraint")) {
|
|
192
|
+
if (e.message.includes("git_remote")) {
|
|
193
|
+
return err(
|
|
194
|
+
new AppError(
|
|
195
|
+
"DUPLICATE",
|
|
196
|
+
`Git remote already linked to another project: ${input.gitRemote}`,
|
|
197
|
+
e
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
195
201
|
return err(new AppError("DUPLICATE", `Project name already exists: ${input.name}`, e));
|
|
196
202
|
}
|
|
197
203
|
return err(new AppError("DB_ERROR", "Failed to insert project", e));
|
|
@@ -222,6 +228,14 @@ var SqliteProjectRepository = class {
|
|
|
222
228
|
return err(new AppError("DB_ERROR", "Failed to find project by name", e));
|
|
223
229
|
}
|
|
224
230
|
}
|
|
231
|
+
findByGitRemote(remote) {
|
|
232
|
+
try {
|
|
233
|
+
const row = this.db.prepare(`SELECT * FROM projects WHERE git_remote = ? AND ${NOT_DELETED}`).get(remote);
|
|
234
|
+
return ok(row ? rowToProject(row) : null);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return err(new AppError("DB_ERROR", "Failed to find project by git remote", e));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
225
239
|
findDefault() {
|
|
226
240
|
try {
|
|
227
241
|
const row = this.db.prepare(`SELECT * FROM projects WHERE is_default = 1 AND ${NOT_DELETED}`).get();
|
|
@@ -251,12 +265,13 @@ var SqliteProjectRepository = class {
|
|
|
251
265
|
}
|
|
252
266
|
this.db.prepare(
|
|
253
267
|
`UPDATE projects SET
|
|
254
|
-
name = ?, description = ?, is_default = ?, updated_at = ?
|
|
268
|
+
name = ?, description = ?, is_default = ?, git_remote = ?, updated_at = ?
|
|
255
269
|
WHERE id = ?`
|
|
256
270
|
).run(
|
|
257
271
|
input.name ?? existing.name,
|
|
258
272
|
input.description ?? existing.description,
|
|
259
273
|
input.isDefault !== void 0 ? input.isDefault ? 1 : 0 : existing.is_default,
|
|
274
|
+
input.gitRemote !== void 0 ? input.gitRemote : existing.git_remote,
|
|
260
275
|
now,
|
|
261
276
|
id
|
|
262
277
|
);
|
|
@@ -267,6 +282,11 @@ var SqliteProjectRepository = class {
|
|
|
267
282
|
return ok(rowToProject(row));
|
|
268
283
|
} catch (e) {
|
|
269
284
|
if (e instanceof Error && e.message.includes("UNIQUE constraint")) {
|
|
285
|
+
if (e.message.includes("git_remote")) {
|
|
286
|
+
return err(
|
|
287
|
+
new AppError("DUPLICATE", `Git remote already linked to another project`, e)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
270
290
|
return err(new AppError("DUPLICATE", `Project name already exists`, e));
|
|
271
291
|
}
|
|
272
292
|
return err(new AppError("DB_ERROR", "Failed to update project", e));
|
|
@@ -942,20 +962,24 @@ var CreateProjectSchema = z.object({
|
|
|
942
962
|
name: z.string().min(1, "Project name is required").max(255),
|
|
943
963
|
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(),
|
|
944
964
|
description: z.string().max(5e3).optional(),
|
|
945
|
-
isDefault: z.boolean().optional()
|
|
965
|
+
isDefault: z.boolean().optional(),
|
|
966
|
+
gitRemote: z.string().min(1, "Git remote URL must not be empty").nullable().optional()
|
|
946
967
|
});
|
|
947
968
|
var UpdateProjectSchema = z.object({
|
|
948
969
|
name: z.string().min(1).max(255).optional(),
|
|
949
970
|
description: z.string().max(5e3).optional(),
|
|
950
|
-
isDefault: z.boolean().optional()
|
|
971
|
+
isDefault: z.boolean().optional(),
|
|
972
|
+
gitRemote: z.string().min(1, "Git remote URL must not be empty").nullable().optional()
|
|
951
973
|
});
|
|
952
974
|
|
|
953
975
|
// src/service/project.service.ts
|
|
954
976
|
var ProjectServiceImpl = class {
|
|
955
|
-
constructor(repo) {
|
|
977
|
+
constructor(repo, detectRemote = detectGitRemote) {
|
|
956
978
|
this.repo = repo;
|
|
979
|
+
this.detectRemote = detectRemote;
|
|
957
980
|
}
|
|
958
981
|
repo;
|
|
982
|
+
detectRemote;
|
|
959
983
|
createProject(input) {
|
|
960
984
|
return logger.startSpan("ProjectService.createProject", () => {
|
|
961
985
|
const parsed = CreateProjectSchema.safeParse(input);
|
|
@@ -1037,6 +1061,58 @@ var ProjectServiceImpl = class {
|
|
|
1037
1061
|
);
|
|
1038
1062
|
});
|
|
1039
1063
|
}
|
|
1064
|
+
resolveProjectWithGit(idOrName, cwd) {
|
|
1065
|
+
return logger.startSpan("ProjectService.resolveProjectWithGit", () => {
|
|
1066
|
+
if (idOrName) {
|
|
1067
|
+
return this.resolveProject(idOrName);
|
|
1068
|
+
}
|
|
1069
|
+
const remoteResult = this.detectRemote(cwd);
|
|
1070
|
+
if (remoteResult.ok && remoteResult.value) {
|
|
1071
|
+
const byRemote = this.repo.findByGitRemote(remoteResult.value);
|
|
1072
|
+
if (!byRemote.ok) return byRemote;
|
|
1073
|
+
if (byRemote.value) {
|
|
1074
|
+
logger.info(
|
|
1075
|
+
`resolveProjectWithGit: matched git remote to project key=${byRemote.value.key}`
|
|
1076
|
+
);
|
|
1077
|
+
return ok(byRemote.value);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return this.resolveProject();
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
linkGitRemote(idOrName, remote) {
|
|
1084
|
+
return logger.startSpan("ProjectService.linkGitRemote", () => {
|
|
1085
|
+
const resolved = this.resolveProject(idOrName);
|
|
1086
|
+
if (!resolved.ok) return resolved;
|
|
1087
|
+
let url = remote;
|
|
1088
|
+
if (!url) {
|
|
1089
|
+
const detected = this.detectRemote();
|
|
1090
|
+
if (!detected.ok) return detected;
|
|
1091
|
+
if (!detected.value) {
|
|
1092
|
+
return err(
|
|
1093
|
+
new AppError(
|
|
1094
|
+
"NOT_FOUND",
|
|
1095
|
+
"No git remote detected in current directory. Use --remote <url> to specify one explicitly."
|
|
1096
|
+
)
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
url = detected.value;
|
|
1100
|
+
}
|
|
1101
|
+
return this.repo.update(resolved.value.id, { gitRemote: url });
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
unlinkGitRemote(idOrName) {
|
|
1105
|
+
return logger.startSpan("ProjectService.unlinkGitRemote", () => {
|
|
1106
|
+
const resolved = this.resolveProject(idOrName);
|
|
1107
|
+
if (!resolved.ok) return resolved;
|
|
1108
|
+
if (!resolved.value.gitRemote) {
|
|
1109
|
+
return err(
|
|
1110
|
+
new AppError("NOT_FOUND", `Project "${resolved.value.name}" has no linked git remote.`)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
return this.repo.update(resolved.value.id, { gitRemote: null });
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1040
1116
|
nextTaskId(project) {
|
|
1041
1117
|
return logger.startSpan("ProjectService.nextTaskId", () => {
|
|
1042
1118
|
const counterResult = this.repo.incrementTaskCounter(project.id);
|
|
@@ -1168,12 +1244,12 @@ var TaskServiceImpl = class {
|
|
|
1168
1244
|
if (!parsed.success) {
|
|
1169
1245
|
return err(new AppError("VALIDATION", parsed.error.message));
|
|
1170
1246
|
}
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
}
|
|
1247
|
+
const projectResult = this.projectService.resolveProject(parsed.data.projectId);
|
|
1248
|
+
if (!projectResult.ok) return projectResult;
|
|
1249
|
+
const resolvedFilter = {
|
|
1250
|
+
...parsed.data,
|
|
1251
|
+
projectId: projectResult.value.id
|
|
1252
|
+
};
|
|
1177
1253
|
return this.repo.findMany(resolvedFilter);
|
|
1178
1254
|
});
|
|
1179
1255
|
}
|
|
@@ -1453,13 +1529,9 @@ var TaskServiceImpl = class {
|
|
|
1453
1529
|
if (!query.trim()) {
|
|
1454
1530
|
return err(new AppError("VALIDATION", "Search query cannot be empty"));
|
|
1455
1531
|
}
|
|
1456
|
-
|
|
1457
|
-
if (
|
|
1458
|
-
|
|
1459
|
-
if (!projectResult.ok) return projectResult;
|
|
1460
|
-
projectId = projectResult.value.id;
|
|
1461
|
-
}
|
|
1462
|
-
return this.repo.search(query, projectId);
|
|
1532
|
+
const projectResult = this.projectService.resolveProject(projectIdOrName);
|
|
1533
|
+
if (!projectResult.ok) return projectResult;
|
|
1534
|
+
return this.repo.search(query, projectResult.value.id);
|
|
1463
1535
|
});
|
|
1464
1536
|
}
|
|
1465
1537
|
/**
|
|
@@ -1953,11 +2025,11 @@ var PortabilityServiceImpl = class {
|
|
|
1953
2025
|
};
|
|
1954
2026
|
|
|
1955
2027
|
// src/cli/container.ts
|
|
1956
|
-
function createContainer(db, dbPath) {
|
|
2028
|
+
function createContainer(db, dbPath, detectGitRemote2) {
|
|
1957
2029
|
const projectRepo = new SqliteProjectRepository(db);
|
|
1958
2030
|
const taskRepo = new SqliteTaskRepository(db);
|
|
1959
2031
|
const depRepo = new SqliteDependencyRepository(db);
|
|
1960
|
-
const projectService = new ProjectServiceImpl(projectRepo);
|
|
2032
|
+
const projectService = new ProjectServiceImpl(projectRepo, detectGitRemote2);
|
|
1961
2033
|
const dependencyService = new DependencyServiceImpl(depRepo, taskRepo);
|
|
1962
2034
|
const taskService = new TaskServiceImpl(taskRepo, projectService, () => dependencyService);
|
|
1963
2035
|
const portabilityService = new PortabilityServiceImpl(
|
|
@@ -1993,16 +2065,19 @@ function handleResult(result) {
|
|
|
1993
2065
|
function registerProjectCreate(parent, container) {
|
|
1994
2066
|
parent.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Project name").option(
|
|
1995
2067
|
"-k, --key <key>",
|
|
1996
|
-
"Project key (2-
|
|
1997
|
-
).option("-d, --description <description>", "Project description").option("--default", "Set as default project").
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2068
|
+
"Project key (2-7 uppercase alphanumeric chars, defaults to first 3 chars of name)"
|
|
2069
|
+
).option("-d, --description <description>", "Project description").option("--default", "Set as default project").option("--git-remote <url>", "Git remote URL to associate with the project").action(
|
|
2070
|
+
(opts) => {
|
|
2071
|
+
const result = container.projectService.createProject({
|
|
2072
|
+
name: opts.name,
|
|
2073
|
+
key: opts.key,
|
|
2074
|
+
description: opts.description,
|
|
2075
|
+
isDefault: opts.default,
|
|
2076
|
+
gitRemote: opts.gitRemote
|
|
2077
|
+
});
|
|
2078
|
+
handleResult(result);
|
|
2079
|
+
}
|
|
2080
|
+
);
|
|
2006
2081
|
}
|
|
2007
2082
|
|
|
2008
2083
|
// src/cli/commands/project/list.ts
|
|
@@ -2060,6 +2135,22 @@ function registerProjectSetDefault(parent, container) {
|
|
|
2060
2135
|
});
|
|
2061
2136
|
}
|
|
2062
2137
|
|
|
2138
|
+
// src/cli/commands/project/link.ts
|
|
2139
|
+
function registerProjectLink(parent, container) {
|
|
2140
|
+
parent.command("link <idOrKeyOrName>").description("Link a project to a git remote (auto-detects from cwd if --remote omitted)").option("-r, --remote <url>", "Git remote URL").action((idOrKeyOrName, opts) => {
|
|
2141
|
+
const result = container.projectService.linkGitRemote(idOrKeyOrName, opts.remote);
|
|
2142
|
+
handleResult(result);
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
// src/cli/commands/project/unlink.ts
|
|
2147
|
+
function registerProjectUnlink(parent, container) {
|
|
2148
|
+
parent.command("unlink <idOrKeyOrName>").description("Remove git remote link from a project").action((idOrKeyOrName) => {
|
|
2149
|
+
const result = container.projectService.unlinkGitRemote(idOrKeyOrName);
|
|
2150
|
+
handleResult(result);
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2063
2154
|
// src/cli/commands/task/create.ts
|
|
2064
2155
|
function registerTaskCreate(parent, container) {
|
|
2065
2156
|
parent.command("create").description("Create a new task (appended to bottom of backlog)").requiredOption("-n, --name <name>", "Task name").option("-p, --project <project>", "Project id or name").option("-d, --description <description>", "Task description").option("-t, --type <type>", "Task type: epic, story, tech-debt, bug", "story").option("-s, --status <status>", "Task status", "backlog").option("--parent <parentId>", "Parent task id for subtask").option("--technical-notes <notes>", "Technical notes (markdown)").option("--additional-requirements <requirements>", "Additional requirements (markdown)").option("--depends-on <ids...>", "Task ids this task depends on (blocks relationship)").action(
|
|
@@ -2302,6 +2393,8 @@ function buildCLI(container) {
|
|
|
2302
2393
|
registerProjectUpdate(project, container);
|
|
2303
2394
|
registerProjectDelete(project, container);
|
|
2304
2395
|
registerProjectSetDefault(project, container);
|
|
2396
|
+
registerProjectLink(project, container);
|
|
2397
|
+
registerProjectUnlink(project, container);
|
|
2305
2398
|
const task = program.command("task").description("Manage tasks");
|
|
2306
2399
|
registerTaskCreate(task, container);
|
|
2307
2400
|
registerTaskList(task, container);
|
|
@@ -2319,7 +2412,7 @@ function buildCLI(container) {
|
|
|
2319
2412
|
registerDepList(dep, container);
|
|
2320
2413
|
registerDepGraph(dep, container);
|
|
2321
2414
|
program.command("tui").description("Launch interactive terminal UI").option("-p, --project <project>", "Start with specific project").action(async (opts) => {
|
|
2322
|
-
const { launchTUI } = await import("./tui-
|
|
2415
|
+
const { launchTUI } = await import("./tui-4GNIGMCK.js");
|
|
2323
2416
|
await launchTUI(container, opts.project);
|
|
2324
2417
|
});
|
|
2325
2418
|
return program;
|
|
@@ -2335,7 +2428,7 @@ async function main() {
|
|
|
2335
2428
|
const container = createContainer(db, config.dbPath);
|
|
2336
2429
|
const args = process.argv.slice(2);
|
|
2337
2430
|
if (args.length === 0) {
|
|
2338
|
-
const { launchTUI } = await import("./tui-
|
|
2431
|
+
const { launchTUI } = await import("./tui-4GNIGMCK.js");
|
|
2339
2432
|
await launchTUI(container);
|
|
2340
2433
|
} else {
|
|
2341
2434
|
const program = buildCLI(container);
|