@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/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
- } from "./chunk-STYT4TGJ.js";
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, projectIdOrName) {
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
- let resolvedFilter = parsed.data;
1172
- if (parsed.data.projectId) {
1173
- const projectResult = this.projectService.resolveProject(parsed.data.projectId);
1174
- if (!projectResult.ok) return projectResult;
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.resolveProject(parent.projectId);
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, projectIdOrName) {
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 projectRef = projectIdOrName ?? task.projectId;
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, projectIdOrName) {
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
- let projectId;
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, projectService) {
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
- projectService;
1748
- exportTasks(projectIdOrName) {
1796
+ exportTasks(project) {
1749
1797
  return logger.startSpan("PortabilityService.exportTasks", () => {
1750
- const projectResult = this.projectService.resolveProject(projectIdOrName);
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, projectIdOrName, fieldMapping) {
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
- projectIdOrName
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-10 alphanumeric chars, defaults to first 3 chars of name)"
1997
- ).option("-d, --description <description>", "Project description").option("--default", "Set as default project").action((opts) => {
1998
- const result = container.projectService.createProject({
1999
- name: opts.name,
2000
- key: opts.key,
2001
- description: opts.description,
2002
- isDefault: opts.default
2003
- });
2004
- handleResult(result);
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
- opts.project
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 result = container.taskService.listTasks({
2090
- projectId: opts.project,
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
- opts.project
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 result = container.taskService.searchTasks(query, opts.project);
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 result = container.portabilityService.exportTasks(opts.project);
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 result = container.portabilityService.importTasks(fileData, opts.project, fieldMapping);
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-JEP3F4JS.js");
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-JEP3F4JS.js");
2420
+ const { launchTUI } = await import("./tui-IXZGQMWN.js");
2339
2421
  await launchTUI(container);
2340
2422
  } else {
2341
2423
  const program = buildCLI(container);