floq 0.0.1 → 0.2.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.
Files changed (55) hide show
  1. package/README.ja.md +157 -29
  2. package/README.md +157 -29
  3. package/dist/changelog.d.ts +13 -0
  4. package/dist/changelog.js +95 -0
  5. package/dist/cli.js +70 -1
  6. package/dist/commands/add.js +5 -6
  7. package/dist/commands/comment.d.ts +2 -0
  8. package/dist/commands/comment.js +67 -0
  9. package/dist/commands/config.d.ts +7 -0
  10. package/dist/commands/config.js +163 -14
  11. package/dist/commands/done.js +4 -6
  12. package/dist/commands/list.js +9 -13
  13. package/dist/commands/move.js +4 -6
  14. package/dist/commands/project.js +18 -26
  15. package/dist/commands/setup.d.ts +1 -0
  16. package/dist/commands/setup.js +13 -0
  17. package/dist/config.d.ts +15 -1
  18. package/dist/config.js +53 -2
  19. package/dist/db/index.d.ts +5 -4
  20. package/dist/db/index.js +127 -32
  21. package/dist/db/schema.d.ts +83 -0
  22. package/dist/db/schema.js +6 -0
  23. package/dist/i18n/en.d.ts +258 -0
  24. package/dist/i18n/en.js +138 -3
  25. package/dist/i18n/ja.js +138 -3
  26. package/dist/index.js +33 -1
  27. package/dist/paths.d.ts +4 -0
  28. package/dist/paths.js +63 -5
  29. package/dist/ui/App.js +384 -136
  30. package/dist/ui/ModeSelector.d.ts +7 -0
  31. package/dist/ui/ModeSelector.js +37 -0
  32. package/dist/ui/SetupWizard.d.ts +6 -0
  33. package/dist/ui/SetupWizard.js +321 -0
  34. package/dist/ui/ThemeSelector.d.ts +1 -1
  35. package/dist/ui/ThemeSelector.js +23 -10
  36. package/dist/ui/components/FunctionKeyBar.d.ts +5 -4
  37. package/dist/ui/components/FunctionKeyBar.js +19 -15
  38. package/dist/ui/components/HelpModal.d.ts +2 -1
  39. package/dist/ui/components/HelpModal.js +118 -4
  40. package/dist/ui/components/KanbanBoard.d.ts +6 -0
  41. package/dist/ui/components/KanbanBoard.js +508 -0
  42. package/dist/ui/components/KanbanColumn.d.ts +12 -0
  43. package/dist/ui/components/KanbanColumn.js +11 -0
  44. package/dist/ui/components/ProgressBar.d.ts +7 -0
  45. package/dist/ui/components/ProgressBar.js +13 -0
  46. package/dist/ui/components/SearchBar.d.ts +8 -0
  47. package/dist/ui/components/SearchBar.js +11 -0
  48. package/dist/ui/components/SearchResults.d.ts +9 -0
  49. package/dist/ui/components/SearchResults.js +18 -0
  50. package/dist/ui/components/TaskItem.d.ts +6 -1
  51. package/dist/ui/components/TaskItem.js +3 -2
  52. package/dist/ui/theme/themes.d.ts +12 -0
  53. package/dist/ui/theme/themes.js +495 -3
  54. package/dist/ui/theme/types.d.ts +1 -1
  55. package/package.json +2 -1
package/dist/config.js CHANGED
@@ -1,9 +1,37 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
2
2
  import { dirname, join, isAbsolute } from 'path';
3
3
  import { CONFIG_FILE, DATA_DIR } from './paths.js';
4
+ // Migrate legacy DB file names (including related metadata files)
5
+ function migrateDbFiles() {
6
+ const legacyDb = join(DATA_DIR, 'gtd.db');
7
+ const newDb = join(DATA_DIR, 'floq.db');
8
+ const legacyTursoDb = join(DATA_DIR, 'gtd-turso.db');
9
+ const newTursoDb = join(DATA_DIR, 'floq-turso.db');
10
+ // Turso/libsql related file suffixes
11
+ const tursoSuffixes = ['', '-info', '-shm', '-wal'];
12
+ try {
13
+ if (existsSync(legacyDb) && !existsSync(newDb)) {
14
+ renameSync(legacyDb, newDb);
15
+ }
16
+ // Migrate Turso DB and all related metadata files
17
+ for (const suffix of tursoSuffixes) {
18
+ const legacyFile = legacyTursoDb + suffix;
19
+ const newFile = newTursoDb + suffix;
20
+ if (existsSync(legacyFile) && !existsSync(newFile)) {
21
+ renameSync(legacyFile, newFile);
22
+ }
23
+ }
24
+ }
25
+ catch {
26
+ // Ignore migration errors
27
+ }
28
+ }
29
+ // Run DB file migration on module load
30
+ migrateDbFiles();
4
31
  const DEFAULT_CONFIG = {
5
32
  locale: 'en',
6
33
  theme: 'modern',
34
+ viewMode: 'gtd',
7
35
  };
8
36
  let configCache = null;
9
37
  export function loadConfig() {
@@ -39,6 +67,16 @@ export function saveConfig(updates) {
39
67
  // Ignore errors
40
68
  }
41
69
  }
70
+ export function getTursoConfig() {
71
+ return loadConfig().turso;
72
+ }
73
+ export function setTursoConfig(config) {
74
+ saveConfig({ turso: config });
75
+ }
76
+ export function isTursoEnabled() {
77
+ const turso = getTursoConfig();
78
+ return turso !== undefined && turso.url !== '' && turso.authToken !== '';
79
+ }
42
80
  export function getDbPath() {
43
81
  const config = loadConfig();
44
82
  if (config.db_path) {
@@ -48,7 +86,11 @@ export function getDbPath() {
48
86
  }
49
87
  return join(DATA_DIR, config.db_path);
50
88
  }
51
- return join(DATA_DIR, 'gtd.db');
89
+ // Turso モードでは別のDBファイルを使用(embedded replica 用)
90
+ if (isTursoEnabled()) {
91
+ return join(DATA_DIR, 'floq-turso.db');
92
+ }
93
+ return join(DATA_DIR, 'floq.db');
52
94
  }
53
95
  export function getLocale() {
54
96
  return loadConfig().locale;
@@ -62,3 +104,12 @@ export function getThemeName() {
62
104
  export function setThemeName(theme) {
63
105
  saveConfig({ theme });
64
106
  }
107
+ export function getViewMode() {
108
+ return loadConfig().viewMode || 'gtd';
109
+ }
110
+ export function setViewMode(viewMode) {
111
+ saveConfig({ viewMode });
112
+ }
113
+ export function isFirstRun() {
114
+ return !existsSync(CONFIG_FILE);
115
+ }
@@ -1,6 +1,7 @@
1
- import Database from 'better-sqlite3';
1
+ import { drizzle } from 'drizzle-orm/libsql';
2
2
  import * as schema from './schema.js';
3
- export declare function getDb(): import("drizzle-orm/better-sqlite3").BetterSQLite3Database<typeof schema> & {
4
- $client: Database.Database;
5
- };
3
+ export type DbInstance = ReturnType<typeof drizzle<typeof schema>>;
4
+ export declare function initDb(): Promise<void>;
5
+ export declare function getDb(): DbInstance;
6
+ export declare function syncDb(): Promise<void>;
6
7
  export { schema };
package/dist/db/index.js CHANGED
@@ -1,45 +1,93 @@
1
- import Database from 'better-sqlite3';
2
- import { drizzle } from 'drizzle-orm/better-sqlite3';
1
+ import { drizzle } from 'drizzle-orm/libsql';
2
+ import { createClient } from '@libsql/client';
3
3
  import { mkdirSync, existsSync } from 'fs';
4
4
  import { dirname } from 'path';
5
5
  import * as schema from './schema.js';
6
- import { getDbPath } from '../config.js';
6
+ import { getDbPath, isTursoEnabled, getTursoConfig } from '../config.js';
7
7
  function ensureDbDir(dbPath) {
8
8
  const dbDir = dirname(dbPath);
9
9
  if (!existsSync(dbDir)) {
10
10
  mkdirSync(dbDir, { recursive: true });
11
11
  }
12
12
  }
13
- function initializeSchema(sqlite) {
14
- // Check if we need to migrate from old schema
15
- const tableInfo = sqlite.prepare("PRAGMA table_info(tasks)").all();
13
+ let client = null;
14
+ let dbInstance = null;
15
+ // リモート DB のスキーマを初期化
16
+ async function initializeRemoteSchema(tursoUrl, authToken) {
17
+ // リモート専用クライアントでスキーマを作成
18
+ const remoteClient = createClient({
19
+ url: tursoUrl,
20
+ authToken: authToken,
21
+ });
22
+ try {
23
+ const tableInfoResult = await remoteClient.execute("PRAGMA table_info(tasks)");
24
+ const tableInfo = tableInfoResult.rows;
25
+ const tableExists = tableInfo.length > 0;
26
+ if (!tableExists) {
27
+ // Fresh install: create new schema on remote
28
+ await remoteClient.execute(`
29
+ CREATE TABLE IF NOT EXISTS tasks (
30
+ id TEXT PRIMARY KEY,
31
+ title TEXT NOT NULL,
32
+ description TEXT,
33
+ status TEXT NOT NULL DEFAULT 'inbox' CHECK(status IN ('inbox', 'next', 'waiting', 'someday', 'done')),
34
+ is_project INTEGER NOT NULL DEFAULT 0,
35
+ parent_id TEXT,
36
+ waiting_for TEXT,
37
+ due_date INTEGER,
38
+ created_at INTEGER NOT NULL,
39
+ updated_at INTEGER NOT NULL
40
+ )
41
+ `);
42
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
43
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
44
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
45
+ }
46
+ // Create comments table
47
+ await remoteClient.execute(`
48
+ CREATE TABLE IF NOT EXISTS comments (
49
+ id TEXT PRIMARY KEY,
50
+ task_id TEXT NOT NULL,
51
+ content TEXT NOT NULL,
52
+ created_at INTEGER NOT NULL
53
+ )
54
+ `);
55
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_comments_task_id ON comments(task_id)");
56
+ }
57
+ finally {
58
+ remoteClient.close();
59
+ }
60
+ }
61
+ // ローカル DB のスキーマを初期化(ローカルモード用)
62
+ async function initializeLocalSchema() {
63
+ if (!client)
64
+ return;
65
+ const tableInfoResult = await client.execute("PRAGMA table_info(tasks)");
66
+ const tableInfo = tableInfoResult.rows;
16
67
  const hasProjectId = tableInfo.some(col => col.name === 'project_id');
17
68
  const hasIsProject = tableInfo.some(col => col.name === 'is_project');
18
69
  const tableExists = tableInfo.length > 0;
19
70
  if (tableExists && hasProjectId && !hasIsProject) {
20
71
  // Migration: old schema -> new schema
21
- // Add new columns
22
- sqlite.prepare("ALTER TABLE tasks ADD COLUMN is_project INTEGER NOT NULL DEFAULT 0").run();
23
- sqlite.prepare("ALTER TABLE tasks ADD COLUMN parent_id TEXT").run();
24
- // Migrate: convert projects to tasks with is_project=1
25
- const projects = sqlite.prepare("SELECT * FROM projects").all();
26
- const insertStmt = sqlite.prepare(`
27
- INSERT INTO tasks (id, title, description, status, is_project, parent_id, waiting_for, due_date, created_at, updated_at)
28
- VALUES (?, ?, ?, ?, 1, NULL, NULL, NULL, ?, ?)
29
- `);
72
+ await client.execute("ALTER TABLE tasks ADD COLUMN is_project INTEGER NOT NULL DEFAULT 0");
73
+ await client.execute("ALTER TABLE tasks ADD COLUMN parent_id TEXT");
74
+ const projectsResult = await client.execute("SELECT * FROM projects");
75
+ const projects = projectsResult.rows;
30
76
  for (const p of projects) {
31
77
  const newStatus = p.status === 'active' ? 'next' : (p.status === 'completed' ? 'done' : p.status);
32
- insertStmt.run(p.id, p.name, p.description, newStatus, p.created_at, p.updated_at);
78
+ await client.execute({
79
+ sql: `INSERT INTO tasks (id, title, description, status, is_project, parent_id, waiting_for, due_date, created_at, updated_at)
80
+ VALUES (?, ?, ?, ?, 1, NULL, NULL, NULL, ?, ?)`,
81
+ args: [p.id, p.name, p.description, newStatus, p.created_at, p.updated_at]
82
+ });
33
83
  }
34
- // Update parent_id from old project_id
35
- sqlite.prepare("UPDATE tasks SET parent_id = project_id WHERE project_id IS NOT NULL").run();
36
- // Create new indexes
37
- sqlite.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)").run();
38
- sqlite.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)").run();
84
+ await client.execute("UPDATE tasks SET parent_id = project_id WHERE project_id IS NOT NULL");
85
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
86
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
39
87
  }
40
88
  else if (!tableExists) {
41
89
  // Fresh install: create new schema
42
- sqlite.prepare(`
90
+ await client.execute(`
43
91
  CREATE TABLE IF NOT EXISTS tasks (
44
92
  id TEXT PRIMARY KEY,
45
93
  title TEXT NOT NULL,
@@ -52,21 +100,68 @@ function initializeSchema(sqlite) {
52
100
  created_at INTEGER NOT NULL,
53
101
  updated_at INTEGER NOT NULL
54
102
  )
55
- `).run();
56
- sqlite.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)").run();
57
- sqlite.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)").run();
58
- sqlite.prepare("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)").run();
103
+ `);
104
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
105
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
106
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
59
107
  }
108
+ // Create comments table
109
+ await client.execute(`
110
+ CREATE TABLE IF NOT EXISTS comments (
111
+ id TEXT PRIMARY KEY,
112
+ task_id TEXT NOT NULL,
113
+ content TEXT NOT NULL,
114
+ created_at INTEGER NOT NULL
115
+ )
116
+ `);
117
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_comments_task_id ON comments(task_id)");
60
118
  }
61
- let dbInstance = null;
119
+ // DB 初期化
120
+ export async function initDb() {
121
+ if (dbInstance)
122
+ return;
123
+ const dbPath = getDbPath();
124
+ ensureDbDir(dbPath);
125
+ if (isTursoEnabled()) {
126
+ // Turso embedded replica モード
127
+ const turso = getTursoConfig();
128
+ // 1. まずリモートにスキーマを作成
129
+ await initializeRemoteSchema(turso.url, turso.authToken);
130
+ // 2. embedded replica クライアントを作成
131
+ client = createClient({
132
+ url: `file:${dbPath}`,
133
+ syncUrl: turso.url,
134
+ authToken: turso.authToken,
135
+ syncInterval: 60, // 60秒ごとに自動同期
136
+ });
137
+ // 3. 初回同期(リモートからローカルにデータをプル)
138
+ await client.sync();
139
+ }
140
+ else {
141
+ // ローカルモード(libsql でローカルファイルのみ)
142
+ client = createClient({
143
+ url: `file:${dbPath}`,
144
+ });
145
+ // ローカルモードではスキーマをローカルに作成
146
+ await initializeLocalSchema();
147
+ }
148
+ dbInstance = drizzle(client, { schema });
149
+ }
150
+ // DB インスタンス取得
62
151
  export function getDb() {
63
152
  if (!dbInstance) {
64
- const dbPath = getDbPath();
65
- ensureDbDir(dbPath);
66
- const sqlite = new Database(dbPath);
67
- initializeSchema(sqlite);
68
- dbInstance = drizzle(sqlite, { schema });
153
+ throw new Error('Database not initialized. Call initDb() first.');
69
154
  }
70
155
  return dbInstance;
71
156
  }
157
+ // 手動同期(Turso モード時のみ有効)
158
+ export async function syncDb() {
159
+ if (!client) {
160
+ throw new Error('Database not initialized');
161
+ }
162
+ if (!isTursoEnabled()) {
163
+ throw new Error('Turso sync is not enabled');
164
+ }
165
+ await client.sync();
166
+ }
72
167
  export { schema };
@@ -190,3 +190,86 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
190
190
  export type Task = typeof tasks.$inferSelect;
191
191
  export type NewTask = typeof tasks.$inferInsert;
192
192
  export type TaskStatus = 'inbox' | 'next' | 'waiting' | 'someday' | 'done';
193
+ export declare const comments: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
194
+ name: "comments";
195
+ schema: undefined;
196
+ columns: {
197
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
198
+ name: "id";
199
+ tableName: "comments";
200
+ dataType: "string";
201
+ columnType: "SQLiteText";
202
+ data: string;
203
+ driverParam: string;
204
+ notNull: true;
205
+ hasDefault: false;
206
+ isPrimaryKey: true;
207
+ isAutoincrement: false;
208
+ hasRuntimeDefault: false;
209
+ enumValues: [string, ...string[]];
210
+ baseColumn: never;
211
+ identity: undefined;
212
+ generated: undefined;
213
+ }, {}, {
214
+ length: number | undefined;
215
+ }>;
216
+ taskId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
217
+ name: "task_id";
218
+ tableName: "comments";
219
+ dataType: "string";
220
+ columnType: "SQLiteText";
221
+ data: string;
222
+ driverParam: string;
223
+ notNull: true;
224
+ hasDefault: false;
225
+ isPrimaryKey: false;
226
+ isAutoincrement: false;
227
+ hasRuntimeDefault: false;
228
+ enumValues: [string, ...string[]];
229
+ baseColumn: never;
230
+ identity: undefined;
231
+ generated: undefined;
232
+ }, {}, {
233
+ length: number | undefined;
234
+ }>;
235
+ content: import("drizzle-orm/sqlite-core").SQLiteColumn<{
236
+ name: "content";
237
+ tableName: "comments";
238
+ dataType: "string";
239
+ columnType: "SQLiteText";
240
+ data: string;
241
+ driverParam: string;
242
+ notNull: true;
243
+ hasDefault: false;
244
+ isPrimaryKey: false;
245
+ isAutoincrement: false;
246
+ hasRuntimeDefault: false;
247
+ enumValues: [string, ...string[]];
248
+ baseColumn: never;
249
+ identity: undefined;
250
+ generated: undefined;
251
+ }, {}, {
252
+ length: number | undefined;
253
+ }>;
254
+ createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
255
+ name: "created_at";
256
+ tableName: "comments";
257
+ dataType: "date";
258
+ columnType: "SQLiteTimestamp";
259
+ data: Date;
260
+ driverParam: number;
261
+ notNull: true;
262
+ hasDefault: false;
263
+ isPrimaryKey: false;
264
+ isAutoincrement: false;
265
+ hasRuntimeDefault: false;
266
+ enumValues: undefined;
267
+ baseColumn: never;
268
+ identity: undefined;
269
+ generated: undefined;
270
+ }, {}, {}>;
271
+ };
272
+ dialect: "sqlite";
273
+ }>;
274
+ export type Comment = typeof comments.$inferSelect;
275
+ export type NewComment = typeof comments.$inferInsert;
package/dist/db/schema.js CHANGED
@@ -11,3 +11,9 @@ export const tasks = sqliteTable('tasks', {
11
11
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
12
12
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
13
13
  });
14
+ export const comments = sqliteTable('comments', {
15
+ id: text('id').primaryKey(),
16
+ taskId: text('task_id').notNull(),
17
+ content: text('content').notNull(),
18
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
19
+ });