floq 1.6.0 → 1.8.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/cli.js CHANGED
@@ -6,8 +6,8 @@ import { addTask } from './commands/add.js';
6
6
  import { listTasks, listProjects } from './commands/list.js';
7
7
  import { moveTask } from './commands/move.js';
8
8
  import { markDone } from './commands/done.js';
9
- import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
10
- import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
9
+ import { addProject, listProjectsCommand, showProject, completeProject, deleteProjectCommand, } from './commands/project.js';
10
+ import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, enableTurso, clearTurso, showTursoQr, syncCommand, resetDatabase, setSplashCommand, showSplash, showDateFormatCommand, setDateFormatCommand } from './commands/config.js';
11
11
  import { addComment, listComments } from './commands/comment.js';
12
12
  import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
13
13
  import { showInsights } from './commands/insights.js';
@@ -96,6 +96,16 @@ projectCmd
96
96
  .action(async (id) => {
97
97
  await completeProject(id);
98
98
  });
99
+ projectCmd
100
+ .command('delete <id>')
101
+ .alias('rm')
102
+ .description('Delete a project (prompts how to handle its tasks)')
103
+ .option('--with-tasks', 'Delete the project together with all its tasks')
104
+ .option('--keep-tasks', 'Delete the project but move its tasks to Inbox')
105
+ .option('-f, --force', 'Skip confirmation prompts')
106
+ .action(async (id, options) => {
107
+ await deleteProjectCommand(id, options);
108
+ });
99
109
  // Config commands
100
110
  const configCmd = program
101
111
  .command('config')
@@ -153,8 +163,12 @@ configCmd
153
163
  .option('--disable', 'Temporarily disable Turso sync (preserves config)')
154
164
  .option('--enable', 'Re-enable Turso sync')
155
165
  .option('--clear', 'Remove Turso configuration completely')
166
+ .option('--qr', 'Display Turso config as QR code')
156
167
  .action(async (options) => {
157
- if (options.clear) {
168
+ if (options.qr) {
169
+ await showTursoQr();
170
+ }
171
+ else if (options.clear) {
158
172
  await clearTurso();
159
173
  }
160
174
  else if (options.disable) {
@@ -7,6 +7,7 @@ export declare function selectTheme(): Promise<void>;
7
7
  export declare function showViewMode(): Promise<void>;
8
8
  export declare function setViewModeCommand(mode: string): Promise<void>;
9
9
  export declare function selectMode(): Promise<void>;
10
+ export declare function showTursoQr(): Promise<void>;
10
11
  export declare function setTurso(url: string, token: string): Promise<void>;
11
12
  export declare function disableTurso(): Promise<void>;
12
13
  export declare function enableTurso(): Promise<void>;
@@ -127,6 +127,22 @@ export async function selectMode() {
127
127
  }));
128
128
  });
129
129
  }
130
+ export async function showTursoQr() {
131
+ const turso = getTursoConfig();
132
+ if (!turso || !turso.url || !turso.authToken) {
133
+ console.error('Turso is not configured.');
134
+ console.error('Use "floq config turso --url <url> --token <token>" to configure first.');
135
+ process.exit(1);
136
+ }
137
+ const qrcode = await import('qrcode');
138
+ const data = JSON.stringify({ url: turso.url, authToken: turso.authToken });
139
+ const qr = await qrcode.default.toString(data, { type: 'terminal', small: true });
140
+ console.log('');
141
+ console.log('Turso Configuration QR Code:');
142
+ console.log('');
143
+ console.log(qr);
144
+ console.log('⚠️ This QR code contains your auth token. Do not share publicly.');
145
+ }
130
146
  export async function setTurso(url, token) {
131
147
  setTursoConfig({ url, authToken: token, enabled: true });
132
148
  console.log('Turso sync enabled');
@@ -5,4 +5,10 @@ export declare function addProject(name: string, options: AddProjectOptions): Pr
5
5
  export declare function listProjectsCommand(): Promise<void>;
6
6
  export declare function showProject(projectId: string): Promise<void>;
7
7
  export declare function completeProject(projectId: string): Promise<void>;
8
+ interface DeleteProjectOptions {
9
+ withTasks?: boolean;
10
+ keepTasks?: boolean;
11
+ force?: boolean;
12
+ }
13
+ export declare function deleteProjectCommand(projectId: string, options: DeleteProjectOptions): Promise<void>;
8
14
  export {};
@@ -1,6 +1,8 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
+ import { createInterface } from 'readline';
2
3
  import { eq, like, and } from 'drizzle-orm';
3
4
  import { getDb, schema } from '../db/index.js';
5
+ import { deleteProject } from '../db/projectDelete.js';
4
6
  import { t, fmt } from '../i18n/index.js';
5
7
  export async function addProject(name, options) {
6
8
  const db = getDb();
@@ -150,3 +152,96 @@ export async function completeProject(projectId) {
150
152
  .where(eq(schema.tasks.id, project.id));
151
153
  console.log(fmt(i18n.commands.project.completed, { name: project.title }));
152
154
  }
155
+ function ask(rl, question) {
156
+ return new Promise((resolve) => {
157
+ rl.question(question, (answer) => {
158
+ resolve(answer.trim().toLowerCase());
159
+ });
160
+ });
161
+ }
162
+ export async function deleteProjectCommand(projectId, options) {
163
+ const db = getDb();
164
+ const i18n = t();
165
+ // Find project by ID prefix, then by exact name.
166
+ let projects = await db
167
+ .select()
168
+ .from(schema.tasks)
169
+ .where(and(eq(schema.tasks.isProject, true), like(schema.tasks.id, `${projectId}%`)));
170
+ if (projects.length === 0) {
171
+ projects = await db
172
+ .select()
173
+ .from(schema.tasks)
174
+ .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.title, projectId)));
175
+ }
176
+ if (projects.length === 0) {
177
+ console.error(fmt(i18n.commands.project.notFound, { id: projectId }));
178
+ process.exit(1);
179
+ }
180
+ if (projects.length > 1) {
181
+ console.error(fmt(i18n.commands.project.multipleMatch, { id: projectId }));
182
+ for (const p of projects) {
183
+ console.error(` [${p.id.slice(0, 8)}] ${p.title}`);
184
+ }
185
+ process.exit(1);
186
+ }
187
+ const project = projects[0];
188
+ const children = await db
189
+ .select()
190
+ .from(schema.tasks)
191
+ .where(eq(schema.tasks.parentId, project.id));
192
+ const childCount = children.length;
193
+ // Decide how to handle child tasks. When neither flag is given and the
194
+ // project has tasks, the choice is resolved interactively below.
195
+ let mode = options.withTasks ? 'cascade' :
196
+ options.keepTasks ? 'keep' :
197
+ childCount === 0 ? 'cascade' :
198
+ null;
199
+ if (!options.force) {
200
+ // A single prompt keeps this reliable for piped/non-TTY input.
201
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
202
+ try {
203
+ if (mode !== null) {
204
+ const answer = await ask(rl, `${fmt(i18n.commands.project.deletePrompt, { name: project.title })} (y/N): `);
205
+ if (answer !== 'y' && answer !== 'yes') {
206
+ console.log(i18n.commands.project.deleteCancelled);
207
+ return;
208
+ }
209
+ }
210
+ else {
211
+ const answer = await ask(rl, `${fmt(i18n.commands.project.deleteChildrenPrompt, { name: project.title, count: childCount })}: `);
212
+ if (answer === 'a' || answer === 'all') {
213
+ mode = 'cascade';
214
+ }
215
+ else if (answer === 'k' || answer === 'keep') {
216
+ mode = 'keep';
217
+ }
218
+ else {
219
+ console.log(i18n.commands.project.deleteCancelled);
220
+ return;
221
+ }
222
+ }
223
+ }
224
+ finally {
225
+ rl.close();
226
+ }
227
+ }
228
+ else if (mode === null) {
229
+ // Forced with no explicit choice: keep tasks to avoid silent data loss.
230
+ mode = 'keep';
231
+ }
232
+ await deleteProject(project, mode);
233
+ if (mode === 'cascade') {
234
+ if (childCount > 0) {
235
+ console.log(fmt(i18n.commands.project.deletedWithTasks, { name: project.title, count: childCount }));
236
+ }
237
+ else {
238
+ console.log(fmt(i18n.commands.project.deleted, { name: project.title }));
239
+ }
240
+ }
241
+ else {
242
+ console.log(fmt(i18n.commands.project.deleted, { name: project.title }));
243
+ if (childCount > 0) {
244
+ console.log(fmt(i18n.commands.project.tasksMovedToInbox, { count: childCount }));
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,20 @@
1
+ import type { Task, Comment } from './schema.js';
2
+ /**
3
+ * How to handle child tasks when deleting a project.
4
+ * - `cascade`: delete the project together with all its child tasks (and their comments)
5
+ * - `keep`: delete only the project, moving its child tasks back to Inbox (unlinked)
6
+ */
7
+ export type ProjectDeleteMode = 'cascade' | 'keep';
8
+ export interface ProjectDeleteResult {
9
+ /** Child tasks that were deleted (only populated for `cascade`). */
10
+ deletedTasks: Task[];
11
+ /** Child tasks moved to Inbox, in their original state (only populated for `keep`). */
12
+ movedTasks: Task[];
13
+ /** Comments that were deleted (project's own comments, plus child comments in `cascade`). */
14
+ deletedComments: Comment[];
15
+ }
16
+ /**
17
+ * Delete a project and handle its child tasks according to `mode`.
18
+ * Returns the prior state so the operation can be undone.
19
+ */
20
+ export declare function deleteProject(project: Task, mode: ProjectDeleteMode): Promise<ProjectDeleteResult>;
@@ -0,0 +1,49 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb, schema } from './index.js';
3
+ /**
4
+ * Delete a project and handle its child tasks according to `mode`.
5
+ * Returns the prior state so the operation can be undone.
6
+ */
7
+ export async function deleteProject(project, mode) {
8
+ const db = getDb();
9
+ const children = await db
10
+ .select()
11
+ .from(schema.tasks)
12
+ .where(eq(schema.tasks.parentId, project.id));
13
+ const deletedTasks = [];
14
+ const movedTasks = [];
15
+ let deletedComments = [];
16
+ // Always remove the project's own comments.
17
+ const projectComments = await db
18
+ .select()
19
+ .from(schema.comments)
20
+ .where(eq(schema.comments.taskId, project.id));
21
+ deletedComments = deletedComments.concat(projectComments);
22
+ await db.delete(schema.comments).where(eq(schema.comments.taskId, project.id));
23
+ if (mode === 'cascade') {
24
+ for (const child of children) {
25
+ const childComments = await db
26
+ .select()
27
+ .from(schema.comments)
28
+ .where(eq(schema.comments.taskId, child.id));
29
+ deletedComments = deletedComments.concat(childComments);
30
+ await db.delete(schema.comments).where(eq(schema.comments.taskId, child.id));
31
+ await db.delete(schema.tasks).where(eq(schema.tasks.id, child.id));
32
+ deletedTasks.push(child);
33
+ }
34
+ }
35
+ else {
36
+ // keep: unlink children and move them back to Inbox.
37
+ const now = new Date();
38
+ for (const child of children) {
39
+ movedTasks.push(child);
40
+ await db
41
+ .update(schema.tasks)
42
+ .set({ parentId: null, status: 'inbox', updatedAt: now })
43
+ .where(eq(schema.tasks.id, child.id));
44
+ }
45
+ }
46
+ // Finally remove the project itself.
47
+ await db.delete(schema.tasks).where(eq(schema.tasks.id, project.id));
48
+ return { deletedTasks, movedTasks, deletedComments };
49
+ }
package/dist/i18n/en.d.ts CHANGED
@@ -51,6 +51,12 @@ export declare const en: {
51
51
  notFound: string;
52
52
  multipleMatch: string;
53
53
  completed: string;
54
+ deleted: string;
55
+ deletedWithTasks: string;
56
+ tasksMovedToInbox: string;
57
+ deletePrompt: string;
58
+ deleteChildrenPrompt: string;
59
+ deleteCancelled: string;
54
60
  noProjects: string;
55
61
  description: string;
56
62
  statusLabel: string;
@@ -247,6 +253,11 @@ export declare const en: {
247
253
  deleteConfirm: string;
248
254
  deleted: string;
249
255
  deleteCancelled: string;
256
+ deleteProjectConfirm: string;
257
+ deleteProjectChildren: string;
258
+ deletedProject: string;
259
+ deletedProjectWithTasks: string;
260
+ projectTasksToInbox: string;
250
261
  undone: string;
251
262
  redone: string;
252
263
  nothingToUndo: string;
@@ -675,6 +686,11 @@ export type TuiTranslations = {
675
686
  deleteConfirm: string;
676
687
  deleted: string;
677
688
  deleteCancelled: string;
689
+ deleteProjectConfirm?: string;
690
+ deleteProjectChildren?: string;
691
+ deletedProject?: string;
692
+ deletedProjectWithTasks?: string;
693
+ projectTasksToInbox?: string;
678
694
  undone: string;
679
695
  redone: string;
680
696
  nothingToUndo: string;
package/dist/i18n/en.js CHANGED
@@ -55,6 +55,12 @@ export const en = {
55
55
  notFound: 'Project not found: {id}',
56
56
  multipleMatch: 'Multiple projects match "{id}". Please be more specific.',
57
57
  completed: 'Completed project: "{name}"',
58
+ deleted: 'Deleted project: "{name}"',
59
+ deletedWithTasks: 'Deleted project "{name}" and {count} task(s)',
60
+ tasksMovedToInbox: 'Moved {count} task(s) to Inbox',
61
+ deletePrompt: 'Delete project "{name}"?',
62
+ deleteChildrenPrompt: 'Project "{name}" has {count} task(s). a=delete all k=keep tasks (→Inbox) n=cancel',
63
+ deleteCancelled: 'Cancelled.',
58
64
  noProjects: 'No projects',
59
65
  description: 'Description: {description}',
60
66
  statusLabel: 'Status: {status}',
@@ -260,6 +266,11 @@ export const en = {
260
266
  deleteConfirm: 'Delete "{title}"? (y/n)',
261
267
  deleted: 'Deleted: "{title}"',
262
268
  deleteCancelled: 'Delete cancelled',
269
+ deleteProjectConfirm: 'Delete project "{title}"? (y/n)',
270
+ deleteProjectChildren: 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel',
271
+ deletedProject: 'Deleted project: "{title}"',
272
+ deletedProjectWithTasks: 'Deleted project "{title}" and {count} task(s)',
273
+ projectTasksToInbox: 'Deleted project "{title}", moved {count} task(s) to Inbox',
263
274
  // Undo/Redo
264
275
  undone: 'Undone: {action}',
265
276
  redone: 'Redone: {action}',
package/dist/i18n/ja.js CHANGED
@@ -55,6 +55,12 @@ export const ja = {
55
55
  notFound: 'プロジェクトが見つかりません: {id}',
56
56
  multipleMatch: '「{id}」に一致するプロジェクトが複数あります。より具体的に指定してください。',
57
57
  completed: 'プロジェクトを完了しました: 「{name}」',
58
+ deleted: 'プロジェクトを削除しました: 「{name}」',
59
+ deletedWithTasks: 'プロジェクト「{name}」とタスク{count}件を削除しました',
60
+ tasksMovedToInbox: 'タスク{count}件をInboxへ移動しました',
61
+ deletePrompt: 'プロジェクト「{name}」を削除しますか?',
62
+ deleteChildrenPrompt: 'プロジェクト「{name}」にはタスクが{count}件あります。 a=すべて削除 k=タスクを残す(→Inbox) n=キャンセル',
63
+ deleteCancelled: 'キャンセルしました。',
58
64
  noProjects: 'プロジェクトなし',
59
65
  description: '説明: {description}',
60
66
  statusLabel: 'ステータス: {status}',
@@ -260,6 +266,11 @@ export const ja = {
260
266
  deleteConfirm: '「{title}」を削除しますか? (y/n)',
261
267
  deleted: '削除しました: 「{title}」',
262
268
  deleteCancelled: '削除をキャンセルしました',
269
+ deleteProjectConfirm: 'プロジェクト「{title}」を削除しますか? (y/n)',
270
+ deleteProjectChildren: 'プロジェクト「{title}」にはタスクが{count}件あります。 t=すべて削除 i=タスクを残す(→Inbox) n=キャンセル',
271
+ deletedProject: 'プロジェクトを削除しました: 「{title}」',
272
+ deletedProjectWithTasks: 'プロジェクト「{title}」とタスク{count}件を削除しました',
273
+ projectTasksToInbox: 'プロジェクト「{title}」を削除し、タスク{count}件をInboxへ移動しました',
263
274
  // Undo/Redo
264
275
  undone: '元に戻しました: {action}',
265
276
  redone: 'やり直しました: {action}',
package/dist/ui/App.js CHANGED
@@ -30,7 +30,7 @@ import { KanbanMario } from './components/KanbanMario.js';
30
30
  import { GtdDQ } from './components/GtdDQ.js';
31
31
  import { GtdMario } from './components/GtdMario.js';
32
32
  import { VERSION } from '../version.js';
33
- import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './history/index.js';
33
+ import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './history/index.js';
34
34
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
35
35
  export function App() {
36
36
  const [themeName, setThemeNameState] = useState(getThemeName);
@@ -102,6 +102,8 @@ function AppContent({ onOpenSettings }) {
102
102
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
103
103
  const [taskToWaiting, setTaskToWaiting] = useState(null);
104
104
  const [taskToDelete, setTaskToDelete] = useState(null);
105
+ const [projectToDelete, setProjectToDelete] = useState(null);
106
+ const [projectChildCount, setProjectChildCount] = useState(0);
105
107
  const [projectProgress, setProjectProgress] = useState({});
106
108
  // Search state
107
109
  const [searchQuery, setSearchQuery] = useState('');
@@ -517,6 +519,19 @@ function AppContent({ onOpenSettings }) {
517
519
  setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
518
520
  await loadTasks();
519
521
  }, [i18n.tui.deleted, loadTasks, history]);
522
+ const deleteProjectAction = useCallback(async (project, mode, childCount) => {
523
+ const message = mode === 'cascade'
524
+ ? (childCount > 0
525
+ ? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
526
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
527
+ : (childCount > 0
528
+ ? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
529
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
530
+ const command = new DeleteProjectCommand({ project, mode, description: message });
531
+ await history.execute(command);
532
+ setMessage(message);
533
+ await loadTasks();
534
+ }, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
520
535
  const getTabLabel = (tab) => {
521
536
  switch (tab) {
522
537
  case 'inbox':
@@ -576,6 +591,51 @@ function AppContent({ onOpenSettings }) {
576
591
  // Ignore other keys in confirm mode
577
592
  return;
578
593
  }
594
+ // Handle confirm-delete-project mode
595
+ if (mode === 'confirm-delete-project' && projectToDelete) {
596
+ const cancel = () => {
597
+ setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
598
+ setProjectToDelete(null);
599
+ setMode('normal');
600
+ };
601
+ const runDelete = (deleteMode) => {
602
+ const project = projectToDelete;
603
+ const count = projectChildCount;
604
+ deleteProjectAction(project, deleteMode, count).then(() => {
605
+ if (selectedTaskIndex >= currentTasks.length - 1) {
606
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
607
+ }
608
+ });
609
+ setProjectToDelete(null);
610
+ setMode('normal');
611
+ };
612
+ if (projectChildCount > 0) {
613
+ if (input === 't' || input === 'T') {
614
+ runDelete('cascade');
615
+ return;
616
+ }
617
+ if (input === 'i' || input === 'I') {
618
+ runDelete('keep');
619
+ return;
620
+ }
621
+ if (input === 'n' || input === 'N' || key.escape) {
622
+ cancel();
623
+ return;
624
+ }
625
+ }
626
+ else {
627
+ if (input === 'y' || input === 'Y') {
628
+ runDelete('cascade');
629
+ return;
630
+ }
631
+ if (input === 'n' || input === 'N' || key.escape) {
632
+ cancel();
633
+ return;
634
+ }
635
+ }
636
+ // Ignore other keys in confirm mode
637
+ return;
638
+ }
579
639
  // Handle add mode
580
640
  if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting') {
581
641
  if (key.escape) {
@@ -1077,6 +1137,20 @@ function AppContent({ onOpenSettings }) {
1077
1137
  });
1078
1138
  return;
1079
1139
  }
1140
+ // Delete project (D key on projects tab - with confirmation)
1141
+ if (input === 'D' && currentTasks.length > 0 && currentTab === 'projects') {
1142
+ const project = currentTasks[selectedTaskIndex];
1143
+ const db = getDb();
1144
+ db.select()
1145
+ .from(schema.tasks)
1146
+ .where(eq(schema.tasks.parentId, project.id))
1147
+ .then((children) => {
1148
+ setProjectChildCount(children.length);
1149
+ setProjectToDelete(project);
1150
+ setMode('confirm-delete-project');
1151
+ });
1152
+ return;
1153
+ }
1080
1154
  // Delete task (D key - with confirmation)
1081
1155
  if (input === 'D' && currentTasks.length > 0 && currentTab !== 'projects') {
1082
1156
  const task = currentTasks[selectedTaskIndex];
@@ -1224,7 +1298,9 @@ function AppContent({ onOpenSettings }) {
1224
1298
  const isActive = (ctx === 'clear' && !currentContext) ||
1225
1299
  (ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
1226
1300
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
1227
- }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-effort' && (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: i18n.tui.effort?.set || 'Set effort' }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, option.label] }, option.label))) })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [(mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
1301
+ }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-effort' && (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: i18n.tui.effort?.set || 'Set effort' }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, option.label] }, option.label))) })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), mode === 'confirm-delete-project' && projectToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
1302
+ ? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
1303
+ : fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [(mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
1228
1304
  { key: 'i', label: i18n.tui.keyBar.comment },
1229
1305
  { key: 'd', label: i18n.tui.keyBar.delete },
1230
1306
  { key: 'P', label: i18n.tui.keyBar.project },
@@ -8,7 +8,7 @@ import { getDb, schema } from '../../db/index.js';
8
8
  import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
10
  import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../../config.js';
11
- import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
11
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
12
12
  import { SearchBar } from './SearchBar.js';
13
13
  import { SearchResults } from './SearchResults.js';
14
14
  import { HelpModal } from './HelpModal.js';
@@ -145,6 +145,8 @@ export function GtdDQ({ onOpenSettings }) {
145
145
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
146
146
  const [taskToWaiting, setTaskToWaiting] = useState(null);
147
147
  const [taskToDelete, setTaskToDelete] = useState(null);
148
+ const [projectToDelete, setProjectToDelete] = useState(null);
149
+ const [projectChildCount, setProjectChildCount] = useState(0);
148
150
  const [projectProgress, setProjectProgress] = useState({});
149
151
  // Context filter state - load from config for persistence across sessions/terminals
150
152
  const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
@@ -401,6 +403,19 @@ export function GtdDQ({ onOpenSettings }) {
401
403
  setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
402
404
  await loadTasks();
403
405
  }, [i18n.tui.deleted, loadTasks, history]);
406
+ const deleteProjectAction = useCallback(async (project, mode, childCount) => {
407
+ const message = mode === 'cascade'
408
+ ? (childCount > 0
409
+ ? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
410
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
411
+ : (childCount > 0
412
+ ? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
413
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
414
+ const command = new DeleteProjectCommand({ project, mode, description: message });
415
+ await history.execute(command);
416
+ setMessage(message);
417
+ await loadTasks();
418
+ }, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
404
419
  const linkTaskToProject = useCallback(async (task, project) => {
405
420
  const command = new LinkTaskCommand({
406
421
  taskId: task.id,
@@ -738,6 +753,50 @@ export function GtdDQ({ onOpenSettings }) {
738
753
  }
739
754
  return;
740
755
  }
756
+ // Handle confirm-delete-project
757
+ if (mode === 'confirm-delete-project' && projectToDelete) {
758
+ const cancel = () => {
759
+ setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
760
+ setProjectToDelete(null);
761
+ setMode('normal');
762
+ };
763
+ const runDelete = (deleteMode) => {
764
+ const project = projectToDelete;
765
+ const count = projectChildCount;
766
+ deleteProjectAction(project, deleteMode, count).then(() => {
767
+ if (selectedTaskIndex >= currentTasks.length - 1) {
768
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
769
+ }
770
+ });
771
+ setProjectToDelete(null);
772
+ setMode('normal');
773
+ };
774
+ if (projectChildCount > 0) {
775
+ if (input === 't' || input === 'T') {
776
+ runDelete('cascade');
777
+ return;
778
+ }
779
+ if (input === 'i' || input === 'I') {
780
+ runDelete('keep');
781
+ return;
782
+ }
783
+ if (input === 'n' || input === 'N' || key.escape) {
784
+ cancel();
785
+ return;
786
+ }
787
+ }
788
+ else {
789
+ if (input === 'y' || input === 'Y') {
790
+ runDelete('cascade');
791
+ return;
792
+ }
793
+ if (input === 'n' || input === 'N' || key.escape) {
794
+ cancel();
795
+ return;
796
+ }
797
+ }
798
+ return;
799
+ }
741
800
  // Handle context-filter mode
742
801
  if (mode === 'context-filter') {
743
802
  if (key.escape) {
@@ -1052,6 +1111,20 @@ export function GtdDQ({ onOpenSettings }) {
1052
1111
  setMode('select-project');
1053
1112
  return;
1054
1113
  }
1114
+ // Delete project (on projects tab)
1115
+ if (input === 'D' && currentTab === 'projects') {
1116
+ const project = task;
1117
+ const db = getDb();
1118
+ db.select()
1119
+ .from(schema.tasks)
1120
+ .where(eq(schema.tasks.parentId, project.id))
1121
+ .then((children) => {
1122
+ setProjectChildCount(children.length);
1123
+ setProjectToDelete(project);
1124
+ setMode('confirm-delete-project');
1125
+ });
1126
+ return;
1127
+ }
1055
1128
  // Delete
1056
1129
  if (input === 'D' && currentTab !== 'projects') {
1057
1130
  setTaskToDelete(task);
@@ -1179,7 +1252,9 @@ export function GtdDQ({ onOpenSettings }) {
1179
1252
  }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
1180
1253
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1181
1254
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
1182
- }) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1255
+ }) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : mode === 'confirm-delete-project' && projectToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
1256
+ ? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
1257
+ : fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1183
1258
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1184
1259
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '▶ ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
1185
1260
  })) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
@@ -9,7 +9,7 @@ import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
10
  import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../../config.js';
11
11
  import { VERSION } from '../../version.js';
12
- import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
12
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
13
13
  import { SearchBar } from './SearchBar.js';
14
14
  import { SearchResults } from './SearchResults.js';
15
15
  import { HelpModal } from './HelpModal.js';
@@ -87,6 +87,8 @@ export function GtdMario({ onOpenSettings }) {
87
87
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
88
88
  const [taskToWaiting, setTaskToWaiting] = useState(null);
89
89
  const [taskToDelete, setTaskToDelete] = useState(null);
90
+ const [projectToDelete, setProjectToDelete] = useState(null);
91
+ const [projectChildCount, setProjectChildCount] = useState(0);
90
92
  const [projectProgress, setProjectProgress] = useState({});
91
93
  // Context filter state - load from config for persistence across sessions/terminals
92
94
  const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
@@ -347,6 +349,19 @@ export function GtdMario({ onOpenSettings }) {
347
349
  setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
348
350
  await loadTasks();
349
351
  }, [i18n.tui.deleted, loadTasks, history]);
352
+ const deleteProjectAction = useCallback(async (project, mode, childCount) => {
353
+ const message = mode === 'cascade'
354
+ ? (childCount > 0
355
+ ? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
356
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
357
+ : (childCount > 0
358
+ ? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
359
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
360
+ const command = new DeleteProjectCommand({ project, mode, description: message });
361
+ await history.execute(command);
362
+ setMessage(message);
363
+ await loadTasks();
364
+ }, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
350
365
  const linkTaskToProject = useCallback(async (task, project) => {
351
366
  const command = new LinkTaskCommand({
352
367
  taskId: task.id,
@@ -653,6 +668,49 @@ export function GtdMario({ onOpenSettings }) {
653
668
  }
654
669
  return;
655
670
  }
671
+ if (mode === 'confirm-delete-project' && projectToDelete) {
672
+ const cancel = () => {
673
+ setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
674
+ setProjectToDelete(null);
675
+ setMode('normal');
676
+ };
677
+ const runDelete = (deleteMode) => {
678
+ const project = projectToDelete;
679
+ const count = projectChildCount;
680
+ deleteProjectAction(project, deleteMode, count).then(() => {
681
+ if (selectedTaskIndex >= currentTasks.length - 1) {
682
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
683
+ }
684
+ });
685
+ setProjectToDelete(null);
686
+ setMode('normal');
687
+ };
688
+ if (projectChildCount > 0) {
689
+ if (input === 't' || input === 'T') {
690
+ runDelete('cascade');
691
+ return;
692
+ }
693
+ if (input === 'i' || input === 'I') {
694
+ runDelete('keep');
695
+ return;
696
+ }
697
+ if (input === 'n' || input === 'N' || key.escape) {
698
+ cancel();
699
+ return;
700
+ }
701
+ }
702
+ else {
703
+ if (input === 'y' || input === 'Y') {
704
+ runDelete('cascade');
705
+ return;
706
+ }
707
+ if (input === 'n' || input === 'N' || key.escape) {
708
+ cancel();
709
+ return;
710
+ }
711
+ }
712
+ return;
713
+ }
656
714
  if (mode === 'context-filter') {
657
715
  if (key.escape) {
658
716
  setContextSelectIndex(0);
@@ -950,6 +1008,20 @@ export function GtdMario({ onOpenSettings }) {
950
1008
  setMode('select-project');
951
1009
  return;
952
1010
  }
1011
+ // Delete project (on projects tab)
1012
+ if (input === 'D' && currentTab === 'projects') {
1013
+ const project = task;
1014
+ const db = getDb();
1015
+ db.select()
1016
+ .from(schema.tasks)
1017
+ .where(eq(schema.tasks.parentId, project.id))
1018
+ .then((children) => {
1019
+ setProjectChildCount(children.length);
1020
+ setProjectToDelete(project);
1021
+ setMode('confirm-delete-project');
1022
+ });
1023
+ return;
1024
+ }
953
1025
  if (input === 'D' && currentTab !== 'projects') {
954
1026
  setTaskToDelete(task);
955
1027
  setMode('confirm-delete');
@@ -1073,7 +1145,9 @@ export function GtdMario({ onOpenSettings }) {
1073
1145
  }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
1074
1146
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1075
1147
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
1076
- }) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1148
+ }) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : mode === 'confirm-delete-project' && projectToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
1149
+ ? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
1150
+ : fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1077
1151
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1078
1152
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '🍄 ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
1079
1153
  })) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(MarioBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'STAGE', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
@@ -0,0 +1,31 @@
1
+ import { type ProjectDeleteMode } from '../../../db/projectDelete.js';
2
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
3
+ import type { Task, Comment } from '../../../db/schema.js';
4
+ interface DeleteProjectParams {
5
+ project: Task;
6
+ mode: ProjectDeleteMode;
7
+ description: string;
8
+ deletedTasks?: Task[];
9
+ movedTasks?: Task[];
10
+ deletedComments?: Comment[];
11
+ }
12
+ /**
13
+ * Command to delete a project, handling its child tasks via `mode`
14
+ * (cascade = delete children too, keep = move children back to Inbox).
15
+ */
16
+ export declare class DeleteProjectCommand implements UndoableCommand {
17
+ readonly description: string;
18
+ private readonly project;
19
+ private readonly mode;
20
+ private deletedTasks;
21
+ private movedTasks;
22
+ private deletedComments;
23
+ constructor(params: DeleteProjectParams);
24
+ execute(): Promise<void>;
25
+ undo(): Promise<void>;
26
+ toJSON(): SerializedCommand;
27
+ static fromJSON(json: {
28
+ data: Record<string, unknown>;
29
+ }): DeleteProjectCommand;
30
+ }
31
+ export {};
@@ -0,0 +1,133 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb, schema } from '../../../db/index.js';
3
+ import { deleteProject } from '../../../db/projectDelete.js';
4
+ function taskToValues(task) {
5
+ return {
6
+ id: task.id,
7
+ title: task.title,
8
+ description: task.description,
9
+ status: task.status,
10
+ isProject: task.isProject,
11
+ parentId: task.parentId,
12
+ waitingFor: task.waitingFor,
13
+ context: task.context,
14
+ isFocused: task.isFocused,
15
+ effort: task.effort,
16
+ dueDate: task.dueDate,
17
+ completedAt: task.completedAt,
18
+ createdAt: task.createdAt,
19
+ updatedAt: task.updatedAt,
20
+ };
21
+ }
22
+ function serializeTask(task) {
23
+ return {
24
+ ...task,
25
+ dueDate: task.dueDate ? task.dueDate.toISOString() : null,
26
+ completedAt: task.completedAt ? task.completedAt.toISOString() : null,
27
+ createdAt: task.createdAt.toISOString(),
28
+ updatedAt: task.updatedAt.toISOString(),
29
+ };
30
+ }
31
+ function deserializeTask(data) {
32
+ return {
33
+ ...data,
34
+ dueDate: data.dueDate ? new Date(data.dueDate) : null,
35
+ completedAt: data.completedAt ? new Date(data.completedAt) : null,
36
+ createdAt: new Date(data.createdAt),
37
+ updatedAt: new Date(data.updatedAt),
38
+ };
39
+ }
40
+ /**
41
+ * Command to delete a project, handling its child tasks via `mode`
42
+ * (cascade = delete children too, keep = move children back to Inbox).
43
+ */
44
+ export class DeleteProjectCommand {
45
+ description;
46
+ project;
47
+ mode;
48
+ deletedTasks = [];
49
+ movedTasks = [];
50
+ deletedComments = [];
51
+ constructor(params) {
52
+ this.project = params.project;
53
+ this.mode = params.mode;
54
+ this.description = params.description;
55
+ if (params.deletedTasks)
56
+ this.deletedTasks = params.deletedTasks;
57
+ if (params.movedTasks)
58
+ this.movedTasks = params.movedTasks;
59
+ if (params.deletedComments)
60
+ this.deletedComments = params.deletedComments;
61
+ }
62
+ async execute() {
63
+ const result = await deleteProject(this.project, this.mode);
64
+ this.deletedTasks = result.deletedTasks;
65
+ this.movedTasks = result.movedTasks;
66
+ this.deletedComments = result.deletedComments;
67
+ }
68
+ async undo() {
69
+ const db = getDb();
70
+ // Restore the project.
71
+ await db.insert(schema.tasks).values(taskToValues(this.project));
72
+ if (this.mode === 'cascade') {
73
+ // Re-create deleted child tasks.
74
+ for (const task of this.deletedTasks) {
75
+ await db.insert(schema.tasks).values(taskToValues(task));
76
+ }
77
+ }
78
+ else {
79
+ // Re-link moved children and restore their original status.
80
+ for (const task of this.movedTasks) {
81
+ await db
82
+ .update(schema.tasks)
83
+ .set({
84
+ parentId: task.parentId,
85
+ status: task.status,
86
+ updatedAt: task.updatedAt,
87
+ })
88
+ .where(eq(schema.tasks.id, task.id));
89
+ }
90
+ }
91
+ // Restore deleted comments.
92
+ for (const comment of this.deletedComments) {
93
+ await db.insert(schema.comments).values({
94
+ id: comment.id,
95
+ taskId: comment.taskId,
96
+ content: comment.content,
97
+ createdAt: comment.createdAt,
98
+ });
99
+ }
100
+ }
101
+ toJSON() {
102
+ return {
103
+ type: 'delete_project',
104
+ data: {
105
+ project: serializeTask(this.project),
106
+ mode: this.mode,
107
+ deletedTasks: this.deletedTasks.map(serializeTask),
108
+ movedTasks: this.movedTasks.map(serializeTask),
109
+ deletedComments: this.deletedComments.map((c) => ({
110
+ ...c,
111
+ createdAt: c.createdAt.toISOString(),
112
+ })),
113
+ description: this.description,
114
+ },
115
+ };
116
+ }
117
+ static fromJSON(json) {
118
+ const deletedTasksData = json.data.deletedTasks || [];
119
+ const movedTasksData = json.data.movedTasks || [];
120
+ const commentsData = json.data.deletedComments || [];
121
+ return new DeleteProjectCommand({
122
+ project: deserializeTask(json.data.project),
123
+ mode: json.data.mode,
124
+ deletedTasks: deletedTasksData.map(deserializeTask),
125
+ movedTasks: movedTasksData.map(deserializeTask),
126
+ deletedComments: commentsData.map((c) => ({
127
+ ...c,
128
+ createdAt: new Date(c.createdAt),
129
+ })),
130
+ description: json.data.description,
131
+ });
132
+ }
133
+ }
@@ -1,5 +1,6 @@
1
1
  export { CreateTaskCommand } from './CreateTaskCommand.js';
2
2
  export { DeleteTaskCommand } from './DeleteTaskCommand.js';
3
+ export { DeleteProjectCommand } from './DeleteProjectCommand.js';
3
4
  export { MoveTaskCommand } from './MoveTaskCommand.js';
4
5
  export { LinkTaskCommand } from './LinkTaskCommand.js';
5
6
  export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
@@ -1,5 +1,6 @@
1
1
  export { CreateTaskCommand } from './CreateTaskCommand.js';
2
2
  export { DeleteTaskCommand } from './DeleteTaskCommand.js';
3
+ export { DeleteProjectCommand } from './DeleteProjectCommand.js';
3
4
  export { MoveTaskCommand } from './MoveTaskCommand.js';
4
5
  export { LinkTaskCommand } from './LinkTaskCommand.js';
5
6
  export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
@@ -1,5 +1,6 @@
1
1
  import { CreateTaskCommand } from './CreateTaskCommand.js';
2
2
  import { DeleteTaskCommand } from './DeleteTaskCommand.js';
3
+ import { DeleteProjectCommand } from './DeleteProjectCommand.js';
3
4
  import { MoveTaskCommand } from './MoveTaskCommand.js';
4
5
  import { LinkTaskCommand } from './LinkTaskCommand.js';
5
6
  import { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
@@ -11,6 +12,7 @@ import { SetEffortCommand } from './SetEffortCommand.js';
11
12
  const registry = {
12
13
  'create_task': (data) => CreateTaskCommand.fromJSON({ data }),
13
14
  'delete_task': (data) => DeleteTaskCommand.fromJSON({ data }),
15
+ 'delete_project': (data) => DeleteProjectCommand.fromJSON({ data }),
14
16
  'move_task': (data) => MoveTaskCommand.fromJSON({ data }),
15
17
  'link_task': (data) => LinkTaskCommand.fromJSON({ data }),
16
18
  'convert_to_project': (data) => ConvertToProjectCommand.fromJSON({ data }),
@@ -3,4 +3,4 @@ export { MAX_HISTORY_SIZE } from './types.js';
3
3
  export { HistoryManager, getHistoryManager } from './HistoryManager.js';
4
4
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
5
5
  export { useHistory } from './useHistory.js';
6
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
6
+ export { CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
@@ -5,4 +5,4 @@ export { HistoryManager, getHistoryManager } from './HistoryManager.js';
5
5
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
6
6
  export { useHistory } from './useHistory.js';
7
7
  // Commands
8
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
8
+ export { CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,24 +39,25 @@
39
39
  "dependencies": {
40
40
  "@inkjs/ui": "^2.0.0",
41
41
  "@libsql/client": "^0.17.0",
42
- "@modelcontextprotocol/sdk": "^1.27.0",
42
+ "@modelcontextprotocol/sdk": "^1.29.0",
43
43
  "better-sqlite3": "^11.7.0",
44
44
  "commander": "^13.1.0",
45
- "drizzle-orm": "^0.39.1",
46
- "googleapis": "^144.0.0",
45
+ "drizzle-orm": "^0.45.2",
47
46
  "ical.js": "^2.1.0",
48
47
  "ink": "^5.1.0",
49
48
  "ink-text-input": "^6.0.0",
50
49
  "open": "^10.1.0",
50
+ "qrcode": "^1.5.4",
51
51
  "react": "^18.3.1",
52
- "uuid": "^11.0.5"
52
+ "uuid": "^14.0.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/better-sqlite3": "^7.6.12",
56
56
  "@types/node": "^22.10.7",
57
+ "@types/qrcode": "^1.5.6",
57
58
  "@types/react": "^18.3.18",
58
59
  "@types/uuid": "^10.0.0",
59
- "drizzle-kit": "^0.31.8",
60
+ "drizzle-kit": "^0.31.10",
60
61
  "typescript": "^5.7.3"
61
62
  },
62
63
  "overrides": {