@tomkapa/tayto 0.3.2 → 0.4.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.
- package/README.md +190 -111
- package/dist/{chunk-STYT4TGJ.js → chunk-74Q55TOV.js} +41 -2
- package/dist/chunk-74Q55TOV.js.map +1 -0
- package/dist/index.js +162 -80
- package/dist/index.js.map +1 -1
- package/dist/migrations/005_project_git_remote.sql +5 -0
- package/dist/{tui-JEP3F4JS.js → tui-IXZGQMWN.js} +308 -143
- package/dist/tui-IXZGQMWN.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
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);
|
|
@@ -1012,7 +1036,7 @@ var ProjectServiceImpl = class {
|
|
|
1012
1036
|
deleteProject(id) {
|
|
1013
1037
|
return this.repo.delete(id);
|
|
1014
1038
|
}
|
|
1015
|
-
resolveProject(idOrName) {
|
|
1039
|
+
resolveProject(idOrName, cwd) {
|
|
1016
1040
|
return logger.startSpan("ProjectService.resolveProject", () => {
|
|
1017
1041
|
if (idOrName) {
|
|
1018
1042
|
const byId = this.repo.findById(idOrName);
|
|
@@ -1026,6 +1050,15 @@ var ProjectServiceImpl = class {
|
|
|
1026
1050
|
if (byName.value) return ok(byName.value);
|
|
1027
1051
|
return err(new AppError("NOT_FOUND", `Project not found: ${idOrName}`));
|
|
1028
1052
|
}
|
|
1053
|
+
const remoteResult = this.detectRemote(cwd);
|
|
1054
|
+
if (remoteResult.ok && remoteResult.value) {
|
|
1055
|
+
const byRemote = this.repo.findByGitRemote(remoteResult.value);
|
|
1056
|
+
if (!byRemote.ok) return byRemote;
|
|
1057
|
+
if (byRemote.value) {
|
|
1058
|
+
logger.info(`resolveProject: matched git remote to project key=${byRemote.value.key}`);
|
|
1059
|
+
return ok(byRemote.value);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1029
1062
|
const defaultProject = this.repo.findDefault();
|
|
1030
1063
|
if (!defaultProject.ok) return defaultProject;
|
|
1031
1064
|
if (defaultProject.value) return ok(defaultProject.value);
|
|
@@ -1037,6 +1070,39 @@ var ProjectServiceImpl = class {
|
|
|
1037
1070
|
);
|
|
1038
1071
|
});
|
|
1039
1072
|
}
|
|
1073
|
+
linkGitRemote(idOrName, remote) {
|
|
1074
|
+
return logger.startSpan("ProjectService.linkGitRemote", () => {
|
|
1075
|
+
const resolved = this.resolveProject(idOrName);
|
|
1076
|
+
if (!resolved.ok) return resolved;
|
|
1077
|
+
let url = remote;
|
|
1078
|
+
if (!url) {
|
|
1079
|
+
const detected = this.detectRemote();
|
|
1080
|
+
if (!detected.ok) return detected;
|
|
1081
|
+
if (!detected.value) {
|
|
1082
|
+
return err(
|
|
1083
|
+
new AppError(
|
|
1084
|
+
"NOT_FOUND",
|
|
1085
|
+
"No git remote detected in current directory. Use --remote <url> to specify one explicitly."
|
|
1086
|
+
)
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
url = detected.value;
|
|
1090
|
+
}
|
|
1091
|
+
return this.repo.update(resolved.value.id, { gitRemote: url });
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
unlinkGitRemote(idOrName) {
|
|
1095
|
+
return logger.startSpan("ProjectService.unlinkGitRemote", () => {
|
|
1096
|
+
const resolved = this.resolveProject(idOrName);
|
|
1097
|
+
if (!resolved.ok) return resolved;
|
|
1098
|
+
if (!resolved.value.gitRemote) {
|
|
1099
|
+
return err(
|
|
1100
|
+
new AppError("NOT_FOUND", `Project "${resolved.value.name}" has no linked git remote.`)
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
return this.repo.update(resolved.value.id, { gitRemote: null });
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1040
1106
|
nextTaskId(project) {
|
|
1041
1107
|
return logger.startSpan("ProjectService.nextTaskId", () => {
|
|
1042
1108
|
const counterResult = this.repo.incrementTaskCounter(project.id);
|
|
@@ -1078,7 +1144,6 @@ var UpdateTaskSchema = z2.object({
|
|
|
1078
1144
|
appendRequirements: z2.string().max(5e4).optional()
|
|
1079
1145
|
});
|
|
1080
1146
|
var TaskFilterSchema = z2.object({
|
|
1081
|
-
projectId: z2.string().optional(),
|
|
1082
1147
|
status: z2.enum(taskStatusValues).optional(),
|
|
1083
1148
|
type: z2.enum(taskTypeValues).optional(),
|
|
1084
1149
|
level: z2.number().int().min(1).max(2).optional(),
|
|
@@ -1108,15 +1173,12 @@ var TaskServiceImpl = class {
|
|
|
1108
1173
|
repo;
|
|
1109
1174
|
projectService;
|
|
1110
1175
|
getDependencyService;
|
|
1111
|
-
createTask(input,
|
|
1176
|
+
createTask(input, project) {
|
|
1112
1177
|
return logger.startSpan("TaskService.createTask", () => {
|
|
1113
1178
|
const parsed = CreateTaskSchema.safeParse(input);
|
|
1114
1179
|
if (!parsed.success) {
|
|
1115
1180
|
return err(new AppError("VALIDATION", parsed.error.message));
|
|
1116
1181
|
}
|
|
1117
|
-
const projectRef = parsed.data.projectId ?? projectIdOrName;
|
|
1118
|
-
const projectResult = this.projectService.resolveProject(projectRef);
|
|
1119
|
-
if (!projectResult.ok) return projectResult;
|
|
1120
1182
|
const taskLevel = getTaskLevel(parsed.data.type);
|
|
1121
1183
|
if (taskLevel === TaskLevel.Epic && parsed.data.parentId) {
|
|
1122
1184
|
return err(new AppError("VALIDATION", "Epic tasks cannot have a parent"));
|
|
@@ -1131,7 +1193,6 @@ var TaskServiceImpl = class {
|
|
|
1131
1193
|
return err(new AppError("VALIDATION", "Tasks can only be children of epic-level tasks"));
|
|
1132
1194
|
}
|
|
1133
1195
|
}
|
|
1134
|
-
const project = projectResult.value;
|
|
1135
1196
|
const taskIdResult = this.projectService.nextTaskId(project);
|
|
1136
1197
|
if (!taskIdResult.ok) return taskIdResult;
|
|
1137
1198
|
const insertResult = this.repo.insert(taskIdResult.value, {
|
|
@@ -1162,18 +1223,16 @@ var TaskServiceImpl = class {
|
|
|
1162
1223
|
return ok(result.value);
|
|
1163
1224
|
});
|
|
1164
1225
|
}
|
|
1165
|
-
listTasks(filter) {
|
|
1226
|
+
listTasks(project, filter) {
|
|
1166
1227
|
return logger.startSpan("TaskService.listTasks", () => {
|
|
1167
1228
|
const parsed = TaskFilterSchema.safeParse(filter);
|
|
1168
1229
|
if (!parsed.success) {
|
|
1169
1230
|
return err(new AppError("VALIDATION", parsed.error.message));
|
|
1170
1231
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
resolvedFilter = { ...resolvedFilter, projectId: projectResult.value.id };
|
|
1176
|
-
}
|
|
1232
|
+
const resolvedFilter = {
|
|
1233
|
+
...parsed.data,
|
|
1234
|
+
projectId: project.id
|
|
1235
|
+
};
|
|
1177
1236
|
return this.repo.findMany(resolvedFilter);
|
|
1178
1237
|
});
|
|
1179
1238
|
}
|
|
@@ -1277,7 +1336,7 @@ var TaskServiceImpl = class {
|
|
|
1277
1336
|
if (getTaskLevel(parent.type) !== TaskLevel.Epic) {
|
|
1278
1337
|
return err(new AppError("VALIDATION", "Breakdown parent must be an epic-level task"));
|
|
1279
1338
|
}
|
|
1280
|
-
const projectResult = this.projectService.
|
|
1339
|
+
const projectResult = this.projectService.getProject(parent.projectId);
|
|
1281
1340
|
if (!projectResult.ok) return projectResult;
|
|
1282
1341
|
const project = projectResult.value;
|
|
1283
1342
|
const created = [];
|
|
@@ -1312,7 +1371,7 @@ var TaskServiceImpl = class {
|
|
|
1312
1371
|
* If moving to the top, rank = first_rank - GAP.
|
|
1313
1372
|
* If moving to the bottom, rank = last_rank + GAP.
|
|
1314
1373
|
*/
|
|
1315
|
-
rerankTask(input,
|
|
1374
|
+
rerankTask(input, project) {
|
|
1316
1375
|
return logger.startSpan("TaskService.rerankTask", () => {
|
|
1317
1376
|
const parsed = RerankTaskSchema.safeParse(input);
|
|
1318
1377
|
if (!parsed.success) {
|
|
@@ -1343,10 +1402,7 @@ var TaskServiceImpl = class {
|
|
|
1343
1402
|
);
|
|
1344
1403
|
}
|
|
1345
1404
|
const taskLevel = getTaskLevel(task.type);
|
|
1346
|
-
const
|
|
1347
|
-
const projectResult = this.projectService.resolveProject(projectRef);
|
|
1348
|
-
if (!projectResult.ok) return projectResult;
|
|
1349
|
-
const projectId = projectResult.value.id;
|
|
1405
|
+
const projectId = project.id;
|
|
1350
1406
|
const depService = this.getDependencyService();
|
|
1351
1407
|
const blockersResult = depService.listBlockers(taskId);
|
|
1352
1408
|
if (!blockersResult.ok) return blockersResult;
|
|
@@ -1448,18 +1504,12 @@ var TaskServiceImpl = class {
|
|
|
1448
1504
|
return this.repo.rerank(taskId, newRank);
|
|
1449
1505
|
});
|
|
1450
1506
|
}
|
|
1451
|
-
searchTasks(query,
|
|
1507
|
+
searchTasks(query, project) {
|
|
1452
1508
|
return logger.startSpan("TaskService.searchTasks", () => {
|
|
1453
1509
|
if (!query.trim()) {
|
|
1454
1510
|
return err(new AppError("VALIDATION", "Search query cannot be empty"));
|
|
1455
1511
|
}
|
|
1456
|
-
|
|
1457
|
-
if (projectIdOrName) {
|
|
1458
|
-
const projectResult = this.projectService.resolveProject(projectIdOrName);
|
|
1459
|
-
if (!projectResult.ok) return projectResult;
|
|
1460
|
-
projectId = projectResult.value.id;
|
|
1461
|
-
}
|
|
1462
|
-
return this.repo.search(query, projectId);
|
|
1512
|
+
return this.repo.search(query, project.id);
|
|
1463
1513
|
});
|
|
1464
1514
|
}
|
|
1465
1515
|
/**
|
|
@@ -1737,20 +1787,15 @@ function parseFieldMapping(raw) {
|
|
|
1737
1787
|
|
|
1738
1788
|
// src/service/portability.service.ts
|
|
1739
1789
|
var PortabilityServiceImpl = class {
|
|
1740
|
-
constructor(taskService, depService
|
|
1790
|
+
constructor(taskService, depService) {
|
|
1741
1791
|
this.taskService = taskService;
|
|
1742
1792
|
this.depService = depService;
|
|
1743
|
-
this.projectService = projectService;
|
|
1744
1793
|
}
|
|
1745
1794
|
taskService;
|
|
1746
1795
|
depService;
|
|
1747
|
-
|
|
1748
|
-
exportTasks(projectIdOrName) {
|
|
1796
|
+
exportTasks(project) {
|
|
1749
1797
|
return logger.startSpan("PortabilityService.exportTasks", () => {
|
|
1750
|
-
const
|
|
1751
|
-
if (!projectResult.ok) return projectResult;
|
|
1752
|
-
const project = projectResult.value;
|
|
1753
|
-
const tasksResult = this.taskService.listTasks({ projectId: project.id });
|
|
1798
|
+
const tasksResult = this.taskService.listTasks(project, {});
|
|
1754
1799
|
if (!tasksResult.ok) return tasksResult;
|
|
1755
1800
|
const tasks = tasksResult.value;
|
|
1756
1801
|
const taskIds = new Set(tasks.map((t) => t.id));
|
|
@@ -1792,7 +1837,7 @@ var PortabilityServiceImpl = class {
|
|
|
1792
1837
|
});
|
|
1793
1838
|
});
|
|
1794
1839
|
}
|
|
1795
|
-
importTasks(fileData,
|
|
1840
|
+
importTasks(fileData, project, fieldMapping) {
|
|
1796
1841
|
return logger.startSpan("PortabilityService.importTasks", () => {
|
|
1797
1842
|
const parsed = ImportFileSchema.safeParse(fileData);
|
|
1798
1843
|
if (!parsed.success) {
|
|
@@ -1828,7 +1873,7 @@ var PortabilityServiceImpl = class {
|
|
|
1828
1873
|
technicalNotes: task.technicalNotes || void 0,
|
|
1829
1874
|
additionalRequirements: task.additionalRequirements || void 0
|
|
1830
1875
|
},
|
|
1831
|
-
|
|
1876
|
+
project
|
|
1832
1877
|
);
|
|
1833
1878
|
if (!createResult.ok) return createResult;
|
|
1834
1879
|
idMap.set(task.sourceId, createResult.value.id);
|
|
@@ -1953,18 +1998,14 @@ var PortabilityServiceImpl = class {
|
|
|
1953
1998
|
};
|
|
1954
1999
|
|
|
1955
2000
|
// src/cli/container.ts
|
|
1956
|
-
function createContainer(db, dbPath) {
|
|
2001
|
+
function createContainer(db, dbPath, detectGitRemote2) {
|
|
1957
2002
|
const projectRepo = new SqliteProjectRepository(db);
|
|
1958
2003
|
const taskRepo = new SqliteTaskRepository(db);
|
|
1959
2004
|
const depRepo = new SqliteDependencyRepository(db);
|
|
1960
|
-
const projectService = new ProjectServiceImpl(projectRepo);
|
|
2005
|
+
const projectService = new ProjectServiceImpl(projectRepo, detectGitRemote2);
|
|
1961
2006
|
const dependencyService = new DependencyServiceImpl(depRepo, taskRepo);
|
|
1962
2007
|
const taskService = new TaskServiceImpl(taskRepo, projectService, () => dependencyService);
|
|
1963
|
-
const portabilityService = new PortabilityServiceImpl(
|
|
1964
|
-
taskService,
|
|
1965
|
-
dependencyService,
|
|
1966
|
-
projectService
|
|
1967
|
-
);
|
|
2008
|
+
const portabilityService = new PortabilityServiceImpl(taskService, dependencyService);
|
|
1968
2009
|
return { dbPath, projectService, taskService, dependencyService, portabilityService };
|
|
1969
2010
|
}
|
|
1970
2011
|
|
|
@@ -1993,16 +2034,19 @@ function handleResult(result) {
|
|
|
1993
2034
|
function registerProjectCreate(parent, container) {
|
|
1994
2035
|
parent.command("create").description("Create a new project").requiredOption("-n, --name <name>", "Project name").option(
|
|
1995
2036
|
"-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
|
-
|
|
2037
|
+
"Project key (2-7 uppercase alphanumeric chars, defaults to first 3 chars of name)"
|
|
2038
|
+
).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(
|
|
2039
|
+
(opts) => {
|
|
2040
|
+
const result = container.projectService.createProject({
|
|
2041
|
+
name: opts.name,
|
|
2042
|
+
key: opts.key,
|
|
2043
|
+
description: opts.description,
|
|
2044
|
+
isDefault: opts.default,
|
|
2045
|
+
gitRemote: opts.gitRemote
|
|
2046
|
+
});
|
|
2047
|
+
handleResult(result);
|
|
2048
|
+
}
|
|
2049
|
+
);
|
|
2006
2050
|
}
|
|
2007
2051
|
|
|
2008
2052
|
// src/cli/commands/project/list.ts
|
|
@@ -2060,10 +2104,33 @@ function registerProjectSetDefault(parent, container) {
|
|
|
2060
2104
|
});
|
|
2061
2105
|
}
|
|
2062
2106
|
|
|
2107
|
+
// src/cli/commands/project/link.ts
|
|
2108
|
+
function registerProjectLink(parent, container) {
|
|
2109
|
+
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) => {
|
|
2110
|
+
const result = container.projectService.linkGitRemote(idOrKeyOrName, opts.remote);
|
|
2111
|
+
handleResult(result);
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
// src/cli/commands/project/unlink.ts
|
|
2116
|
+
function registerProjectUnlink(parent, container) {
|
|
2117
|
+
parent.command("unlink <idOrKeyOrName>").description("Remove git remote link from a project").action((idOrKeyOrName) => {
|
|
2118
|
+
const result = container.projectService.unlinkGitRemote(idOrKeyOrName);
|
|
2119
|
+
handleResult(result);
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// src/cli/helpers/project.ts
|
|
2124
|
+
function withProject(container, projectIdOrName) {
|
|
2125
|
+
return container.projectService.resolveProject(projectIdOrName);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2063
2128
|
// src/cli/commands/task/create.ts
|
|
2064
2129
|
function registerTaskCreate(parent, container) {
|
|
2065
2130
|
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(
|
|
2066
2131
|
(opts) => {
|
|
2132
|
+
const projectResult = withProject(container, opts.project);
|
|
2133
|
+
if (!projectResult.ok) return printError(projectResult.error);
|
|
2067
2134
|
const result = container.taskService.createTask(
|
|
2068
2135
|
{
|
|
2069
2136
|
name: opts.name,
|
|
@@ -2075,7 +2142,7 @@ function registerTaskCreate(parent, container) {
|
|
|
2075
2142
|
additionalRequirements: opts.additionalRequirements,
|
|
2076
2143
|
dependsOn: opts.dependsOn?.map((id) => ({ id }))
|
|
2077
2144
|
},
|
|
2078
|
-
|
|
2145
|
+
projectResult.value
|
|
2079
2146
|
);
|
|
2080
2147
|
handleResult(result);
|
|
2081
2148
|
}
|
|
@@ -2086,8 +2153,9 @@ function registerTaskCreate(parent, container) {
|
|
|
2086
2153
|
function registerTaskList(parent, container) {
|
|
2087
2154
|
parent.command("list").description("List tasks in rank order (defaults to level 2 backlog tasks)").option("-p, --project <project>", "Filter by project id or name").option("-s, --status <status>", "Filter by status (default: backlog)").option("-t, --type <type>", "Filter by type (epic, story, tech-debt, bug)").option("-l, --level <level>", "Filter by level (1=epic, 2=work). Default: 2").option("--parent <parentId>", "Filter by parent task id").option("--search <text>", "Search in name, description, and notes").action(
|
|
2088
2155
|
(opts) => {
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2156
|
+
const projectResult = withProject(container, opts.project);
|
|
2157
|
+
if (!projectResult.ok) return printError(projectResult.error);
|
|
2158
|
+
const result = container.taskService.listTasks(projectResult.value, {
|
|
2091
2159
|
status: opts.status ?? "backlog",
|
|
2092
2160
|
type: opts.type,
|
|
2093
2161
|
level: opts.level ? parseInt(opts.level, 10) : void 0,
|
|
@@ -2166,6 +2234,8 @@ function registerTaskBreakdown(parent, container) {
|
|
|
2166
2234
|
function registerTaskRank(parent, container) {
|
|
2167
2235
|
parent.command("rank <id>").description("Re-rank a task in the backlog (Jira-style positioning)").option("--after <taskId>", "Place immediately after this task").option("--before <taskId>", "Place immediately before this task").option("--position <n>", "Place at 1-based position in backlog").option("--top", "Move to the top of active tasks").option("--bottom", "Move to the bottom of active tasks (above done tasks)").option("-p, --project <project>", "Project id or name").action(
|
|
2168
2236
|
(id, opts) => {
|
|
2237
|
+
const projectResult = withProject(container, opts.project);
|
|
2238
|
+
if (!projectResult.ok) return printError(projectResult.error);
|
|
2169
2239
|
const result = container.taskService.rerankTask(
|
|
2170
2240
|
{
|
|
2171
2241
|
taskId: id,
|
|
@@ -2175,7 +2245,7 @@ function registerTaskRank(parent, container) {
|
|
|
2175
2245
|
top: opts.top,
|
|
2176
2246
|
bottom: opts.bottom
|
|
2177
2247
|
},
|
|
2178
|
-
|
|
2248
|
+
projectResult.value
|
|
2179
2249
|
);
|
|
2180
2250
|
handleResult(result);
|
|
2181
2251
|
}
|
|
@@ -2185,7 +2255,9 @@ function registerTaskRank(parent, container) {
|
|
|
2185
2255
|
// src/cli/commands/task/search.ts
|
|
2186
2256
|
function registerTaskSearch(parent, container) {
|
|
2187
2257
|
parent.command("search <query>").description("Full-text search tasks with relevance ranking (FTS5)").option("-p, --project <project>", "Limit search to a project").action((query, opts) => {
|
|
2188
|
-
const
|
|
2258
|
+
const projectResult = withProject(container, opts.project);
|
|
2259
|
+
if (!projectResult.ok) return printError(projectResult.error);
|
|
2260
|
+
const result = container.taskService.searchTasks(query, projectResult.value);
|
|
2189
2261
|
handleResult(result);
|
|
2190
2262
|
});
|
|
2191
2263
|
}
|
|
@@ -2194,7 +2266,9 @@ function registerTaskSearch(parent, container) {
|
|
|
2194
2266
|
import { writeFileSync } from "fs";
|
|
2195
2267
|
function registerTaskExport(parent, container) {
|
|
2196
2268
|
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) => {
|
|
2197
|
-
const
|
|
2269
|
+
const projectResult = withProject(container, opts.project);
|
|
2270
|
+
if (!projectResult.ok) return printError(projectResult.error);
|
|
2271
|
+
const result = container.portabilityService.exportTasks(projectResult.value);
|
|
2198
2272
|
if (!result.ok) {
|
|
2199
2273
|
return printError(result.error);
|
|
2200
2274
|
}
|
|
@@ -2235,7 +2309,13 @@ function registerTaskImport(parent, container) {
|
|
|
2235
2309
|
);
|
|
2236
2310
|
}
|
|
2237
2311
|
const fieldMapping = opts.map ? parseFieldMapping(opts.map) : void 0;
|
|
2238
|
-
const
|
|
2312
|
+
const projectResult = withProject(container, opts.project);
|
|
2313
|
+
if (!projectResult.ok) return printError(projectResult.error);
|
|
2314
|
+
const result = container.portabilityService.importTasks(
|
|
2315
|
+
fileData,
|
|
2316
|
+
projectResult.value,
|
|
2317
|
+
fieldMapping
|
|
2318
|
+
);
|
|
2239
2319
|
handleResult(result);
|
|
2240
2320
|
});
|
|
2241
2321
|
}
|
|
@@ -2302,6 +2382,8 @@ function buildCLI(container) {
|
|
|
2302
2382
|
registerProjectUpdate(project, container);
|
|
2303
2383
|
registerProjectDelete(project, container);
|
|
2304
2384
|
registerProjectSetDefault(project, container);
|
|
2385
|
+
registerProjectLink(project, container);
|
|
2386
|
+
registerProjectUnlink(project, container);
|
|
2305
2387
|
const task = program.command("task").description("Manage tasks");
|
|
2306
2388
|
registerTaskCreate(task, container);
|
|
2307
2389
|
registerTaskList(task, container);
|
|
@@ -2319,7 +2401,7 @@ function buildCLI(container) {
|
|
|
2319
2401
|
registerDepList(dep, container);
|
|
2320
2402
|
registerDepGraph(dep, container);
|
|
2321
2403
|
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-
|
|
2404
|
+
const { launchTUI } = await import("./tui-IXZGQMWN.js");
|
|
2323
2405
|
await launchTUI(container, opts.project);
|
|
2324
2406
|
});
|
|
2325
2407
|
return program;
|
|
@@ -2335,7 +2417,7 @@ async function main() {
|
|
|
2335
2417
|
const container = createContainer(db, config.dbPath);
|
|
2336
2418
|
const args = process.argv.slice(2);
|
|
2337
2419
|
if (args.length === 0) {
|
|
2338
|
-
const { launchTUI } = await import("./tui-
|
|
2420
|
+
const { launchTUI } = await import("./tui-IXZGQMWN.js");
|
|
2339
2421
|
await launchTUI(container);
|
|
2340
2422
|
} else {
|
|
2341
2423
|
const program = buildCLI(container);
|