@fukuchang/taskchute-cli 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/dist/index.js +2250 -0
  4. package/package.json +64 -0
package/dist/index.js ADDED
@@ -0,0 +1,2250 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { Entry } from '@napi-rs/keyring';
4
+ import { input, password } from '@inquirer/prompts';
5
+ import { createClient } from '@supabase/supabase-js';
6
+ import { argon2id } from 'hash-wasm';
7
+ import '@scure/bip39';
8
+ import '@scure/bip39/wordlists/english.js';
9
+
10
+ var KEYRING_SERVICE = "taskchute-cli";
11
+ var KEYRING_ACCOUNT = "passphrase";
12
+ var KeyringPassphraseStore = class {
13
+ get() {
14
+ try {
15
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
16
+ return entry.getPassword();
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ set(passphrase) {
22
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
23
+ entry.setPassword(passphrase);
24
+ }
25
+ clear() {
26
+ try {
27
+ const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
28
+ entry.deletePassword();
29
+ } catch {
30
+ }
31
+ }
32
+ };
33
+
34
+ // src/commands/lock.ts
35
+ function lockCommand() {
36
+ return new Command("lock").description("keyring \u304B\u3089 E2EE passphrase \u3092\u524A\u9664\u3059\u308B (session \u306F\u7DAD\u6301)").action(() => {
37
+ new KeyringPassphraseStore().clear();
38
+ console.log("locked");
39
+ });
40
+ }
41
+ function createNodeSupabaseClient(config, options) {
42
+ return createClient(config.url, config.publishableKey, {
43
+ auth: {
44
+ storage: options.storage,
45
+ persistSession: true,
46
+ autoRefreshToken: true,
47
+ detectSessionInUrl: false
48
+ }
49
+ });
50
+ }
51
+
52
+ // ../infra-supabase/src/supabase-utils.ts
53
+ async function getCurrentUserId(supabase) {
54
+ const { data, error } = await supabase.auth.getUser();
55
+ if (error) throw error;
56
+ if (!data.user) {
57
+ throw new Error("Not authenticated. Sign in to access this resource.");
58
+ }
59
+ return data.user.id;
60
+ }
61
+ function dateToIsoOrNull(date) {
62
+ return date ? date.toISOString() : null;
63
+ }
64
+
65
+ // ../crypto/src/bytes.ts
66
+ function allocBytes(length) {
67
+ return new Uint8Array(new ArrayBuffer(length));
68
+ }
69
+ function randomBytes(length) {
70
+ const bytes = allocBytes(length);
71
+ crypto.getRandomValues(bytes);
72
+ return bytes;
73
+ }
74
+ function copyToArrayBuffer(source) {
75
+ const copy = allocBytes(source.byteLength);
76
+ copy.set(source);
77
+ return copy;
78
+ }
79
+
80
+ // ../crypto/src/aad.ts
81
+ var TEXT_ENCODER = new TextEncoder();
82
+ function buildAAD(ctx) {
83
+ const text = `${ctx.entityId}:${ctx.field}`;
84
+ return copyToArrayBuffer(TEXT_ENCODER.encode(text));
85
+ }
86
+
87
+ // ../crypto/src/base64url.ts
88
+ function base64urlToBytes(s) {
89
+ if (!/^[A-Za-z0-9_-]*$/.test(s)) {
90
+ throw new Error("base64url payload contains characters outside the alphabet");
91
+ }
92
+ const standard = s.replace(/-/g, "+").replace(/_/g, "/");
93
+ const padded = standard + "=".repeat((4 - standard.length % 4) % 4);
94
+ const binary = atob(padded);
95
+ const out = allocBytes(binary.length);
96
+ for (let i = 0; i < binary.length; i++) {
97
+ out[i] = binary.charCodeAt(i);
98
+ }
99
+ return out;
100
+ }
101
+
102
+ // ../crypto/src/bytea-codec.ts
103
+ var HEX_RE = /^[0-9a-f]*$/i;
104
+ function bytesToBytea(bytes) {
105
+ let hex = "";
106
+ for (const b of bytes) {
107
+ hex += b.toString(16).padStart(2, "0");
108
+ }
109
+ return `\\x${hex}`;
110
+ }
111
+ function byteaToBytes(bytea) {
112
+ if (!bytea.startsWith("\\x")) {
113
+ throw new Error(`expected bytea to start with \\x, got: ${bytea.slice(0, 8)}`);
114
+ }
115
+ const hex = bytea.slice(2);
116
+ if (hex.length % 2 !== 0) {
117
+ throw new Error(`bytea hex payload must have an even length, got ${hex.length}`);
118
+ }
119
+ if (!HEX_RE.test(hex)) {
120
+ throw new Error("bytea hex payload contains non-hex characters");
121
+ }
122
+ const bytes = allocBytes(hex.length / 2);
123
+ for (let i = 0; i < bytes.length; i++) {
124
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
125
+ }
126
+ return bytes;
127
+ }
128
+
129
+ // ../crypto/src/field-codec.ts
130
+ async function encryptTextToWire(session, plaintext, ctx) {
131
+ const enc = await session.encryptText(plaintext, ctx);
132
+ return {
133
+ ciphertext: bytesToBytea(enc.ciphertext),
134
+ iv: bytesToBytea(enc.iv)
135
+ };
136
+ }
137
+ async function encryptOptionalTextToWire(session, plaintext, ctx) {
138
+ if (plaintext == null) return null;
139
+ return encryptTextToWire(session, plaintext, ctx);
140
+ }
141
+ async function decryptOptionalTextFromWire(session, ciphertext, iv, keyId, ctx) {
142
+ if (ciphertext == null || iv == null) return null;
143
+ if (keyId == null) {
144
+ throw new Error(
145
+ `decryptOptionalTextFromWire: row has ${ctx.field} ciphertext but key_id is null (${ctx.entityId})`
146
+ );
147
+ }
148
+ return session.decryptText(
149
+ { ciphertext: byteaToBytes(ciphertext), iv: byteaToBytes(iv), keyId },
150
+ ctx
151
+ );
152
+ }
153
+ async function decryptRequiredTextFromWire(session, ciphertext, iv, keyId, ctx) {
154
+ if (ciphertext == null || iv == null || keyId == null) {
155
+ throw new Error(
156
+ `decryptRequiredTextFromWire: row missing ciphertext / iv / key_id for ${ctx.field} (${ctx.entityId})`
157
+ );
158
+ }
159
+ return session.decryptText(
160
+ { ciphertext: byteaToBytes(ciphertext), iv: byteaToBytes(iv), keyId },
161
+ ctx
162
+ );
163
+ }
164
+
165
+ // ../crypto/src/crypto-types.ts
166
+ var DEFAULT_ARGON2ID_PARAMS = {
167
+ memorySize: 65536,
168
+ iterations: 3,
169
+ parallelism: 4,
170
+ hashLength: 32
171
+ };
172
+ var LockedError = class extends Error {
173
+ constructor(message = "crypto session is locked") {
174
+ super(message);
175
+ this.name = "LockedError";
176
+ }
177
+ };
178
+
179
+ // ../crypto/src/key-derivation.ts
180
+ async function deriveKeyMaterial(password4, salt, params = DEFAULT_ARGON2ID_PARAMS) {
181
+ const hash = await argon2id({
182
+ password: password4.normalize("NFC"),
183
+ salt,
184
+ parallelism: params.parallelism,
185
+ iterations: params.iterations,
186
+ memorySize: params.memorySize,
187
+ hashLength: params.hashLength,
188
+ outputType: "binary"
189
+ });
190
+ if (typeof hash === "string") throw new Error("argon2id returned string in binary mode");
191
+ return copyToArrayBuffer(hash);
192
+ }
193
+ async function importKEK(material) {
194
+ return crypto.subtle.importKey(
195
+ "raw",
196
+ material,
197
+ { name: "AES-GCM" },
198
+ false,
199
+ ["wrapKey", "unwrapKey"]
200
+ );
201
+ }
202
+ async function deriveKEK(password4, salt, params = DEFAULT_ARGON2ID_PARAMS) {
203
+ const material = await deriveKeyMaterial(password4, salt, params);
204
+ return importKEK(material);
205
+ }
206
+
207
+ // ../crypto/src/wrap.ts
208
+ async function unwrapMasterKey(wrapped, iv, kek) {
209
+ return crypto.subtle.unwrapKey(
210
+ "raw",
211
+ wrapped,
212
+ kek,
213
+ { name: "AES-GCM", iv },
214
+ { name: "AES-GCM", length: 256 },
215
+ false,
216
+ ["encrypt", "decrypt"]
217
+ );
218
+ }
219
+
220
+ // ../crypto/src/crypto-session.ts
221
+ var TEXT_ENCODER2 = new TextEncoder();
222
+ var TEXT_DECODER = new TextDecoder();
223
+ var CryptoSession = class {
224
+ masterKey = null;
225
+ currentKeyId = null;
226
+ listeners = /* @__PURE__ */ new Set();
227
+ isUnlocked() {
228
+ return this.masterKey !== null;
229
+ }
230
+ getCurrentKeyId() {
231
+ return this.currentKeyId;
232
+ }
233
+ /**
234
+ * Returns the unwrapped CryptoKey handle, or null when locked.
235
+ *
236
+ * Intended primarily for persistence adapters (e.g. IndexedDB) that need
237
+ * to structured-clone the key to outlive the React tree. The handle is
238
+ * still `extractable: false`, so this does not leak raw bytes.
239
+ */
240
+ getMasterKey() {
241
+ return this.masterKey;
242
+ }
243
+ /**
244
+ * Install an unlocked master key. The caller is responsible for obtaining
245
+ * a non-extractable CryptoKey (typically via `unwrapMasterKey`).
246
+ */
247
+ setMasterKey(masterKey, keyId) {
248
+ this.masterKey = masterKey;
249
+ this.currentKeyId = keyId;
250
+ this.notify();
251
+ }
252
+ /**
253
+ * Drop the master key handle. Subsequent encrypt/decrypt calls throw
254
+ * `LockedError` so that callers at the UI boundary can redirect to the
255
+ * unlock screen.
256
+ */
257
+ lock() {
258
+ this.masterKey = null;
259
+ this.currentKeyId = null;
260
+ this.notify();
261
+ }
262
+ /**
263
+ * Subscribe to lock / unlock transitions. The listener is invoked
264
+ * synchronously after `setMasterKey` or `lock` mutates the session.
265
+ *
266
+ * The signature is intentionally `() => void` so that `useSyncExternalStore`
267
+ * can plug straight in: React reads the latest snapshot via the getter
268
+ * functions (`isUnlocked`, `getCurrentKeyId`) on its own schedule.
269
+ *
270
+ * Returns an unsubscribe function. Idempotent — calling twice is fine.
271
+ */
272
+ subscribe(listener) {
273
+ this.listeners.add(listener);
274
+ return () => {
275
+ this.listeners.delete(listener);
276
+ };
277
+ }
278
+ notify() {
279
+ for (const listener of this.listeners) {
280
+ try {
281
+ listener();
282
+ } catch (error) {
283
+ console.error("CryptoSession listener threw during notify()", error);
284
+ }
285
+ }
286
+ }
287
+ /**
288
+ * Encrypt a text field with the active master key. The AAD binds the
289
+ * ciphertext to its (entity, field) coordinates: an attacker who can
290
+ * write to Supabase cannot graft one row's encrypted title onto another
291
+ * row, because the AAD will mismatch on decrypt.
292
+ */
293
+ async encryptText(plaintext, ctx) {
294
+ if (!this.masterKey || !this.currentKeyId) throw new LockedError();
295
+ const iv = randomBytes(12);
296
+ const buffer = await crypto.subtle.encrypt(
297
+ { name: "AES-GCM", iv, additionalData: buildAAD(ctx) },
298
+ this.masterKey,
299
+ TEXT_ENCODER2.encode(plaintext)
300
+ );
301
+ return {
302
+ ciphertext: new Uint8Array(buffer),
303
+ iv,
304
+ keyId: this.currentKeyId
305
+ };
306
+ }
307
+ /**
308
+ * Decrypt a previously encrypted text field. A mismatched AAD, IV, or
309
+ * ciphertext surfaces as an AES-GCM authentication failure
310
+ * (DOMException from `crypto.subtle.decrypt`); callers should treat any
311
+ * thrown error as "this row is corrupt or not ours".
312
+ */
313
+ async decryptText(encrypted, ctx) {
314
+ if (!this.masterKey) throw new LockedError();
315
+ const buffer = await crypto.subtle.decrypt(
316
+ { name: "AES-GCM", iv: encrypted.iv, additionalData: buildAAD(ctx) },
317
+ this.masterKey,
318
+ encrypted.ciphertext
319
+ );
320
+ return TEXT_DECODER.decode(buffer);
321
+ }
322
+ };
323
+
324
+ // ../crypto/src/user-keys-row-mapper.ts
325
+ function buildPassphrase(row) {
326
+ if (!row.passphrase_salt || !row.passphrase_iv || !row.passphrase_wrapped || !row.passphrase_kdf_params) {
327
+ return null;
328
+ }
329
+ return {
330
+ salt: byteaToBytes(row.passphrase_salt),
331
+ iv: byteaToBytes(row.passphrase_iv),
332
+ wrapped: byteaToBytes(row.passphrase_wrapped),
333
+ kdfParams: row.passphrase_kdf_params
334
+ };
335
+ }
336
+ function buildRecovery(row) {
337
+ if (!row.recovery_salt || !row.recovery_iv || !row.recovery_wrapped || !row.recovery_kdf_params) {
338
+ return null;
339
+ }
340
+ return {
341
+ salt: byteaToBytes(row.recovery_salt),
342
+ iv: byteaToBytes(row.recovery_iv),
343
+ wrapped: byteaToBytes(row.recovery_wrapped),
344
+ kdfParams: row.recovery_kdf_params
345
+ };
346
+ }
347
+ function rowToUserKeysSnapshot(row) {
348
+ return {
349
+ currentKeyId: row.current_key_id,
350
+ passphrase: buildPassphrase(row),
351
+ recovery: buildRecovery(row),
352
+ passkeys: row.passkey_wraps ?? [],
353
+ isInitialized: row.is_initialized
354
+ };
355
+ }
356
+
357
+ // ../crypto/src/passkey.ts
358
+ var PRF_INFO_LABEL = "taskchute-e2ee-kek-v1";
359
+ var KEK_BITS = 256;
360
+ async function prfOutputToKEKMaterial(prfOutput, hkdfSalt = new Uint8Array()) {
361
+ const key = await crypto.subtle.importKey("raw", prfOutput, "HKDF", false, ["deriveBits"]);
362
+ const bits = await crypto.subtle.deriveBits(
363
+ {
364
+ name: "HKDF",
365
+ hash: "SHA-256",
366
+ salt: hkdfSalt,
367
+ info: new TextEncoder().encode(PRF_INFO_LABEL)
368
+ },
369
+ key,
370
+ KEK_BITS
371
+ );
372
+ const out = allocBytes(KEK_BITS / 8);
373
+ out.set(new Uint8Array(bits));
374
+ return out;
375
+ }
376
+ async function mnemonicToKEKMaterial(mnemonic, salt, params = DEFAULT_ARGON2ID_PARAMS) {
377
+ return deriveKeyMaterial(normaliseMnemonic(mnemonic), salt, params);
378
+ }
379
+ function normaliseMnemonic(mnemonic) {
380
+ return mnemonic.normalize("NFC").toLowerCase().trim().split(/\s+/).join(" ");
381
+ }
382
+
383
+ // ../crypto/src/unlock-orchestrator.ts
384
+ var UnlockError = class extends Error {
385
+ reason;
386
+ constructor(message, reason) {
387
+ super(message);
388
+ this.name = "UnlockError";
389
+ this.reason = reason;
390
+ }
391
+ };
392
+ async function unlockMasterKey(snapshot, method) {
393
+ switch (method.kind) {
394
+ case "passphrase": {
395
+ if (!snapshot.passphrase) {
396
+ throw new UnlockError("Passphrase envelope not configured", "no-envelope");
397
+ }
398
+ const kek = await deriveKEK(
399
+ method.passphrase,
400
+ snapshot.passphrase.salt,
401
+ snapshot.passphrase.kdfParams
402
+ );
403
+ try {
404
+ return await unwrapMasterKey(
405
+ snapshot.passphrase.wrapped,
406
+ snapshot.passphrase.iv,
407
+ kek
408
+ );
409
+ } catch {
410
+ throw new UnlockError("Wrong passphrase", "wrong-key");
411
+ }
412
+ }
413
+ case "recovery": {
414
+ if (!snapshot.recovery) {
415
+ throw new UnlockError("Recovery envelope not configured", "no-envelope");
416
+ }
417
+ const material = await mnemonicToKEKMaterial(
418
+ method.mnemonic,
419
+ snapshot.recovery.salt,
420
+ snapshot.recovery.kdfParams
421
+ );
422
+ const kek = await importKEK(material);
423
+ try {
424
+ return await unwrapMasterKey(
425
+ snapshot.recovery.wrapped,
426
+ snapshot.recovery.iv,
427
+ kek
428
+ );
429
+ } catch {
430
+ throw new UnlockError("Wrong recovery key", "wrong-key");
431
+ }
432
+ }
433
+ case "passkey": {
434
+ const wrap = findPasskeyWrap(snapshot.passkeys, method.credentialId);
435
+ if (!wrap) {
436
+ throw new UnlockError("Passkey credentialId not registered", "no-credential");
437
+ }
438
+ const material = await prfOutputToKEKMaterial(method.prfOutput);
439
+ const kek = await importKEK(material);
440
+ try {
441
+ return await unwrapMasterKey(
442
+ base64urlToBytes(wrap.wrapped),
443
+ base64urlToBytes(wrap.iv),
444
+ kek
445
+ );
446
+ } catch {
447
+ throw new UnlockError("Passkey PRF output did not unwrap", "wrong-key");
448
+ }
449
+ }
450
+ }
451
+ }
452
+ function findPasskeyWrap(wraps, credentialId) {
453
+ return wraps.find((w) => w.credentialId === credentialId);
454
+ }
455
+
456
+ // ../infra-supabase/src/supabase-context-repository.ts
457
+ async function rowToContext(row, cryptoSession) {
458
+ const [name, description] = await Promise.all([
459
+ decryptRequiredTextFromWire(
460
+ cryptoSession,
461
+ row.name_ciphertext,
462
+ row.name_iv,
463
+ row.key_id,
464
+ { entityId: row.id, field: "name" }
465
+ ),
466
+ decryptOptionalTextFromWire(
467
+ cryptoSession,
468
+ row.description_ciphertext,
469
+ row.description_iv,
470
+ row.key_id,
471
+ { entityId: row.id, field: "description" }
472
+ )
473
+ ]);
474
+ const context = {
475
+ id: row.id,
476
+ name,
477
+ status: row.status,
478
+ createdAt: new Date(row.created_at),
479
+ updatedAt: new Date(row.updated_at)
480
+ };
481
+ if (row.icon !== null) context.icon = row.icon;
482
+ if (row.color !== null) context.color = row.color;
483
+ if (description !== null) context.description = description;
484
+ return context;
485
+ }
486
+ function nonEncryptedFieldsToRow(input2) {
487
+ const row = {};
488
+ if ("icon" in input2) row.icon = input2.icon ?? null;
489
+ if ("color" in input2) row.color = input2.color ?? null;
490
+ if ("status" in input2 && input2.status !== void 0) row.status = input2.status;
491
+ return row;
492
+ }
493
+ async function encryptContextFieldsInto(row, entityId, patch, cryptoSession) {
494
+ let touched = false;
495
+ if (patch.name !== void 0) {
496
+ const wire = await encryptTextToWire(cryptoSession, patch.name, {
497
+ entityId,
498
+ field: "name"
499
+ });
500
+ row.name_ciphertext = wire.ciphertext;
501
+ row.name_iv = wire.iv;
502
+ touched = true;
503
+ }
504
+ if ("description" in patch) {
505
+ const wire = await encryptOptionalTextToWire(cryptoSession, patch.description, {
506
+ entityId,
507
+ field: "description"
508
+ });
509
+ row.description_ciphertext = wire?.ciphertext ?? null;
510
+ row.description_iv = wire?.iv ?? null;
511
+ touched = true;
512
+ }
513
+ if (touched) {
514
+ const keyId = cryptoSession.getCurrentKeyId();
515
+ if (keyId === null) {
516
+ throw new Error("SupabaseContextRepository: session became locked mid-write");
517
+ }
518
+ row.key_id = keyId;
519
+ }
520
+ }
521
+ var SupabaseContextRepository = class {
522
+ supabase;
523
+ cryptoSession;
524
+ constructor(supabase, cryptoSession) {
525
+ this.supabase = supabase;
526
+ this.cryptoSession = cryptoSession;
527
+ }
528
+ async findAll(filter) {
529
+ let query = this.supabase.from("contexts").select("*");
530
+ if (filter?.status) query = query.eq("status", filter.status);
531
+ const { data, error } = await query.order("created_at", { ascending: true });
532
+ if (error) throw error;
533
+ return Promise.all(data.map((row) => rowToContext(row, this.cryptoSession)));
534
+ }
535
+ async findById(id) {
536
+ const { data, error } = await this.supabase.from("contexts").select("*").eq("id", id).maybeSingle();
537
+ if (error) throw error;
538
+ return data ? rowToContext(data, this.cryptoSession) : null;
539
+ }
540
+ async create(input2) {
541
+ const userId = await getCurrentUserId(this.supabase);
542
+ const id = crypto.randomUUID();
543
+ const row = nonEncryptedFieldsToRow(input2);
544
+ await encryptContextFieldsInto(
545
+ row,
546
+ id,
547
+ { name: input2.name, description: input2.description },
548
+ this.cryptoSession
549
+ );
550
+ const insert = {
551
+ ...row,
552
+ id,
553
+ user_id: userId
554
+ };
555
+ const { data, error } = await this.supabase.from("contexts").insert(insert).select().single();
556
+ if (error) throw error;
557
+ return rowToContext(data, this.cryptoSession);
558
+ }
559
+ async update(id, patch) {
560
+ const row = nonEncryptedFieldsToRow(patch);
561
+ await encryptContextFieldsInto(row, id, patch, this.cryptoSession);
562
+ const { data, error } = await this.supabase.from("contexts").update(row).eq("id", id).select().single();
563
+ if (error) throw error;
564
+ return rowToContext(data, this.cryptoSession);
565
+ }
566
+ async archive(id) {
567
+ return this.update(id, { status: "archived" });
568
+ }
569
+ async delete(id) {
570
+ const { error } = await this.supabase.from("contexts").delete().eq("id", id);
571
+ if (error) throw error;
572
+ }
573
+ };
574
+
575
+ // ../infra-supabase/src/supabase-project-repository.ts
576
+ async function rowToProject(row, cryptoSession) {
577
+ const [name, description] = await Promise.all([
578
+ decryptRequiredTextFromWire(
579
+ cryptoSession,
580
+ row.name_ciphertext,
581
+ row.name_iv,
582
+ row.key_id,
583
+ { entityId: row.id, field: "name" }
584
+ ),
585
+ decryptOptionalTextFromWire(
586
+ cryptoSession,
587
+ row.description_ciphertext,
588
+ row.description_iv,
589
+ row.key_id,
590
+ { entityId: row.id, field: "description" }
591
+ )
592
+ ]);
593
+ const project = {
594
+ id: row.id,
595
+ name,
596
+ status: row.status,
597
+ createdAt: new Date(row.created_at),
598
+ updatedAt: new Date(row.updated_at)
599
+ };
600
+ if (description !== null) project.description = description;
601
+ if (row.color !== null) project.color = row.color;
602
+ if (row.parent_project_id !== null) project.parentProjectId = row.parent_project_id;
603
+ if (row.goal_id !== null) project.goalId = row.goal_id;
604
+ if (row.started_at !== null) project.startedAt = new Date(row.started_at);
605
+ if (row.target_end_date !== null) project.targetEndDate = new Date(row.target_end_date);
606
+ if (row.archived_at !== null) project.archivedAt = new Date(row.archived_at);
607
+ return project;
608
+ }
609
+ function nonEncryptedFieldsToRow2(input2) {
610
+ const row = {};
611
+ if ("color" in input2) row.color = input2.color ?? null;
612
+ if ("parentProjectId" in input2) row.parent_project_id = input2.parentProjectId ?? null;
613
+ if ("goalId" in input2) row.goal_id = input2.goalId ?? null;
614
+ if ("status" in input2 && input2.status !== void 0) row.status = input2.status;
615
+ if ("startedAt" in input2) row.started_at = dateToIsoOrNull(input2.startedAt);
616
+ if ("targetEndDate" in input2) row.target_end_date = dateToIsoOrNull(input2.targetEndDate);
617
+ if ("archivedAt" in input2) row.archived_at = dateToIsoOrNull(input2.archivedAt);
618
+ return row;
619
+ }
620
+ async function encryptProjectFieldsInto(row, entityId, patch, cryptoSession) {
621
+ let touched = false;
622
+ if (patch.name !== void 0) {
623
+ const wire = await encryptTextToWire(cryptoSession, patch.name, {
624
+ entityId,
625
+ field: "name"
626
+ });
627
+ row.name_ciphertext = wire.ciphertext;
628
+ row.name_iv = wire.iv;
629
+ touched = true;
630
+ }
631
+ if ("description" in patch) {
632
+ const wire = await encryptOptionalTextToWire(cryptoSession, patch.description, {
633
+ entityId,
634
+ field: "description"
635
+ });
636
+ row.description_ciphertext = wire?.ciphertext ?? null;
637
+ row.description_iv = wire?.iv ?? null;
638
+ touched = true;
639
+ }
640
+ if (touched) {
641
+ const keyId = cryptoSession.getCurrentKeyId();
642
+ if (keyId === null) {
643
+ throw new Error("SupabaseProjectRepository: session became locked mid-write");
644
+ }
645
+ row.key_id = keyId;
646
+ }
647
+ }
648
+ var SupabaseProjectRepository = class {
649
+ supabase;
650
+ cryptoSession;
651
+ constructor(supabase, cryptoSession) {
652
+ this.supabase = supabase;
653
+ this.cryptoSession = cryptoSession;
654
+ }
655
+ async findAll(filter) {
656
+ let query = this.supabase.from("projects").select("*");
657
+ if (filter?.status) query = query.eq("status", filter.status);
658
+ const { data, error } = await query.order("created_at", { ascending: true });
659
+ if (error) throw error;
660
+ return Promise.all(data.map((row) => rowToProject(row, this.cryptoSession)));
661
+ }
662
+ async findById(id) {
663
+ const { data, error } = await this.supabase.from("projects").select("*").eq("id", id).maybeSingle();
664
+ if (error) throw error;
665
+ return data ? rowToProject(data, this.cryptoSession) : null;
666
+ }
667
+ async create(input2) {
668
+ const userId = await getCurrentUserId(this.supabase);
669
+ const id = crypto.randomUUID();
670
+ const row = nonEncryptedFieldsToRow2(input2);
671
+ await encryptProjectFieldsInto(
672
+ row,
673
+ id,
674
+ { name: input2.name, description: input2.description },
675
+ this.cryptoSession
676
+ );
677
+ const insert = {
678
+ ...row,
679
+ id,
680
+ user_id: userId
681
+ };
682
+ const { data, error } = await this.supabase.from("projects").insert(insert).select().single();
683
+ if (error) throw error;
684
+ return rowToProject(data, this.cryptoSession);
685
+ }
686
+ async update(id, patch) {
687
+ const row = nonEncryptedFieldsToRow2(patch);
688
+ await encryptProjectFieldsInto(row, id, patch, this.cryptoSession);
689
+ const { data, error } = await this.supabase.from("projects").update(row).eq("id", id).select().single();
690
+ if (error) throw error;
691
+ return rowToProject(data, this.cryptoSession);
692
+ }
693
+ async archive(id) {
694
+ return this.update(id, { status: "archived", archivedAt: /* @__PURE__ */ new Date() });
695
+ }
696
+ async delete(id) {
697
+ const { error } = await this.supabase.from("projects").delete().eq("id", id);
698
+ if (error) throw error;
699
+ }
700
+ };
701
+
702
+ // ../infra-supabase/src/supabase-routine-repository.ts
703
+ async function rowToRoutine(row, cryptoSession) {
704
+ const [title, description] = await Promise.all([
705
+ decryptRequiredTextFromWire(
706
+ cryptoSession,
707
+ row.title_ciphertext,
708
+ row.title_iv,
709
+ row.key_id,
710
+ { entityId: row.id, field: "title" }
711
+ ),
712
+ decryptOptionalTextFromWire(
713
+ cryptoSession,
714
+ row.description_ciphertext,
715
+ row.description_iv,
716
+ row.key_id,
717
+ { entityId: row.id, field: "description" }
718
+ )
719
+ ]);
720
+ const routine = {
721
+ id: row.id,
722
+ title,
723
+ estimatedSeconds: row.estimated_seconds,
724
+ tagIds: row.tag_ids,
725
+ frequency: row.frequency,
726
+ autoExpand: row.auto_expand,
727
+ status: row.status,
728
+ createdAt: new Date(row.created_at),
729
+ updatedAt: new Date(row.updated_at)
730
+ };
731
+ if (description !== null) routine.description = description;
732
+ if (row.project_id !== null) routine.projectId = row.project_id;
733
+ if (row.context_id !== null) routine.contextId = row.context_id;
734
+ if (row.default_scheduled_start_time !== null) {
735
+ routine.defaultScheduledStartTime = row.default_scheduled_start_time;
736
+ }
737
+ if (row.start_date !== null) routine.startDate = row.start_date;
738
+ if (row.end_date !== null) routine.endDate = row.end_date;
739
+ if (row.last_expanded_date !== null) routine.lastExpandedDate = row.last_expanded_date;
740
+ return routine;
741
+ }
742
+ function nonEncryptedFieldsToRow3(input2) {
743
+ const row = {};
744
+ if ("estimatedSeconds" in input2 && input2.estimatedSeconds !== void 0) {
745
+ row.estimated_seconds = input2.estimatedSeconds;
746
+ }
747
+ if ("projectId" in input2) row.project_id = input2.projectId ?? null;
748
+ if ("contextId" in input2) row.context_id = input2.contextId ?? null;
749
+ if ("tagIds" in input2 && input2.tagIds !== void 0) row.tag_ids = input2.tagIds;
750
+ if ("defaultScheduledStartTime" in input2) {
751
+ row.default_scheduled_start_time = input2.defaultScheduledStartTime ?? null;
752
+ }
753
+ if ("frequency" in input2 && input2.frequency !== void 0) {
754
+ row.frequency = input2.frequency;
755
+ }
756
+ if ("startDate" in input2) row.start_date = input2.startDate ?? null;
757
+ if ("endDate" in input2) row.end_date = input2.endDate ?? null;
758
+ if ("autoExpand" in input2 && input2.autoExpand !== void 0) {
759
+ row.auto_expand = input2.autoExpand;
760
+ }
761
+ if ("lastExpandedDate" in input2) {
762
+ row.last_expanded_date = input2.lastExpandedDate ?? null;
763
+ }
764
+ if ("status" in input2 && input2.status !== void 0) row.status = input2.status;
765
+ return row;
766
+ }
767
+ async function encryptRoutineFieldsInto(row, entityId, patch, cryptoSession) {
768
+ let touched = false;
769
+ if (patch.title !== void 0) {
770
+ const wire = await encryptTextToWire(cryptoSession, patch.title, {
771
+ entityId,
772
+ field: "title"
773
+ });
774
+ row.title_ciphertext = wire.ciphertext;
775
+ row.title_iv = wire.iv;
776
+ touched = true;
777
+ }
778
+ if ("description" in patch) {
779
+ const wire = await encryptOptionalTextToWire(cryptoSession, patch.description, {
780
+ entityId,
781
+ field: "description"
782
+ });
783
+ row.description_ciphertext = wire?.ciphertext ?? null;
784
+ row.description_iv = wire?.iv ?? null;
785
+ touched = true;
786
+ }
787
+ if (touched) {
788
+ const keyId = cryptoSession.getCurrentKeyId();
789
+ if (keyId === null) {
790
+ throw new Error("SupabaseRoutineRepository: session became locked mid-write");
791
+ }
792
+ row.key_id = keyId;
793
+ }
794
+ }
795
+ var SupabaseRoutineRepository = class {
796
+ supabase;
797
+ cryptoSession;
798
+ constructor(supabase, cryptoSession) {
799
+ this.supabase = supabase;
800
+ this.cryptoSession = cryptoSession;
801
+ }
802
+ async findAll(filter) {
803
+ let query = this.supabase.from("routines").select("*");
804
+ if (filter?.status) query = query.eq("status", filter.status);
805
+ const { data, error } = await query.order("created_at", { ascending: true });
806
+ if (error) throw error;
807
+ return Promise.all(data.map((row) => rowToRoutine(row, this.cryptoSession)));
808
+ }
809
+ async findById(id) {
810
+ const { data, error } = await this.supabase.from("routines").select("*").eq("id", id).maybeSingle();
811
+ if (error) throw error;
812
+ return data ? rowToRoutine(data, this.cryptoSession) : null;
813
+ }
814
+ async create(input2) {
815
+ const userId = await getCurrentUserId(this.supabase);
816
+ const id = crypto.randomUUID();
817
+ const row = nonEncryptedFieldsToRow3(input2);
818
+ await encryptRoutineFieldsInto(
819
+ row,
820
+ id,
821
+ { title: input2.title, description: input2.description },
822
+ this.cryptoSession
823
+ );
824
+ const insert = {
825
+ ...row,
826
+ id,
827
+ user_id: userId,
828
+ // CreateRoutineInput requires `frequency`; non-encrypted writer keeps
829
+ // it under `frequency`, but the insert path needs it typed at the
830
+ // call site.
831
+ frequency: input2.frequency
832
+ };
833
+ const { data, error } = await this.supabase.from("routines").insert(insert).select().single();
834
+ if (error) throw error;
835
+ return rowToRoutine(data, this.cryptoSession);
836
+ }
837
+ async update(id, patch) {
838
+ const row = nonEncryptedFieldsToRow3(patch);
839
+ await encryptRoutineFieldsInto(row, id, patch, this.cryptoSession);
840
+ const { data, error } = await this.supabase.from("routines").update(row).eq("id", id).select().single();
841
+ if (error) throw error;
842
+ return rowToRoutine(data, this.cryptoSession);
843
+ }
844
+ async archive(id) {
845
+ return this.update(id, { status: "archived" });
846
+ }
847
+ async delete(id) {
848
+ const { error } = await this.supabase.from("routines").delete().eq("id", id);
849
+ if (error) throw error;
850
+ }
851
+ };
852
+
853
+ // ../infra-supabase/src/supabase-tag-repository.ts
854
+ async function rowToTag(row, cryptoSession) {
855
+ const [name, description] = await Promise.all([
856
+ decryptRequiredTextFromWire(
857
+ cryptoSession,
858
+ row.name_ciphertext,
859
+ row.name_iv,
860
+ row.key_id,
861
+ { entityId: row.id, field: "name" }
862
+ ),
863
+ decryptOptionalTextFromWire(
864
+ cryptoSession,
865
+ row.description_ciphertext,
866
+ row.description_iv,
867
+ row.key_id,
868
+ { entityId: row.id, field: "description" }
869
+ )
870
+ ]);
871
+ const tag = {
872
+ id: row.id,
873
+ name,
874
+ status: row.status,
875
+ createdAt: new Date(row.created_at),
876
+ updatedAt: new Date(row.updated_at)
877
+ };
878
+ if (row.color !== null) tag.color = row.color;
879
+ if (description !== null) tag.description = description;
880
+ return tag;
881
+ }
882
+ function nonEncryptedFieldsToRow4(input2) {
883
+ const row = {};
884
+ if ("color" in input2) row.color = input2.color ?? null;
885
+ if ("status" in input2 && input2.status !== void 0) row.status = input2.status;
886
+ return row;
887
+ }
888
+ async function encryptTagFieldsInto(row, entityId, patch, cryptoSession) {
889
+ let touched = false;
890
+ if (patch.name !== void 0) {
891
+ const wire = await encryptTextToWire(cryptoSession, patch.name, {
892
+ entityId,
893
+ field: "name"
894
+ });
895
+ row.name_ciphertext = wire.ciphertext;
896
+ row.name_iv = wire.iv;
897
+ touched = true;
898
+ }
899
+ if ("description" in patch) {
900
+ const wire = await encryptOptionalTextToWire(cryptoSession, patch.description, {
901
+ entityId,
902
+ field: "description"
903
+ });
904
+ row.description_ciphertext = wire?.ciphertext ?? null;
905
+ row.description_iv = wire?.iv ?? null;
906
+ touched = true;
907
+ }
908
+ if (touched) {
909
+ const keyId = cryptoSession.getCurrentKeyId();
910
+ if (keyId === null) {
911
+ throw new Error("SupabaseTagRepository: session became locked mid-write");
912
+ }
913
+ row.key_id = keyId;
914
+ }
915
+ }
916
+ var SupabaseTagRepository = class {
917
+ supabase;
918
+ cryptoSession;
919
+ constructor(supabase, cryptoSession) {
920
+ this.supabase = supabase;
921
+ this.cryptoSession = cryptoSession;
922
+ }
923
+ async findAll(filter) {
924
+ let query = this.supabase.from("tags").select("*");
925
+ if (filter?.status) query = query.eq("status", filter.status);
926
+ const { data, error } = await query.order("created_at", { ascending: true });
927
+ if (error) throw error;
928
+ return Promise.all(data.map((row) => rowToTag(row, this.cryptoSession)));
929
+ }
930
+ async findById(id) {
931
+ const { data, error } = await this.supabase.from("tags").select("*").eq("id", id).maybeSingle();
932
+ if (error) throw error;
933
+ return data ? rowToTag(data, this.cryptoSession) : null;
934
+ }
935
+ async create(input2) {
936
+ const userId = await getCurrentUserId(this.supabase);
937
+ const id = crypto.randomUUID();
938
+ const row = nonEncryptedFieldsToRow4(input2);
939
+ await encryptTagFieldsInto(
940
+ row,
941
+ id,
942
+ { name: input2.name, description: input2.description },
943
+ this.cryptoSession
944
+ );
945
+ const insert = {
946
+ ...row,
947
+ id,
948
+ user_id: userId
949
+ };
950
+ const { data, error } = await this.supabase.from("tags").insert(insert).select().single();
951
+ if (error) throw error;
952
+ return rowToTag(data, this.cryptoSession);
953
+ }
954
+ async update(id, patch) {
955
+ const row = nonEncryptedFieldsToRow4(patch);
956
+ await encryptTagFieldsInto(row, id, patch, this.cryptoSession);
957
+ const { data, error } = await this.supabase.from("tags").update(row).eq("id", id).select().single();
958
+ if (error) throw error;
959
+ return rowToTag(data, this.cryptoSession);
960
+ }
961
+ async archive(id) {
962
+ return this.update(id, { status: "archived" });
963
+ }
964
+ async delete(id) {
965
+ const { error } = await this.supabase.from("tags").delete().eq("id", id);
966
+ if (error) throw error;
967
+ }
968
+ };
969
+
970
+ // ../infra-supabase/src/supabase-task-note-repository.ts
971
+ async function rowToTaskNote(row, cryptoSession) {
972
+ const [body, metaJson, historyJson] = await Promise.all([
973
+ decryptRequiredTextFromWire(
974
+ cryptoSession,
975
+ row.body_ciphertext,
976
+ row.body_iv,
977
+ row.key_id,
978
+ { entityId: row.id, field: "body" }
979
+ ),
980
+ decryptOptionalTextFromWire(
981
+ cryptoSession,
982
+ row.metadata_ciphertext,
983
+ row.metadata_iv,
984
+ row.key_id,
985
+ { entityId: row.id, field: "metadata" }
986
+ ),
987
+ decryptOptionalTextFromWire(
988
+ cryptoSession,
989
+ row.edit_history_ciphertext,
990
+ row.edit_history_iv,
991
+ row.key_id,
992
+ { entityId: row.id, field: "edit_history" }
993
+ )
994
+ ]);
995
+ const metadata = metaJson !== null ? JSON.parse(metaJson) : null;
996
+ let editHistory = null;
997
+ if (historyJson !== null) {
998
+ const stored = JSON.parse(historyJson);
999
+ editHistory = stored.map((e) => ({
1000
+ at: new Date(e.at),
1001
+ previousBody: e.previous_body
1002
+ }));
1003
+ }
1004
+ const note = {
1005
+ id: row.id,
1006
+ taskId: row.task_id,
1007
+ type: row.type,
1008
+ body,
1009
+ createdAt: new Date(row.created_at),
1010
+ updatedAt: new Date(row.updated_at)
1011
+ };
1012
+ if (metadata !== null) {
1013
+ note.metadata = metadata;
1014
+ }
1015
+ if (row.edited_at !== null) {
1016
+ note.editedAt = new Date(row.edited_at);
1017
+ }
1018
+ if (editHistory !== null) {
1019
+ note.editHistory = editHistory;
1020
+ }
1021
+ return note;
1022
+ }
1023
+ function historyToStored(history) {
1024
+ return history.map((e) => ({ at: e.at.toISOString(), previous_body: e.previousBody }));
1025
+ }
1026
+ function nonEncryptedFieldsToRow5(input2) {
1027
+ const row = {};
1028
+ if ("taskId" in input2 && input2.taskId !== void 0) row.task_id = input2.taskId;
1029
+ if ("type" in input2 && input2.type !== void 0) row.type = input2.type;
1030
+ if ("editedAt" in input2) {
1031
+ row.edited_at = input2.editedAt ? input2.editedAt.toISOString() : null;
1032
+ }
1033
+ return row;
1034
+ }
1035
+ async function encryptTaskNoteFieldsInto(row, entityId, patch, cryptoSession) {
1036
+ let touched = false;
1037
+ if (patch.body !== void 0) {
1038
+ const wire = await encryptTextToWire(cryptoSession, patch.body, {
1039
+ entityId,
1040
+ field: "body"
1041
+ });
1042
+ row.body_ciphertext = wire.ciphertext;
1043
+ row.body_iv = wire.iv;
1044
+ touched = true;
1045
+ }
1046
+ if ("metadata" in patch) {
1047
+ const serialized = patch.metadata === void 0 ? null : JSON.stringify(patch.metadata);
1048
+ const wire = await encryptOptionalTextToWire(cryptoSession, serialized, {
1049
+ entityId,
1050
+ field: "metadata"
1051
+ });
1052
+ row.metadata_ciphertext = wire?.ciphertext ?? null;
1053
+ row.metadata_iv = wire?.iv ?? null;
1054
+ touched = true;
1055
+ }
1056
+ if ("editHistory" in patch) {
1057
+ const stored = patch.editHistory === void 0 ? null : historyToStored(patch.editHistory);
1058
+ const serialized = stored === null ? null : JSON.stringify(stored);
1059
+ const wire = await encryptOptionalTextToWire(cryptoSession, serialized, {
1060
+ entityId,
1061
+ field: "edit_history"
1062
+ });
1063
+ row.edit_history_ciphertext = wire?.ciphertext ?? null;
1064
+ row.edit_history_iv = wire?.iv ?? null;
1065
+ touched = true;
1066
+ }
1067
+ if (touched) {
1068
+ const keyId = cryptoSession.getCurrentKeyId();
1069
+ if (keyId === null) {
1070
+ throw new Error(
1071
+ "SupabaseTaskNoteRepository: session became locked mid-write"
1072
+ );
1073
+ }
1074
+ row.key_id = keyId;
1075
+ }
1076
+ }
1077
+ var SupabaseTaskNoteRepository = class {
1078
+ supabase;
1079
+ cryptoSession;
1080
+ constructor(supabase, cryptoSession) {
1081
+ this.supabase = supabase;
1082
+ this.cryptoSession = cryptoSession;
1083
+ }
1084
+ async findByTaskId(taskId, filter) {
1085
+ let query = this.supabase.from("task_notes").select("*").eq("task_id", taskId).order("created_at", { ascending: true });
1086
+ if (filter?.type !== void 0) {
1087
+ query = query.eq("type", filter.type);
1088
+ }
1089
+ const { data, error } = await query;
1090
+ if (error) throw error;
1091
+ return Promise.all(data.map((row) => rowToTaskNote(row, this.cryptoSession)));
1092
+ }
1093
+ async findByTaskIds(taskIds) {
1094
+ const result = /* @__PURE__ */ new Map();
1095
+ for (const id of taskIds) result.set(id, []);
1096
+ if (taskIds.length === 0) return result;
1097
+ const { data, error } = await this.supabase.from("task_notes").select("*").in("task_id", taskIds).order("created_at", { ascending: true });
1098
+ if (error) throw error;
1099
+ const decoded = await Promise.all(
1100
+ data.map((row) => rowToTaskNote(row, this.cryptoSession))
1101
+ );
1102
+ for (const note of decoded) {
1103
+ const bucket = result.get(note.taskId);
1104
+ if (bucket) bucket.push(note);
1105
+ }
1106
+ return result;
1107
+ }
1108
+ async findById(id) {
1109
+ const { data, error } = await this.supabase.from("task_notes").select("*").eq("id", id).maybeSingle();
1110
+ if (error) throw error;
1111
+ return data ? rowToTaskNote(data, this.cryptoSession) : null;
1112
+ }
1113
+ async create(input2) {
1114
+ const userId = await getCurrentUserId(this.supabase);
1115
+ const id = crypto.randomUUID();
1116
+ const row = nonEncryptedFieldsToRow5(input2);
1117
+ await encryptTaskNoteFieldsInto(
1118
+ row,
1119
+ id,
1120
+ { body: input2.body, metadata: input2.metadata, editHistory: input2.editHistory },
1121
+ this.cryptoSession
1122
+ );
1123
+ const insert = {
1124
+ ...row,
1125
+ id,
1126
+ user_id: userId,
1127
+ task_id: input2.taskId,
1128
+ type: input2.type
1129
+ };
1130
+ const { data, error } = await this.supabase.from("task_notes").insert(insert).select().single();
1131
+ if (error) throw error;
1132
+ return rowToTaskNote(data, this.cryptoSession);
1133
+ }
1134
+ async update(id, patch) {
1135
+ const row = nonEncryptedFieldsToRow5(patch);
1136
+ await encryptTaskNoteFieldsInto(row, id, patch, this.cryptoSession);
1137
+ const { data, error } = await this.supabase.from("task_notes").update(row).eq("id", id).select().single();
1138
+ if (error) throw error;
1139
+ return rowToTaskNote(data, this.cryptoSession);
1140
+ }
1141
+ async delete(id) {
1142
+ const { error } = await this.supabase.from("task_notes").delete().eq("id", id);
1143
+ if (error) throw error;
1144
+ }
1145
+ };
1146
+
1147
+ // ../infra-supabase/src/supabase-task-repository.ts
1148
+ async function rowToTask(row, cryptoSession) {
1149
+ const [title, description] = await Promise.all([
1150
+ decryptRequiredTextFromWire(
1151
+ cryptoSession,
1152
+ row.title_ciphertext,
1153
+ row.title_iv,
1154
+ row.key_id,
1155
+ { entityId: row.id, field: "title" }
1156
+ ),
1157
+ decryptOptionalTextFromWire(
1158
+ cryptoSession,
1159
+ row.description_ciphertext,
1160
+ row.description_iv,
1161
+ row.key_id,
1162
+ { entityId: row.id, field: "description" }
1163
+ )
1164
+ ]);
1165
+ const sessions = row.sessions.map((s) => ({
1166
+ startedAt: new Date(s.started_at),
1167
+ finishedAt: new Date(s.finished_at)
1168
+ }));
1169
+ const task = {
1170
+ id: row.id,
1171
+ date: row.date,
1172
+ title,
1173
+ estimatedSeconds: row.estimated_seconds,
1174
+ sessions,
1175
+ status: row.status,
1176
+ orderIndex: row.order_index,
1177
+ tagIds: row.tag_ids,
1178
+ createdAt: new Date(row.created_at),
1179
+ updatedAt: new Date(row.updated_at)
1180
+ };
1181
+ if (description !== null) task.description = description;
1182
+ if (row.scheduled_start_time !== null) task.scheduledStartTime = row.scheduled_start_time;
1183
+ if (row.actual_start_time !== null) task.actualStartTime = row.actual_start_time;
1184
+ if (row.current_session) {
1185
+ const cs = row.current_session;
1186
+ task.currentSession = { startedAt: new Date(cs.started_at) };
1187
+ }
1188
+ if (row.is_background !== null) task.isBackground = row.is_background;
1189
+ if (row.project_id !== null) task.projectId = row.project_id;
1190
+ if (row.routine_id !== null) task.routineId = row.routine_id;
1191
+ if (row.context_id !== null) task.contextId = row.context_id;
1192
+ if (row.split_from_task_id !== null) task.splitFromTaskId = row.split_from_task_id;
1193
+ if (row.notify_at_scheduled_start) task.notifyAtScheduledStart = true;
1194
+ return task;
1195
+ }
1196
+ function nonEncryptedFieldsToRow6(input2) {
1197
+ const row = {};
1198
+ if ("date" in input2 && input2.date !== void 0) row.date = input2.date;
1199
+ if ("estimatedSeconds" in input2 && input2.estimatedSeconds !== void 0) {
1200
+ row.estimated_seconds = input2.estimatedSeconds;
1201
+ }
1202
+ if ("scheduledStartTime" in input2) {
1203
+ row.scheduled_start_time = input2.scheduledStartTime ?? null;
1204
+ }
1205
+ if ("actualStartTime" in input2) {
1206
+ row.actual_start_time = input2.actualStartTime ?? null;
1207
+ }
1208
+ if ("currentSession" in input2) {
1209
+ row.current_session = input2.currentSession ? { started_at: input2.currentSession.startedAt.toISOString() } : null;
1210
+ }
1211
+ if ("sessions" in input2 && input2.sessions !== void 0) {
1212
+ row.sessions = input2.sessions.map((s) => ({
1213
+ started_at: s.startedAt.toISOString(),
1214
+ finished_at: s.finishedAt.toISOString()
1215
+ }));
1216
+ }
1217
+ if ("status" in input2 && input2.status !== void 0) row.status = input2.status;
1218
+ if ("orderIndex" in input2 && input2.orderIndex !== void 0) {
1219
+ row.order_index = input2.orderIndex;
1220
+ }
1221
+ if ("isBackground" in input2) row.is_background = input2.isBackground ?? null;
1222
+ if ("projectId" in input2) row.project_id = input2.projectId ?? null;
1223
+ if ("routineId" in input2) row.routine_id = input2.routineId ?? null;
1224
+ if ("contextId" in input2) row.context_id = input2.contextId ?? null;
1225
+ if ("tagIds" in input2 && input2.tagIds !== void 0) row.tag_ids = input2.tagIds;
1226
+ if ("splitFromTaskId" in input2) {
1227
+ row.split_from_task_id = input2.splitFromTaskId ?? null;
1228
+ }
1229
+ if ("notifyAtScheduledStart" in input2) {
1230
+ row.notify_at_scheduled_start = input2.notifyAtScheduledStart ?? false;
1231
+ }
1232
+ return row;
1233
+ }
1234
+ async function encryptTaskFieldsInto(row, entityId, patch, cryptoSession) {
1235
+ let touched = false;
1236
+ if (patch.title !== void 0) {
1237
+ const wire = await encryptTextToWire(cryptoSession, patch.title, {
1238
+ entityId,
1239
+ field: "title"
1240
+ });
1241
+ row.title_ciphertext = wire.ciphertext;
1242
+ row.title_iv = wire.iv;
1243
+ touched = true;
1244
+ }
1245
+ if ("description" in patch) {
1246
+ const wire = await encryptOptionalTextToWire(cryptoSession, patch.description, {
1247
+ entityId,
1248
+ field: "description"
1249
+ });
1250
+ row.description_ciphertext = wire?.ciphertext ?? null;
1251
+ row.description_iv = wire?.iv ?? null;
1252
+ touched = true;
1253
+ }
1254
+ if (touched) {
1255
+ const keyId = cryptoSession.getCurrentKeyId();
1256
+ if (keyId === null) {
1257
+ throw new Error("SupabaseTaskRepository: session became locked mid-write");
1258
+ }
1259
+ row.key_id = keyId;
1260
+ }
1261
+ }
1262
+ var SupabaseTaskRepository = class {
1263
+ supabase;
1264
+ cryptoSession;
1265
+ constructor(supabase, cryptoSession) {
1266
+ this.supabase = supabase;
1267
+ this.cryptoSession = cryptoSession;
1268
+ }
1269
+ async findById(id) {
1270
+ const { data, error } = await this.supabase.from("tasks").select("*").eq("id", id).maybeSingle();
1271
+ if (error) throw error;
1272
+ return data ? rowToTask(data, this.cryptoSession) : null;
1273
+ }
1274
+ async findByDate(date) {
1275
+ const { data, error } = await this.supabase.from("tasks").select("*").eq("date", date).order("order_index", { ascending: true });
1276
+ if (error) throw error;
1277
+ return Promise.all(data.map((row) => rowToTask(row, this.cryptoSession)));
1278
+ }
1279
+ async findByRoutineIdFromDate(routineId, fromDate) {
1280
+ const { data, error } = await this.supabase.from("tasks").select("*").eq("routine_id", routineId).gte("date", fromDate).order("date", { ascending: true });
1281
+ if (error) throw error;
1282
+ return Promise.all(data.map((row) => rowToTask(row, this.cryptoSession)));
1283
+ }
1284
+ async findFromDate(fromDate) {
1285
+ const { data, error } = await this.supabase.from("tasks").select("*").gte("date", fromDate).order("updated_at", { ascending: false });
1286
+ if (error) throw error;
1287
+ return Promise.all(data.map((row) => rowToTask(row, this.cryptoSession)));
1288
+ }
1289
+ async create(input2) {
1290
+ const userId = await getCurrentUserId(this.supabase);
1291
+ const id = crypto.randomUUID();
1292
+ const row = nonEncryptedFieldsToRow6(input2);
1293
+ await encryptTaskFieldsInto(
1294
+ row,
1295
+ id,
1296
+ { title: input2.title, description: input2.description },
1297
+ this.cryptoSession
1298
+ );
1299
+ const insert = {
1300
+ ...row,
1301
+ id,
1302
+ user_id: userId,
1303
+ date: input2.date
1304
+ };
1305
+ const { data, error } = await this.supabase.from("tasks").insert(insert).select().single();
1306
+ if (error) throw error;
1307
+ return rowToTask(data, this.cryptoSession);
1308
+ }
1309
+ async update(id, patch) {
1310
+ const row = nonEncryptedFieldsToRow6(patch);
1311
+ await encryptTaskFieldsInto(row, id, patch, this.cryptoSession);
1312
+ const { data, error } = await this.supabase.from("tasks").update(row).eq("id", id).select().single();
1313
+ if (error) throw error;
1314
+ return rowToTask(data, this.cryptoSession);
1315
+ }
1316
+ async delete(id) {
1317
+ const { error } = await this.supabase.from("tasks").delete().eq("id", id);
1318
+ if (error) throw error;
1319
+ }
1320
+ };
1321
+
1322
+ // ../infra-supabase/src/supabase-time-block-repository.ts
1323
+ async function rowToTimeBlock(row, cryptoSession) {
1324
+ const name = await decryptRequiredTextFromWire(
1325
+ cryptoSession,
1326
+ row.name_ciphertext,
1327
+ row.name_iv,
1328
+ row.key_id,
1329
+ { entityId: row.id, field: "name" }
1330
+ );
1331
+ const block = {
1332
+ id: row.id,
1333
+ name,
1334
+ startTime: row.start_time,
1335
+ endTime: row.end_time,
1336
+ weekdays: row.weekdays,
1337
+ status: row.status,
1338
+ createdAt: new Date(row.created_at),
1339
+ updatedAt: new Date(row.updated_at)
1340
+ };
1341
+ if (row.default_project_id !== null) block.defaultProjectId = row.default_project_id;
1342
+ if (row.default_context_id !== null) block.defaultContextId = row.default_context_id;
1343
+ if (row.default_tag_ids.length > 0) block.defaultTagIds = row.default_tag_ids;
1344
+ if (row.color !== null) block.color = row.color;
1345
+ if (row.start_date !== null) block.startDate = row.start_date;
1346
+ if (row.end_date !== null) block.endDate = row.end_date;
1347
+ return block;
1348
+ }
1349
+ function nonEncryptedFieldsToRow7(input2) {
1350
+ const row = {};
1351
+ if ("startTime" in input2 && input2.startTime !== void 0) row.start_time = input2.startTime;
1352
+ if ("endTime" in input2 && input2.endTime !== void 0) row.end_time = input2.endTime;
1353
+ if ("weekdays" in input2 && input2.weekdays !== void 0) row.weekdays = input2.weekdays;
1354
+ if ("defaultProjectId" in input2) {
1355
+ row.default_project_id = input2.defaultProjectId ?? null;
1356
+ }
1357
+ if ("defaultContextId" in input2) {
1358
+ row.default_context_id = input2.defaultContextId ?? null;
1359
+ }
1360
+ if ("defaultTagIds" in input2) {
1361
+ row.default_tag_ids = input2.defaultTagIds ?? [];
1362
+ }
1363
+ if ("color" in input2) row.color = input2.color ?? null;
1364
+ if ("status" in input2 && input2.status !== void 0) row.status = input2.status;
1365
+ if ("startDate" in input2) row.start_date = input2.startDate ?? null;
1366
+ if ("endDate" in input2) row.end_date = input2.endDate ?? null;
1367
+ return row;
1368
+ }
1369
+ async function encryptTimeBlockFieldsInto(row, entityId, patch, cryptoSession) {
1370
+ if (patch.name === void 0) return;
1371
+ const wire = await encryptTextToWire(cryptoSession, patch.name, {
1372
+ entityId,
1373
+ field: "name"
1374
+ });
1375
+ row.name_ciphertext = wire.ciphertext;
1376
+ row.name_iv = wire.iv;
1377
+ const keyId = cryptoSession.getCurrentKeyId();
1378
+ if (keyId === null) {
1379
+ throw new Error("SupabaseTimeBlockRepository: session became locked mid-write");
1380
+ }
1381
+ row.key_id = keyId;
1382
+ }
1383
+ var SupabaseTimeBlockRepository = class {
1384
+ supabase;
1385
+ cryptoSession;
1386
+ constructor(supabase, cryptoSession) {
1387
+ this.supabase = supabase;
1388
+ this.cryptoSession = cryptoSession;
1389
+ }
1390
+ async findAll(filter) {
1391
+ let query = this.supabase.from("time_blocks").select("*");
1392
+ if (filter?.status) query = query.eq("status", filter.status);
1393
+ const { data, error } = await query.order("start_time", { ascending: true });
1394
+ if (error) throw error;
1395
+ return Promise.all(data.map((row) => rowToTimeBlock(row, this.cryptoSession)));
1396
+ }
1397
+ async findById(id) {
1398
+ const { data, error } = await this.supabase.from("time_blocks").select("*").eq("id", id).maybeSingle();
1399
+ if (error) throw error;
1400
+ return data ? rowToTimeBlock(data, this.cryptoSession) : null;
1401
+ }
1402
+ async create(input2) {
1403
+ const userId = await getCurrentUserId(this.supabase);
1404
+ const id = crypto.randomUUID();
1405
+ const row = nonEncryptedFieldsToRow7(input2);
1406
+ await encryptTimeBlockFieldsInto(row, id, { name: input2.name }, this.cryptoSession);
1407
+ const insert = {
1408
+ ...row,
1409
+ id,
1410
+ user_id: userId,
1411
+ start_time: input2.startTime,
1412
+ end_time: input2.endTime
1413
+ };
1414
+ const { data, error } = await this.supabase.from("time_blocks").insert(insert).select().single();
1415
+ if (error) throw error;
1416
+ return rowToTimeBlock(data, this.cryptoSession);
1417
+ }
1418
+ async update(id, patch) {
1419
+ const row = nonEncryptedFieldsToRow7(patch);
1420
+ await encryptTimeBlockFieldsInto(row, id, patch, this.cryptoSession);
1421
+ const { data, error } = await this.supabase.from("time_blocks").update(row).eq("id", id).select().single();
1422
+ if (error) throw error;
1423
+ return rowToTimeBlock(data, this.cryptoSession);
1424
+ }
1425
+ async archive(id) {
1426
+ return this.update(id, { status: "archived" });
1427
+ }
1428
+ async delete(id) {
1429
+ const { error } = await this.supabase.from("time_blocks").delete().eq("id", id);
1430
+ if (error) throw error;
1431
+ }
1432
+ };
1433
+
1434
+ // ../infra-supabase/src/supabase-user-keys-repository.ts
1435
+ var SupabaseUserKeysRepository = class {
1436
+ supabase;
1437
+ constructor(supabase) {
1438
+ this.supabase = supabase;
1439
+ }
1440
+ async requireUserId() {
1441
+ const { data, error } = await this.supabase.auth.getUser();
1442
+ if (error) throw error;
1443
+ if (!data.user) throw new Error("not authenticated");
1444
+ return data.user.id;
1445
+ }
1446
+ async getOwn() {
1447
+ const { data, error } = await this.supabase.from("user_keys").select("*").maybeSingle();
1448
+ if (error) throw error;
1449
+ if (!data) return null;
1450
+ return rowToUserKeysSnapshot(data);
1451
+ }
1452
+ async initialize(input2) {
1453
+ const userId = await this.requireUserId();
1454
+ const { data, error } = await this.supabase.from("user_keys").upsert(
1455
+ {
1456
+ user_id: userId,
1457
+ current_key_id: input2.currentKeyId,
1458
+ passphrase_salt: bytesToBytea(input2.passphrase.salt),
1459
+ passphrase_iv: bytesToBytea(input2.passphrase.iv),
1460
+ passphrase_wrapped: bytesToBytea(input2.passphrase.wrapped),
1461
+ // The database.types.ts JSONB columns are widened to `Json`. Our
1462
+ // domain types (Argon2idParams, PasskeyWrapEnvelope) are interfaces
1463
+ // without an index signature, so TS rejects the direct assignment.
1464
+ // Cast at the boundary — repositories own the narrow shape.
1465
+ passphrase_kdf_params: input2.passphrase.kdfParams,
1466
+ recovery_salt: bytesToBytea(input2.recovery.salt),
1467
+ recovery_iv: bytesToBytea(input2.recovery.iv),
1468
+ recovery_wrapped: bytesToBytea(input2.recovery.wrapped),
1469
+ recovery_kdf_params: input2.recovery.kdfParams,
1470
+ passkey_wraps: input2.passkeys ?? [],
1471
+ is_initialized: true
1472
+ },
1473
+ { onConflict: "user_id" }
1474
+ ).select("*").single();
1475
+ if (error) throw error;
1476
+ return rowToUserKeysSnapshot(data);
1477
+ }
1478
+ async updatePassphrase(envelope) {
1479
+ const userId = await this.requireUserId();
1480
+ const { error } = await this.supabase.from("user_keys").update({
1481
+ passphrase_salt: bytesToBytea(envelope.salt),
1482
+ passphrase_iv: bytesToBytea(envelope.iv),
1483
+ passphrase_wrapped: bytesToBytea(envelope.wrapped),
1484
+ passphrase_kdf_params: envelope.kdfParams
1485
+ }).eq("user_id", userId);
1486
+ if (error) throw error;
1487
+ }
1488
+ async updateRecovery(envelope) {
1489
+ const userId = await this.requireUserId();
1490
+ const { error } = await this.supabase.from("user_keys").update({
1491
+ recovery_salt: bytesToBytea(envelope.salt),
1492
+ recovery_iv: bytesToBytea(envelope.iv),
1493
+ recovery_wrapped: bytesToBytea(envelope.wrapped),
1494
+ recovery_kdf_params: envelope.kdfParams
1495
+ }).eq("user_id", userId);
1496
+ if (error) throw error;
1497
+ }
1498
+ async addPasskey(wrap) {
1499
+ const userId = await this.requireUserId();
1500
+ const { data, error: readErr } = await this.supabase.from("user_keys").select("passkey_wraps").eq("user_id", userId).maybeSingle();
1501
+ if (readErr) throw readErr;
1502
+ const current = data?.passkey_wraps ?? [];
1503
+ if (current.some((w) => w.credentialId === wrap.credentialId)) {
1504
+ throw new Error(`passkey credentialId already registered: ${wrap.credentialId}`);
1505
+ }
1506
+ const next = [...current, wrap];
1507
+ const { error } = await this.supabase.from("user_keys").update({ passkey_wraps: next }).eq("user_id", userId);
1508
+ if (error) throw error;
1509
+ }
1510
+ async removePasskey(credentialId) {
1511
+ const userId = await this.requireUserId();
1512
+ const { data, error: readErr } = await this.supabase.from("user_keys").select("passkey_wraps").eq("user_id", userId).maybeSingle();
1513
+ if (readErr) throw readErr;
1514
+ const current = data?.passkey_wraps ?? [];
1515
+ const next = current.filter((w) => w.credentialId !== credentialId);
1516
+ if (next.length === current.length) return;
1517
+ const { error } = await this.supabase.from("user_keys").update({ passkey_wraps: next }).eq("user_id", userId);
1518
+ if (error) throw error;
1519
+ }
1520
+ };
1521
+
1522
+ // ../domain/src/shared/date.ts
1523
+ function toDateString(date) {
1524
+ const year = date.getFullYear();
1525
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1526
+ const day = String(date.getDate()).padStart(2, "0");
1527
+ return `${year}-${month}-${day}`;
1528
+ }
1529
+ function todayDateString() {
1530
+ return toDateString(/* @__PURE__ */ new Date());
1531
+ }
1532
+ function toTimeString(date) {
1533
+ const hours = String(date.getHours()).padStart(2, "0");
1534
+ const minutes = String(date.getMinutes()).padStart(2, "0");
1535
+ return `${hours}:${minutes}`;
1536
+ }
1537
+
1538
+ // ../domain/src/task-operations.ts
1539
+ async function startTask(repo, taskId) {
1540
+ const task = await repo.findById(taskId);
1541
+ if (!task) throw new Error(`Task not found: ${taskId}`);
1542
+ if (task.status === "running") return task;
1543
+ const now = /* @__PURE__ */ new Date();
1544
+ const patch = {
1545
+ status: "running",
1546
+ currentSession: { startedAt: now }
1547
+ };
1548
+ if (!task.actualStartTime) {
1549
+ patch.actualStartTime = toTimeString(now);
1550
+ }
1551
+ return repo.update(taskId, patch);
1552
+ }
1553
+ async function pauseTask(repo, taskId) {
1554
+ const task = await repo.findById(taskId);
1555
+ if (!task) throw new Error(`Task not found: ${taskId}`);
1556
+ if (!task.currentSession) {
1557
+ throw new Error(`Task is not currently running: ${taskId}`);
1558
+ }
1559
+ const newSession = {
1560
+ startedAt: task.currentSession.startedAt,
1561
+ finishedAt: /* @__PURE__ */ new Date()
1562
+ };
1563
+ return repo.update(taskId, {
1564
+ status: "paused",
1565
+ sessions: [...task.sessions, newSession],
1566
+ currentSession: void 0
1567
+ });
1568
+ }
1569
+ async function completeTask(repo, taskId) {
1570
+ const task = await repo.findById(taskId);
1571
+ if (!task) throw new Error(`Task not found: ${taskId}`);
1572
+ const updates = { status: "completed" };
1573
+ if (task.currentSession) {
1574
+ updates.sessions = [
1575
+ ...task.sessions,
1576
+ {
1577
+ startedAt: task.currentSession.startedAt,
1578
+ finishedAt: /* @__PURE__ */ new Date()
1579
+ }
1580
+ ];
1581
+ updates.currentSession = void 0;
1582
+ }
1583
+ return repo.update(taskId, updates);
1584
+ }
1585
+
1586
+ // ../domain/src/task-reorder.ts
1587
+ function computeNextOrderIndex(existingTasks) {
1588
+ if (existingTasks.length === 0) return 10;
1589
+ const max = existingTasks.reduce(
1590
+ (acc, t) => t.orderIndex > acc ? t.orderIndex : acc,
1591
+ 0
1592
+ );
1593
+ return max + 10;
1594
+ }
1595
+
1596
+ // ../domain/src/task-change-log.ts
1597
+ function describeStatusTransition(from, to) {
1598
+ if (from === "planned" && to === "running") return "\u958B\u59CB";
1599
+ if (from === "paused" && to === "running") return "\u518D\u958B";
1600
+ if (from === "running" && to === "paused") return "\u4E00\u6642\u505C\u6B62";
1601
+ if (to === "completed") return "\u5B8C\u4E86";
1602
+ if (to === "skipped") return "\u30B9\u30AD\u30C3\u30D7";
1603
+ if (to === "planned") return "\u672A\u7740\u624B\u306B\u623B\u3059";
1604
+ return `\u30B9\u30C6\u30FC\u30BF\u30B9\u3092 ${from} \u2192 ${to} \u306B\u5909\u66F4`;
1605
+ }
1606
+ function formatMinutes(seconds) {
1607
+ return `${Math.floor(seconds / 60)}\u5206`;
1608
+ }
1609
+ function lookupName(list, id) {
1610
+ if (!id) return null;
1611
+ const found = list.find((e) => e.id === id);
1612
+ return found?.name ?? null;
1613
+ }
1614
+ function eqNullable(a, b) {
1615
+ return (a ?? null) === (b ?? null);
1616
+ }
1617
+ var FIELD_DESCRIPTORS = [
1618
+ {
1619
+ field: "status",
1620
+ hasChanged: (b, a) => b.status !== a.status,
1621
+ toFragments: (b, a) => [
1622
+ {
1623
+ type: "status-change",
1624
+ body: describeStatusTransition(b.status, a.status),
1625
+ metadata: { field: "status", from: b.status, to: a.status }
1626
+ }
1627
+ ]
1628
+ },
1629
+ {
1630
+ field: "date",
1631
+ hasChanged: (b, a) => b.date !== a.date,
1632
+ toFragments: (b, a) => [
1633
+ {
1634
+ type: "system",
1635
+ body: `\u65E5\u4ED8\u3092 ${b.date} \u2192 ${a.date} \u306B\u5909\u66F4`,
1636
+ metadata: { field: "date", from: b.date, to: a.date }
1637
+ }
1638
+ ]
1639
+ },
1640
+ {
1641
+ field: "scheduledStartTime",
1642
+ hasChanged: (b, a) => !eqNullable(b.scheduledStartTime, a.scheduledStartTime),
1643
+ toFragments: (b, a) => [
1644
+ {
1645
+ type: "system",
1646
+ body: `\u4E88\u5B9A\u6642\u523B\u3092 ${b.scheduledStartTime ?? "\u672A\u8A2D\u5B9A"} \u2192 ${a.scheduledStartTime ?? "\u672A\u8A2D\u5B9A"} \u306B\u5909\u66F4`,
1647
+ metadata: {
1648
+ field: "scheduledStartTime",
1649
+ from: b.scheduledStartTime ?? null,
1650
+ to: a.scheduledStartTime ?? null
1651
+ }
1652
+ }
1653
+ ]
1654
+ },
1655
+ {
1656
+ field: "estimatedSeconds",
1657
+ hasChanged: (b, a) => b.estimatedSeconds !== a.estimatedSeconds,
1658
+ toFragments: (b, a) => [
1659
+ {
1660
+ type: "system",
1661
+ body: `\u898B\u7A4D\u3092 ${formatMinutes(b.estimatedSeconds)} \u2192 ${formatMinutes(a.estimatedSeconds)} \u306B\u5909\u66F4`,
1662
+ metadata: {
1663
+ field: "estimatedSeconds",
1664
+ from: b.estimatedSeconds,
1665
+ to: a.estimatedSeconds
1666
+ }
1667
+ }
1668
+ ]
1669
+ },
1670
+ {
1671
+ field: "title",
1672
+ hasChanged: (b, a) => b.title !== a.title,
1673
+ toFragments: (b, a) => [
1674
+ {
1675
+ type: "system",
1676
+ body: `\u30BF\u30A4\u30C8\u30EB\u3092\u300C${b.title || "\u7121\u984C"}\u300D\u2192\u300C${a.title || "\u7121\u984C"}\u300D\u306B\u5909\u66F4`,
1677
+ metadata: { field: "title", from: b.title, to: a.title }
1678
+ }
1679
+ ]
1680
+ },
1681
+ {
1682
+ field: "description",
1683
+ hasChanged: (b, a) => (b.description ?? "") !== (a.description ?? ""),
1684
+ toFragments: (b, a) => [
1685
+ {
1686
+ type: "system",
1687
+ body: "\u8AAC\u660E\u3092\u5909\u66F4",
1688
+ metadata: {
1689
+ field: "description",
1690
+ from: b.description ?? null,
1691
+ to: a.description ?? null
1692
+ }
1693
+ }
1694
+ ]
1695
+ },
1696
+ {
1697
+ field: "projectId",
1698
+ hasChanged: (b, a) => !eqNullable(b.projectId, a.projectId),
1699
+ toFragments: (b, a, lookups) => {
1700
+ const fromName = lookupName(lookups.projects, b.projectId);
1701
+ const toName = lookupName(lookups.projects, a.projectId);
1702
+ let body;
1703
+ if (a.projectId && b.projectId) {
1704
+ body = `\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u300C${fromName ?? b.projectId}\u300D\u2192\u300C${toName ?? a.projectId}\u300D\u306B\u5909\u66F4`;
1705
+ } else if (a.projectId) {
1706
+ body = `\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3092\u300C${toName ?? a.projectId}\u300D\u306B\u8A2D\u5B9A`;
1707
+ } else {
1708
+ body = `\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u300C${fromName ?? b.projectId ?? ""}\u300D\u3092\u89E3\u9664`;
1709
+ }
1710
+ return [
1711
+ {
1712
+ type: "system",
1713
+ body,
1714
+ metadata: {
1715
+ field: "projectId",
1716
+ from: b.projectId ?? null,
1717
+ to: a.projectId ?? null,
1718
+ fromName,
1719
+ toName
1720
+ }
1721
+ }
1722
+ ];
1723
+ }
1724
+ },
1725
+ {
1726
+ field: "contextId",
1727
+ hasChanged: (b, a) => !eqNullable(b.contextId, a.contextId),
1728
+ toFragments: (b, a, lookups) => {
1729
+ const fromName = lookupName(lookups.contexts, b.contextId);
1730
+ const toName = lookupName(lookups.contexts, a.contextId);
1731
+ let body;
1732
+ if (a.contextId && b.contextId) {
1733
+ body = `\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u3092\u300C${fromName ?? b.contextId}\u300D\u2192\u300C${toName ?? a.contextId}\u300D\u306B\u5909\u66F4`;
1734
+ } else if (a.contextId) {
1735
+ body = `\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u3092\u300C${toName ?? a.contextId}\u300D\u306B\u8A2D\u5B9A`;
1736
+ } else {
1737
+ body = `\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u300C${fromName ?? b.contextId ?? ""}\u300D\u3092\u89E3\u9664`;
1738
+ }
1739
+ return [
1740
+ {
1741
+ type: "system",
1742
+ body,
1743
+ metadata: {
1744
+ field: "contextId",
1745
+ from: b.contextId ?? null,
1746
+ to: a.contextId ?? null,
1747
+ fromName,
1748
+ toName
1749
+ }
1750
+ }
1751
+ ];
1752
+ }
1753
+ },
1754
+ {
1755
+ field: "tagIds",
1756
+ // 配列の参照比較は意味がない。要素単位で diff を取って add / remove に分ける。
1757
+ hasChanged: (b, a) => {
1758
+ const bs = new Set(b.tagIds);
1759
+ const as = new Set(a.tagIds);
1760
+ if (bs.size !== as.size) return true;
1761
+ for (const id of bs) if (!as.has(id)) return true;
1762
+ return false;
1763
+ },
1764
+ toFragments: (b, a, lookups) => {
1765
+ const bs = new Set(b.tagIds);
1766
+ const as = new Set(a.tagIds);
1767
+ const added = a.tagIds.filter((id) => !bs.has(id));
1768
+ const removed = b.tagIds.filter((id) => !as.has(id));
1769
+ const fragments = [];
1770
+ for (const tagId of added) {
1771
+ const name = lookupName(lookups.tags, tagId);
1772
+ fragments.push({
1773
+ type: "system",
1774
+ body: `\u30BF\u30B0\u300C${name ?? tagId}\u300D\u3092\u8FFD\u52A0`,
1775
+ metadata: { field: "tagIds", op: "add", tagId, tagName: name }
1776
+ });
1777
+ }
1778
+ for (const tagId of removed) {
1779
+ const name = lookupName(lookups.tags, tagId);
1780
+ fragments.push({
1781
+ type: "system",
1782
+ body: `\u30BF\u30B0\u300C${name ?? tagId}\u300D\u3092\u524A\u9664`,
1783
+ metadata: { field: "tagIds", op: "remove", tagId, tagName: name }
1784
+ });
1785
+ }
1786
+ return fragments;
1787
+ }
1788
+ },
1789
+ {
1790
+ field: "isBackground",
1791
+ hasChanged: (b, a) => (b.isBackground ?? false) !== (a.isBackground ?? false),
1792
+ toFragments: (b, a) => [
1793
+ {
1794
+ type: "system",
1795
+ body: a.isBackground ? "\u80CC\u666F\u30BF\u30B9\u30AF\u306B\u8A2D\u5B9A" : "\u80CC\u666F\u30BF\u30B9\u30AF\u3092\u89E3\u9664",
1796
+ metadata: {
1797
+ field: "isBackground",
1798
+ from: b.isBackground ?? false,
1799
+ to: a.isBackground ?? false
1800
+ }
1801
+ }
1802
+ ]
1803
+ },
1804
+ {
1805
+ field: "notifyAtScheduledStart",
1806
+ hasChanged: (b, a) => (b.notifyAtScheduledStart ?? false) !== (a.notifyAtScheduledStart ?? false),
1807
+ toFragments: (b, a) => [
1808
+ {
1809
+ type: "system",
1810
+ body: a.notifyAtScheduledStart ? "\u4E88\u5B9A\u6642\u523B\u901A\u77E5\u3092 ON" : "\u4E88\u5B9A\u6642\u523B\u901A\u77E5\u3092 OFF",
1811
+ metadata: {
1812
+ field: "notifyAtScheduledStart",
1813
+ from: b.notifyAtScheduledStart ?? false,
1814
+ to: a.notifyAtScheduledStart ?? false
1815
+ }
1816
+ }
1817
+ ]
1818
+ }
1819
+ ];
1820
+ function diffTasksToNoteInputs(before, after, lookups) {
1821
+ const result = [];
1822
+ for (const desc of FIELD_DESCRIPTORS) {
1823
+ if (!desc.hasChanged(before, after)) continue;
1824
+ for (const fragment of desc.toFragments(before, after, lookups)) {
1825
+ result.push({ taskId: after.id, ...fragment });
1826
+ }
1827
+ }
1828
+ return result;
1829
+ }
1830
+
1831
+ // ../infra-supabase/src/audit-task-repository.ts
1832
+ var AuditingTaskRepository = class {
1833
+ // `erasableSyntaxOnly` 配下では parameter property が使えないので、明示的に
1834
+ // フィールド宣言 + コンストラクタ代入の形にしている。
1835
+ inner;
1836
+ notes;
1837
+ getLookups;
1838
+ constructor(inner, notes, getLookups) {
1839
+ this.inner = inner;
1840
+ this.notes = notes;
1841
+ this.getLookups = getLookups;
1842
+ }
1843
+ findById(id) {
1844
+ return this.inner.findById(id);
1845
+ }
1846
+ findByDate(date) {
1847
+ return this.inner.findByDate(date);
1848
+ }
1849
+ findByRoutineIdFromDate(routineId, fromDate) {
1850
+ return this.inner.findByRoutineIdFromDate(routineId, fromDate);
1851
+ }
1852
+ findFromDate(fromDate) {
1853
+ return this.inner.findFromDate(fromDate);
1854
+ }
1855
+ create(input2) {
1856
+ return this.inner.create(input2);
1857
+ }
1858
+ update(id, patch) {
1859
+ return this.runUpdate(id, patch, null);
1860
+ }
1861
+ /**
1862
+ * 「ユーザーの直接操作ではなく、別イベント由来の派生変更」を audit に記録
1863
+ * したいときに使う。各 note の metadata に `cause` が載るので、UI フィルタや
1864
+ * SQL 集計で「直接 vs 派生」を区別できる。
1865
+ *
1866
+ * 標準の `update` には影響しない(呼び分けが必要なときだけ使う)。
1867
+ */
1868
+ updateWithCause(id, patch, cause) {
1869
+ return this.runUpdate(id, patch, cause);
1870
+ }
1871
+ async runUpdate(id, patch, cause) {
1872
+ const before = await this.inner.findById(id);
1873
+ const after = await this.inner.update(id, patch);
1874
+ if (before) {
1875
+ try {
1876
+ const lookups = await this.getLookups();
1877
+ const inputs = diffTasksToNoteInputs(before, after, lookups);
1878
+ const tagged = cause ? inputs.map((input2) => ({
1879
+ ...input2,
1880
+ metadata: {
1881
+ ...input2.metadata ?? {},
1882
+ cause
1883
+ }
1884
+ })) : inputs;
1885
+ if (tagged.length > 0) {
1886
+ await Promise.all(tagged.map((input2) => this.notes.create(input2)));
1887
+ }
1888
+ } catch (err) {
1889
+ console.error("[audit] failed to record task changes", { taskId: id, err });
1890
+ }
1891
+ }
1892
+ return after;
1893
+ }
1894
+ delete(id) {
1895
+ return this.inner.delete(id);
1896
+ }
1897
+ };
1898
+
1899
+ // ../infra-supabase/src/composition-root.ts
1900
+ function buildRepositories(supabaseClient, cryptoSession, getLookups) {
1901
+ const taskNote = new SupabaseTaskNoteRepository(supabaseClient, cryptoSession);
1902
+ const rawTask = new SupabaseTaskRepository(supabaseClient, cryptoSession);
1903
+ const task = getLookups ? new AuditingTaskRepository(rawTask, taskNote, getLookups) : rawTask;
1904
+ return {
1905
+ task,
1906
+ taskNote,
1907
+ routine: new SupabaseRoutineRepository(supabaseClient, cryptoSession),
1908
+ project: new SupabaseProjectRepository(supabaseClient, cryptoSession),
1909
+ tag: new SupabaseTagRepository(supabaseClient, cryptoSession),
1910
+ context: new SupabaseContextRepository(supabaseClient, cryptoSession),
1911
+ timeBlock: new SupabaseTimeBlockRepository(supabaseClient, cryptoSession)
1912
+ };
1913
+ }
1914
+
1915
+ // src/config.ts
1916
+ var ENV_URL = "TASKCHUTE_SUPABASE_URL";
1917
+ var ENV_KEY = "TASKCHUTE_SUPABASE_PUBLISHABLE_KEY";
1918
+ function readSupabaseConfigFromEnv() {
1919
+ const url = process.env.TASKCHUTE_SUPABASE_URL;
1920
+ const publishableKey = process.env.TASKCHUTE_SUPABASE_PUBLISHABLE_KEY;
1921
+ if (!url || !publishableKey) {
1922
+ throw new Error(
1923
+ `Supabase \u306E\u74B0\u5883\u5909\u6570\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u3002${ENV_URL} \u3068 ${ENV_KEY} \u3092\u8A2D\u5B9A\u3057\u3066\u306D\u301C`
1924
+ );
1925
+ }
1926
+ return { url, publishableKey };
1927
+ }
1928
+ var KEYRING_SERVICE2 = "taskchute-cli";
1929
+ var KeyringSessionStore = class {
1930
+ getItem(key) {
1931
+ try {
1932
+ const entry = new Entry(KEYRING_SERVICE2, key);
1933
+ return entry.getPassword();
1934
+ } catch {
1935
+ return null;
1936
+ }
1937
+ }
1938
+ setItem(key, value) {
1939
+ const entry = new Entry(KEYRING_SERVICE2, key);
1940
+ entry.setPassword(value);
1941
+ }
1942
+ removeItem(key) {
1943
+ try {
1944
+ const entry = new Entry(KEYRING_SERVICE2, key);
1945
+ entry.deletePassword();
1946
+ } catch {
1947
+ }
1948
+ }
1949
+ };
1950
+
1951
+ // src/commands/login.ts
1952
+ function loginCommand() {
1953
+ return new Command("login").description("Supabase \u306B email + password \u3067\u30ED\u30B0\u30A4\u30F3\u3057\u3066 session \u3092 keyring \u306B\u4FDD\u5B58\u3059\u308B").action(async () => {
1954
+ const config = readSupabaseConfigFromEnv();
1955
+ const supabase = createNodeSupabaseClient(config, {
1956
+ storage: new KeyringSessionStore()
1957
+ });
1958
+ const email = await input({ message: "Email:" });
1959
+ const pw = await password({ message: "Password:", mask: "*" });
1960
+ const { error } = await supabase.auth.signInWithPassword({ email, password: pw });
1961
+ if (error) {
1962
+ console.error("login failed:", error.message);
1963
+ process.exitCode = 1;
1964
+ return;
1965
+ }
1966
+ console.log("login OK \u26A1");
1967
+ });
1968
+ }
1969
+ function logoutCommand() {
1970
+ return new Command("logout").description("\u30ED\u30B0\u30A2\u30A6\u30C8\u3057\u3066 keyring \u304B\u3089 session \u3092\u524A\u9664\u3059\u308B").action(async () => {
1971
+ const config = readSupabaseConfigFromEnv();
1972
+ const supabase = createNodeSupabaseClient(config, {
1973
+ storage: new KeyringSessionStore()
1974
+ });
1975
+ const { error } = await supabase.auth.signOut();
1976
+ if (error) {
1977
+ console.error("logout failed:", error.message);
1978
+ process.exitCode = 1;
1979
+ return;
1980
+ }
1981
+ console.log("logged out \u{1F44B}");
1982
+ });
1983
+ }
1984
+ var CliError = class extends Error {
1985
+ exitCode;
1986
+ constructor(message, exitCode = 1) {
1987
+ super(message);
1988
+ this.name = "CliError";
1989
+ this.exitCode = exitCode;
1990
+ }
1991
+ };
1992
+ async function resolvePassphrase(store, allowPrompt = true) {
1993
+ const envPw = process.env.TASKCHUTE_PASSPHRASE;
1994
+ if (envPw) return envPw;
1995
+ const stored = store.get();
1996
+ if (stored) return stored;
1997
+ if (!allowPrompt) {
1998
+ throw new CliError(
1999
+ "No passphrase available. Set TASKCHUTE_PASSPHRASE env or run `taskchute unlock` first."
2000
+ );
2001
+ }
2002
+ return await password({ message: "Passphrase:", mask: "*" });
2003
+ }
2004
+ async function createAppContext(opts = {}) {
2005
+ const passphraseStore = opts.passphraseStore ?? new KeyringPassphraseStore();
2006
+ const allowPrompt = opts.allowPrompt ?? true;
2007
+ const supabase = createNodeSupabaseClient(readSupabaseConfigFromEnv(), {
2008
+ storage: new KeyringSessionStore()
2009
+ });
2010
+ const {
2011
+ data: { user },
2012
+ error: authError
2013
+ } = await supabase.auth.getUser();
2014
+ if (authError || !user || !user.email) {
2015
+ throw new CliError("Not logged in. Run `taskchute login` first.");
2016
+ }
2017
+ const userKeysRepo = new SupabaseUserKeysRepository(supabase);
2018
+ const snapshot = await userKeysRepo.getOwn();
2019
+ if (!snapshot) {
2020
+ throw new CliError(
2021
+ "E2EE not set up. Complete setup from the web app first (Settings \u2192 \u30C7\u30FC\u30BF\u306E\u6697\u53F7\u5316)."
2022
+ );
2023
+ }
2024
+ const passphrase = await resolvePassphrase(passphraseStore, allowPrompt);
2025
+ let masterKey;
2026
+ try {
2027
+ masterKey = await unlockMasterKey(snapshot, { kind: "passphrase", passphrase });
2028
+ } catch (err) {
2029
+ if (err instanceof UnlockError) {
2030
+ throw new CliError(`Unlock failed: ${err.message}`);
2031
+ }
2032
+ throw err;
2033
+ }
2034
+ const cryptoSession = new CryptoSession();
2035
+ cryptoSession.setMasterKey(masterKey, snapshot.currentKeyId);
2036
+ const repos = buildRepositories(supabase, cryptoSession);
2037
+ return {
2038
+ supabase,
2039
+ cryptoSession,
2040
+ repos,
2041
+ userId: user.id,
2042
+ email: user.email
2043
+ };
2044
+ }
2045
+
2046
+ // src/format/parse-estimated.ts
2047
+ var HOURS_HEAD = /^(\d{1,4})h/;
2048
+ var MINUTES_TAIL = /^(\d{1,4})m$/;
2049
+ var MAX_HOURS = 9999;
2050
+ function parseEstimated(input2) {
2051
+ const normalized = input2.trim().toLowerCase();
2052
+ let rest = normalized;
2053
+ let hours = 0;
2054
+ let minutes = 0;
2055
+ const hMatch = HOURS_HEAD.exec(rest);
2056
+ if (hMatch) {
2057
+ hours = Number(hMatch[1]);
2058
+ rest = rest.slice(hMatch[0].length);
2059
+ }
2060
+ const mMatch = MINUTES_TAIL.exec(rest);
2061
+ if (mMatch) {
2062
+ minutes = Number(mMatch[1]);
2063
+ rest = "";
2064
+ }
2065
+ if (rest !== "" || hMatch === null && mMatch === null || hours > MAX_HOURS) {
2066
+ throw new Error(
2067
+ `Invalid estimated format: "${input2}". Use formats like "30m", "1h", "1h30m".`
2068
+ );
2069
+ }
2070
+ return (hours * 60 + minutes) * 60;
2071
+ }
2072
+
2073
+ // src/format/task-row.ts
2074
+ function shortenId(id, length = 8) {
2075
+ return id.slice(0, length);
2076
+ }
2077
+ function formatEstimated(seconds) {
2078
+ const totalMinutes = Math.round(seconds / 60);
2079
+ if (totalMinutes < 60) return `${totalMinutes}m`;
2080
+ const h = Math.floor(totalMinutes / 60);
2081
+ const m = totalMinutes % 60;
2082
+ return m === 0 ? `${h}h` : `${h}h${m}m`;
2083
+ }
2084
+ function formatTaskRow(task) {
2085
+ return {
2086
+ id: shortenId(task.id),
2087
+ status: task.status,
2088
+ scheduled: task.scheduledStartTime ?? "-",
2089
+ estimated: formatEstimated(task.estimatedSeconds),
2090
+ title: task.title
2091
+ };
2092
+ }
2093
+
2094
+ // src/commands/tasks/add.ts
2095
+ function tasksAddCommand() {
2096
+ return new Command("add").description("\u30BF\u30B9\u30AF\u3092\u8FFD\u52A0\u3059\u308B").argument("<title>", "\u30BF\u30B9\u30AF\u306E\u30BF\u30A4\u30C8\u30EB").option("-d, --date <date>", "YYYY-MM-DD \u5F62\u5F0F\u306E\u65E5\u4ED8 (\u30C7\u30D5\u30A9\u30EB\u30C8: \u4ECA\u65E5)").option("-e, --estimated <duration>", '\u898B\u7A4D\u3082\u308A\u6642\u9593 ("30m" / "1h" / "1h30m") (\u30C7\u30D5\u30A9\u30EB\u30C8: 30m)').option("--description <text>", "\u30BF\u30B9\u30AF\u306E\u8A73\u7D30\u8AAC\u660E").action(async (title, opts) => {
2097
+ const ctx = await createAppContext();
2098
+ const date = opts.date ?? todayDateString();
2099
+ const estimatedSeconds = opts.estimated ? parseEstimated(opts.estimated) : 30 * 60;
2100
+ const existing = await ctx.repos.task.findByDate(date);
2101
+ const orderIndex = computeNextOrderIndex(existing);
2102
+ const task = await ctx.repos.task.create({
2103
+ date,
2104
+ title,
2105
+ description: opts.description,
2106
+ estimatedSeconds,
2107
+ sessions: [],
2108
+ status: "planned",
2109
+ orderIndex,
2110
+ tagIds: []
2111
+ });
2112
+ console.log(`\u2713 Created: ${shortenId(task.id)} ${task.title}`);
2113
+ });
2114
+ }
2115
+
2116
+ // src/resolve-task-id.ts
2117
+ var MIN_PREFIX_LENGTH = 4;
2118
+ function resolveTaskIdByPrefix(tasks, prefix) {
2119
+ if (prefix.length < MIN_PREFIX_LENGTH) {
2120
+ throw new CliError(
2121
+ `Task id prefix must be at least ${MIN_PREFIX_LENGTH} chars to avoid ambiguity.`
2122
+ );
2123
+ }
2124
+ const matches = tasks.filter((t) => t.id.startsWith(prefix));
2125
+ if (matches.length === 0) {
2126
+ throw new CliError(`No task matched prefix "${prefix}".`);
2127
+ }
2128
+ if (matches.length > 1) {
2129
+ const candidates = matches.map((t) => ` ${shortenId(t.id)} ${t.title}`).join("\n");
2130
+ throw new CliError(`Ambiguous prefix "${prefix}":
2131
+ ${candidates}`);
2132
+ }
2133
+ return matches[0].id;
2134
+ }
2135
+
2136
+ // src/commands/tasks/done.ts
2137
+ function tasksDoneCommand() {
2138
+ return new Command("done").description("\u30BF\u30B9\u30AF\u3092\u5B8C\u4E86\u3059\u308B (status \u2192 completed)").argument("<id-prefix>", "\u30BF\u30B9\u30AF id \u306E\u5148\u982D 4 \u6587\u5B57\u4EE5\u4E0A").option("-d, --date <date>", "\u5BFE\u8C61\u65E5 (\u30C7\u30D5\u30A9\u30EB\u30C8: \u4ECA\u65E5)").action(async (prefix, opts) => {
2139
+ const ctx = await createAppContext();
2140
+ const date = opts.date ?? todayDateString();
2141
+ const tasks = await ctx.repos.task.findByDate(date);
2142
+ const taskId = resolveTaskIdByPrefix(tasks, prefix);
2143
+ const updated = await completeTask(ctx.repos.task, taskId);
2144
+ console.log(`\u2713 Done: ${shortenId(updated.id)} ${updated.title}`);
2145
+ });
2146
+ }
2147
+ function tasksListCommand() {
2148
+ return new Command("list").alias("ls").description("\u6307\u5B9A\u65E5 (\u30C7\u30D5\u30A9\u30EB\u30C8\u306F\u4ECA\u65E5) \u306E\u30BF\u30B9\u30AF\u4E00\u89A7\u3092\u8868\u793A\u3059\u308B").option("-d, --date <date>", "YYYY-MM-DD \u5F62\u5F0F\u306E\u65E5\u4ED8 (\u30C7\u30D5\u30A9\u30EB\u30C8: \u4ECA\u65E5)").action(async (opts) => {
2149
+ const ctx = await createAppContext();
2150
+ const date = opts.date ?? todayDateString();
2151
+ const tasks = await ctx.repos.task.findByDate(date);
2152
+ if (tasks.length === 0) {
2153
+ console.log(`No tasks for ${date}`);
2154
+ return;
2155
+ }
2156
+ console.log(`${date}:`);
2157
+ console.table(tasks.map(formatTaskRow));
2158
+ });
2159
+ }
2160
+ function tasksPauseCommand() {
2161
+ return new Command("pause").description("\u8A08\u6E2C\u4E2D\u306E\u30BF\u30B9\u30AF\u3092\u4E00\u6642\u505C\u6B62\u3059\u308B (status \u2192 paused)").argument("<id-prefix>", "\u30BF\u30B9\u30AF id \u306E\u5148\u982D 4 \u6587\u5B57\u4EE5\u4E0A").option("-d, --date <date>", "\u5BFE\u8C61\u65E5 (\u30C7\u30D5\u30A9\u30EB\u30C8: \u4ECA\u65E5)").action(async (prefix, opts) => {
2162
+ const ctx = await createAppContext();
2163
+ const date = opts.date ?? todayDateString();
2164
+ const tasks = await ctx.repos.task.findByDate(date);
2165
+ const taskId = resolveTaskIdByPrefix(tasks, prefix);
2166
+ const updated = await pauseTask(ctx.repos.task, taskId);
2167
+ console.log(`\u23F8 Paused: ${shortenId(updated.id)} ${updated.title}`);
2168
+ });
2169
+ }
2170
+ function tasksStartCommand() {
2171
+ return new Command("start").description("\u30BF\u30B9\u30AF\u306E\u8A08\u6E2C\u3092\u958B\u59CB\u3059\u308B (status \u2192 running)").argument("<id-prefix>", "\u30BF\u30B9\u30AF id \u306E\u5148\u982D 4 \u6587\u5B57\u4EE5\u4E0A").option("-d, --date <date>", "\u5BFE\u8C61\u65E5 (\u30C7\u30D5\u30A9\u30EB\u30C8: \u4ECA\u65E5)").action(async (prefix, opts) => {
2172
+ const ctx = await createAppContext();
2173
+ const date = opts.date ?? todayDateString();
2174
+ const tasks = await ctx.repos.task.findByDate(date);
2175
+ const taskId = resolveTaskIdByPrefix(tasks, prefix);
2176
+ const updated = await startTask(ctx.repos.task, taskId);
2177
+ console.log(`\u25B6 Started: ${shortenId(updated.id)} ${updated.title}`);
2178
+ });
2179
+ }
2180
+
2181
+ // src/commands/tasks/index.ts
2182
+ function tasksCommand() {
2183
+ return new Command("tasks").description("\u30BF\u30B9\u30AF\u306E\u4E00\u89A7/\u8FFD\u52A0/\u64CD\u4F5C").addCommand(tasksListCommand()).addCommand(tasksAddCommand()).addCommand(tasksStartCommand()).addCommand(tasksPauseCommand()).addCommand(tasksDoneCommand());
2184
+ }
2185
+ function unlockCommand() {
2186
+ return new Command("unlock").description("E2EE passphrase \u3092 keyring \u306B\u4FDD\u5B58\u3057\u3066\u4EE5\u964D\u306E\u30B3\u30DE\u30F3\u30C9\u3092\u5BFE\u8A71\u306A\u3057\u306B\u8D70\u3089\u305B\u308B").action(async () => {
2187
+ const supabase = createNodeSupabaseClient(readSupabaseConfigFromEnv(), {
2188
+ storage: new KeyringSessionStore()
2189
+ });
2190
+ const {
2191
+ data: { user },
2192
+ error: authError
2193
+ } = await supabase.auth.getUser();
2194
+ if (authError || !user) {
2195
+ throw new CliError("Not logged in. Run `taskchute login` first.");
2196
+ }
2197
+ const userKeysRepo = new SupabaseUserKeysRepository(supabase);
2198
+ const snapshot = await userKeysRepo.getOwn();
2199
+ if (!snapshot) {
2200
+ throw new CliError(
2201
+ "E2EE not set up. Complete setup from the web app first (Settings \u2192 \u30C7\u30FC\u30BF\u306E\u6697\u53F7\u5316)."
2202
+ );
2203
+ }
2204
+ const envPw = process.env.TASKCHUTE_PASSPHRASE;
2205
+ const pw = envPw ?? await password({ message: "Passphrase:", mask: "*" });
2206
+ try {
2207
+ await unlockMasterKey(snapshot, { kind: "passphrase", passphrase: pw });
2208
+ } catch (err) {
2209
+ if (err instanceof UnlockError) {
2210
+ throw new CliError(`Unlock failed: ${err.message}`);
2211
+ }
2212
+ throw err;
2213
+ }
2214
+ new KeyringPassphraseStore().set(pw);
2215
+ console.log("unlock OK \u26A1");
2216
+ });
2217
+ }
2218
+ function whoamiCommand() {
2219
+ return new Command("whoami").description("\u73FE\u5728\u30ED\u30B0\u30A4\u30F3\u4E2D\u306E\u30E6\u30FC\u30B6\u30FC (Supabase auth) \u3092\u8868\u793A\u3059\u308B").action(async () => {
2220
+ const config = readSupabaseConfigFromEnv();
2221
+ const supabase = createNodeSupabaseClient(config, {
2222
+ storage: new KeyringSessionStore()
2223
+ });
2224
+ const { data, error } = await supabase.auth.getUser();
2225
+ if (error || !data.user) {
2226
+ console.log("\u672A\u30ED\u30B0\u30A4\u30F3 (taskchute login \u3067\u5165\u3063\u3066\u301C)");
2227
+ process.exitCode = 1;
2228
+ return;
2229
+ }
2230
+ console.log(`logged in as: ${data.user.email ?? data.user.id}`);
2231
+ });
2232
+ }
2233
+
2234
+ // src/index.ts
2235
+ var program = new Command();
2236
+ program.name("taskchute").description("TaskChute CLI \u2014 CLI/MCP \u304B\u3089 TaskChute \u306B\u30A2\u30AF\u30BB\u30B9\u3059\u308B\u305F\u3081\u306E\u30A8\u30F3\u30C8\u30EA\u30DD\u30A4\u30F3\u30C8").version("0.0.0");
2237
+ program.addCommand(loginCommand());
2238
+ program.addCommand(whoamiCommand());
2239
+ program.addCommand(logoutCommand());
2240
+ program.addCommand(unlockCommand());
2241
+ program.addCommand(lockCommand());
2242
+ program.addCommand(tasksCommand());
2243
+ program.parseAsync(process.argv).catch((err) => {
2244
+ if (err instanceof CliError) {
2245
+ console.error(err.message);
2246
+ process.exit(err.exitCode);
2247
+ }
2248
+ console.error(err instanceof Error ? err.message : String(err));
2249
+ process.exit(1);
2250
+ });