floq 0.0.1 → 0.1.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/config.js CHANGED
@@ -39,6 +39,16 @@ export function saveConfig(updates) {
39
39
  // Ignore errors
40
40
  }
41
41
  }
42
+ export function getTursoConfig() {
43
+ return loadConfig().turso;
44
+ }
45
+ export function setTursoConfig(config) {
46
+ saveConfig({ turso: config });
47
+ }
48
+ export function isTursoEnabled() {
49
+ const turso = getTursoConfig();
50
+ return turso !== undefined && turso.url !== '' && turso.authToken !== '';
51
+ }
42
52
  export function getDbPath() {
43
53
  const config = loadConfig();
44
54
  if (config.db_path) {
@@ -48,6 +58,10 @@ export function getDbPath() {
48
58
  }
49
59
  return join(DATA_DIR, config.db_path);
50
60
  }
61
+ // Turso モードでは別のDBファイルを使用(embedded replica 用)
62
+ if (isTursoEnabled()) {
63
+ return join(DATA_DIR, 'gtd-turso.db');
64
+ }
51
65
  return join(DATA_DIR, 'gtd.db');
52
66
  }
53
67
  export function getLocale() {
@@ -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,83 @@
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
+ }
47
+ finally {
48
+ remoteClient.close();
49
+ }
50
+ }
51
+ // ローカル DB のスキーマを初期化(ローカルモード用)
52
+ async function initializeLocalSchema() {
53
+ if (!client)
54
+ return;
55
+ const tableInfoResult = await client.execute("PRAGMA table_info(tasks)");
56
+ const tableInfo = tableInfoResult.rows;
16
57
  const hasProjectId = tableInfo.some(col => col.name === 'project_id');
17
58
  const hasIsProject = tableInfo.some(col => col.name === 'is_project');
18
59
  const tableExists = tableInfo.length > 0;
19
60
  if (tableExists && hasProjectId && !hasIsProject) {
20
61
  // 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
- `);
62
+ await client.execute("ALTER TABLE tasks ADD COLUMN is_project INTEGER NOT NULL DEFAULT 0");
63
+ await client.execute("ALTER TABLE tasks ADD COLUMN parent_id TEXT");
64
+ const projectsResult = await client.execute("SELECT * FROM projects");
65
+ const projects = projectsResult.rows;
30
66
  for (const p of projects) {
31
67
  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);
68
+ await client.execute({
69
+ sql: `INSERT INTO tasks (id, title, description, status, is_project, parent_id, waiting_for, due_date, created_at, updated_at)
70
+ VALUES (?, ?, ?, ?, 1, NULL, NULL, NULL, ?, ?)`,
71
+ args: [p.id, p.name, p.description, newStatus, p.created_at, p.updated_at]
72
+ });
33
73
  }
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();
74
+ await client.execute("UPDATE tasks SET parent_id = project_id WHERE project_id IS NOT NULL");
75
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
76
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
39
77
  }
40
78
  else if (!tableExists) {
41
79
  // Fresh install: create new schema
42
- sqlite.prepare(`
80
+ await client.execute(`
43
81
  CREATE TABLE IF NOT EXISTS tasks (
44
82
  id TEXT PRIMARY KEY,
45
83
  title TEXT NOT NULL,
@@ -52,21 +90,58 @@ function initializeSchema(sqlite) {
52
90
  created_at INTEGER NOT NULL,
53
91
  updated_at INTEGER NOT NULL
54
92
  )
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();
93
+ `);
94
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
95
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
96
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
59
97
  }
60
98
  }
61
- let dbInstance = null;
99
+ // DB 初期化
100
+ export async function initDb() {
101
+ if (dbInstance)
102
+ return;
103
+ const dbPath = getDbPath();
104
+ ensureDbDir(dbPath);
105
+ if (isTursoEnabled()) {
106
+ // Turso embedded replica モード
107
+ const turso = getTursoConfig();
108
+ // 1. まずリモートにスキーマを作成
109
+ await initializeRemoteSchema(turso.url, turso.authToken);
110
+ // 2. embedded replica クライアントを作成
111
+ client = createClient({
112
+ url: `file:${dbPath}`,
113
+ syncUrl: turso.url,
114
+ authToken: turso.authToken,
115
+ syncInterval: 60, // 60秒ごとに自動同期
116
+ });
117
+ // 3. 初回同期(リモートからローカルにデータをプル)
118
+ await client.sync();
119
+ }
120
+ else {
121
+ // ローカルモード(libsql でローカルファイルのみ)
122
+ client = createClient({
123
+ url: `file:${dbPath}`,
124
+ });
125
+ // ローカルモードではスキーマをローカルに作成
126
+ await initializeLocalSchema();
127
+ }
128
+ dbInstance = drizzle(client, { schema });
129
+ }
130
+ // DB インスタンス取得
62
131
  export function getDb() {
63
132
  if (!dbInstance) {
64
- const dbPath = getDbPath();
65
- ensureDbDir(dbPath);
66
- const sqlite = new Database(dbPath);
67
- initializeSchema(sqlite);
68
- dbInstance = drizzle(sqlite, { schema });
133
+ throw new Error('Database not initialized. Call initDb() first.');
69
134
  }
70
135
  return dbInstance;
71
136
  }
137
+ // 手動同期(Turso モード時のみ有効)
138
+ export async function syncDb() {
139
+ if (!client) {
140
+ throw new Error('Database not initialized');
141
+ }
142
+ if (!isTursoEnabled()) {
143
+ throw new Error('Turso sync is not enabled');
144
+ }
145
+ await client.sync();
146
+ }
72
147
  export { schema };
package/dist/i18n/en.d.ts CHANGED
@@ -74,6 +74,16 @@ export declare const en: {
74
74
  selectProject: string;
75
75
  selectProjectHelp: string;
76
76
  back: string;
77
+ keyBar: {
78
+ add: string;
79
+ done: string;
80
+ next: string;
81
+ someday: string;
82
+ inbox: string;
83
+ project: string;
84
+ help: string;
85
+ quit: string;
86
+ };
77
87
  help: {
78
88
  title: string;
79
89
  navigation: string;
@@ -122,6 +132,16 @@ export type HelpTranslations = {
122
132
  quit: string;
123
133
  closeHint: string;
124
134
  };
135
+ export type KeyBarTranslations = {
136
+ add: string;
137
+ done: string;
138
+ next: string;
139
+ someday: string;
140
+ inbox: string;
141
+ project: string;
142
+ help: string;
143
+ quit: string;
144
+ };
125
145
  export type TuiTranslations = {
126
146
  title: string;
127
147
  helpHint: string;
@@ -144,6 +164,7 @@ export type TuiTranslations = {
144
164
  selectProject: string;
145
165
  selectProjectHelp: string;
146
166
  back: string;
167
+ keyBar: KeyBarTranslations;
147
168
  help: HelpTranslations;
148
169
  };
149
170
  export type Translations = {
package/dist/i18n/en.js CHANGED
@@ -80,6 +80,17 @@ export const en = {
80
80
  selectProject: 'Select project for',
81
81
  selectProjectHelp: 'j/k: select, Enter: confirm, Esc: cancel',
82
82
  back: 'back',
83
+ // Action key bar labels
84
+ keyBar: {
85
+ add: 'Add',
86
+ done: 'Done',
87
+ next: 'Next',
88
+ someday: 'Someday',
89
+ inbox: 'Inbox',
90
+ project: 'Project',
91
+ help: 'Help',
92
+ quit: 'Quit',
93
+ },
83
94
  // Help modal
84
95
  help: {
85
96
  title: 'Keyboard Shortcuts',
package/dist/i18n/ja.js CHANGED
@@ -80,6 +80,17 @@ export const ja = {
80
80
  selectProject: 'プロジェクトを選択',
81
81
  selectProjectHelp: 'j/k: 選択, Enter: 確定, Esc: キャンセル',
82
82
  back: '戻る',
83
+ // Action key bar labels
84
+ keyBar: {
85
+ add: '追加',
86
+ done: '完了',
87
+ next: '次へ',
88
+ someday: 'いつか',
89
+ inbox: 'Inbox',
90
+ project: 'プロジェクト',
91
+ help: 'ヘルプ',
92
+ quit: '終了',
93
+ },
83
94
  // Help modal
84
95
  help: {
85
96
  title: 'キーボードショートカット',
package/dist/index.js CHANGED
@@ -1,3 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import { program } from './cli.js';
3
- program.parse();
3
+ import { initDb } from './db/index.js';
4
+ import { isTursoEnabled, getTursoConfig } from './config.js';
5
+ async function main() {
6
+ // 起動時にモードを表示(TUI以外のコマンド時)
7
+ const args = process.argv.slice(2);
8
+ const isTuiMode = args.length === 0;
9
+ const isConfigCommand = args[0] === 'config';
10
+ const isSyncCommand = args[0] === 'sync';
11
+ // config/syncコマンド以外でTursoモードの場合は接続先を表示
12
+ if (!isTuiMode && !isConfigCommand && !isSyncCommand && isTursoEnabled()) {
13
+ const turso = getTursoConfig();
14
+ if (turso) {
15
+ const host = new URL(turso.url).host;
16
+ console.log(`🔄 Turso sync: ${host}`);
17
+ }
18
+ }
19
+ // configコマンドはDB不要なのでスキップ
20
+ if (!isConfigCommand) {
21
+ await initDb();
22
+ }
23
+ program.parse();
24
+ }
25
+ main().catch(console.error);