diff --git a/README.md b/README.md index 8cd9243..c2869ca 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,27 @@ Quick Capture 창이 화면 중앙 상단에 뜬다. 한 줄 던지고 `Ctrl+Ent --- +## 패키징 (Windows NSIS 인스톨러) + +```bash +# Windows 개발자 모드 ON 필요 (winCodeSign 캐시 추출 시 darwin symlink 풀어야 해서) +# 설정 → 시스템 → 개발자용 → 개발자 모드 ON + +npm run dist # NSIS 인스톨러: dist/Inkling Setup x.y.z.exe +npm run dist:dir # 패키징 없이 win-unpacked 디렉터리만 +``` + +산출물: +- `dist/Inkling Setup 0.2.0.exe` — 약 100MB, oneClick=false (설치 위치 선택 가능) +- `dist/win-unpacked/` — portable 디렉터리, 그대로 실행 가능 + +설치 후: +- 첫 실행 시 `app.isPackaged === true` 면 `<프로필>/.autostart-init` 마커가 없을 때 한정 자동 시작 ON 으로 설정 (`--hidden` 인자 포함, inbox 창 안 뜨고 트레이만) +- 이후 트레이 메뉴 → "윈도우 시작 시 자동 실행" 토글로 조작 +- 자동 시작 시 inbox 창은 안 뜸. `Ctrl+Shift+J` 또는 트레이 클릭으로 호출 + +--- + ## 테스트 ```bash diff --git a/docs/superpowers/plans/2026-04-26-f6-l1-local-snapshot.md b/docs/superpowers/plans/2026-04-26-f6-l1-local-snapshot.md new file mode 100644 index 0000000..36afbdc --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-f6-l1-local-snapshot.md @@ -0,0 +1,962 @@ +# F6-L1 Local Snapshot Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Atomic SQLite snapshot to `/backups/` with GFS retention (14 daily · 4 weekly · 6 monthly), tray "지금 백업" entry, and on-quit + on-startup daily triggers — protecting dogfood data against accidental delete, DB corruption, and bad migrations. + +**Architecture:** A pure rotation function (`applyGfsRetention`) drives retention math without filesystem deps. `BackupService` orchestrates `db.backup()` writes via atomic temp-file + rename, plus marker-file gating to skip redundant same-day backups. Service instantiated in `main/index.ts`, wired to `app.whenReady` (daily check) + `before-quit` (final flush) + tray callback. + +**Tech Stack:** TypeScript, better-sqlite3 12.9.0 (`db.backup()` async API), Electron 41.3.0 (`app.on('before-quit')`), vitest 4.1.5, Node `fs/promises`. + +--- + +## File Structure + +**Create:** +- `src/main/services/backupRotation.ts` — pure GFS retention function +- `src/main/services/BackupService.ts` — orchestrator (snapshot + rotate + runDaily) +- `tests/unit/backupRotation.test.ts` — rotation policy tests +- `tests/unit/BackupService.test.ts` — service-level tests with real :memory: DB + tmp dir + +**Modify:** +- `src/main/index.ts` — wire BackupService, schedule whenReady + before-quit, pass callback to tray +- `src/main/tray.ts` — add "지금 백업" menu item, accept `runBackup` callback +- `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` — F6-L1 status 🌱 → 🚀 promoted, add link to spec + +**No schema changes. No new dependencies.** + +--- + +## Task 1: Pure GFS Retention Function + +**Files:** +- Create: `src/main/services/backupRotation.ts` +- Test: `tests/unit/backupRotation.test.ts` + +- [ ] **Step 1: Write failing tests for filename parsing** + +```typescript +// tests/unit/backupRotation.test.ts +import { describe, it, expect } from 'vitest'; +import { parseBackupFilename, applyGfsRetention } from '@main/services/backupRotation.js'; + +describe('parseBackupFilename', () => { + it('extracts ISO date from valid filename', () => { + expect(parseBackupFilename('inkling-2026-04-26.sqlite')).toBe('2026-04-26'); + }); + + it('returns null for non-matching filename', () => { + expect(parseBackupFilename('something-else.sqlite')).toBeNull(); + expect(parseBackupFilename('inkling-2026-13-99.sqlite')).toBeNull(); + expect(parseBackupFilename('.last-snapshot')).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Run test, expect fail** + +Run: `npx vitest run tests/unit/backupRotation.test.ts` +Expected: FAIL — `Cannot find module '@main/services/backupRotation.js'` + +- [ ] **Step 3: Write minimal implementation** + +```typescript +// src/main/services/backupRotation.ts +const BACKUP_FILENAME_REGEX = /^inkling-(\d{4}-\d{2}-\d{2})\.sqlite$/; + +export function parseBackupFilename(name: string): string | null { + const m = BACKUP_FILENAME_REGEX.exec(name); + if (!m) return null; + const iso = m[1]!; + const d = new Date(iso + 'T00:00:00Z'); + if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== iso) return null; + return iso; +} + +export interface RetentionResult { + keep: string[]; + remove: string[]; +} + +export function applyGfsRetention( + _filenames: string[], + _now: Date +): RetentionResult { + return { keep: [], remove: [] }; +} +``` + +- [ ] **Step 4: Run, expect pass** + +Run: `npx vitest run tests/unit/backupRotation.test.ts` +Expected: PASS (parseBackupFilename tests). `applyGfsRetention` tests not yet written. + +- [ ] **Step 5: Write failing tests for GFS retention** + +Append to `tests/unit/backupRotation.test.ts`: + +```typescript +describe('applyGfsRetention', () => { + // KST-naive logic — caller passes UTC `now`. Filenames are KST date keys. + const NOW = new Date('2026-04-26T12:00:00Z'); // 2026-04-26 21:00 KST + + function names(...dates: string[]): string[] { + return dates.map((d) => `inkling-${d}.sqlite`); + } + + it('keeps files within last 14 days (daily window)', () => { + const files = names( + '2026-04-26', '2026-04-25', '2026-04-20', '2026-04-13', '2026-04-12' + ); + const r = applyGfsRetention(files, NOW); + // 14 day window from 2026-04-26 reaches back to 2026-04-13 inclusive. + expect(r.keep).toContain('inkling-2026-04-26.sqlite'); + expect(r.keep).toContain('inkling-2026-04-25.sqlite'); + expect(r.keep).toContain('inkling-2026-04-20.sqlite'); + expect(r.keep).toContain('inkling-2026-04-13.sqlite'); + expect(r.remove).toContain('inkling-2026-04-12.sqlite'); + }); + + it('keeps last 4 Mondays beyond the 14 day window', () => { + // Mondays in 2026: 04-13, 04-06, 03-30, 03-23, 03-16, 03-09 + const files = names( + '2026-04-13', // within 14-day, also a Monday + '2026-04-06', // outside 14-day, but a Monday in last 4 weeks + '2026-03-30', // a Monday in last 4 weeks + '2026-03-23', // a Monday in last 4 weeks + '2026-03-16', // a Monday more than 4 weeks ago — REMOVE unless month-1 + '2026-03-09' // a Monday more than 4 weeks ago — REMOVE + ); + const r = applyGfsRetention(files, NOW); + expect(r.keep).toContain('inkling-2026-04-06.sqlite'); + expect(r.keep).toContain('inkling-2026-03-30.sqlite'); + expect(r.keep).toContain('inkling-2026-03-23.sqlite'); + expect(r.remove).toContain('inkling-2026-03-16.sqlite'); + expect(r.remove).toContain('inkling-2026-03-09.sqlite'); + }); + + it('keeps month-firsts within last 6 months', () => { + // Last 6 month-firsts from 2026-04-26: 2026-04-01, 2026-03-01, 2026-02-01, + // 2026-01-01, 2025-12-01, 2025-11-01 + const files = names( + '2026-04-01', // within 14-day already + '2026-03-01', // outside 14-day, outside 4-week-Monday — keep via month rule + '2026-02-01', + '2026-01-01', + '2025-12-01', + '2025-11-01', + '2025-10-01' // outside 6-month window — REMOVE + ); + const r = applyGfsRetention(files, NOW); + expect(r.keep).toContain('inkling-2026-03-01.sqlite'); + expect(r.keep).toContain('inkling-2026-02-01.sqlite'); + expect(r.keep).toContain('inkling-2025-11-01.sqlite'); + expect(r.remove).toContain('inkling-2025-10-01.sqlite'); + }); + + it('ignores files that do not match backup pattern', () => { + const files = ['random.sqlite', 'inkling.sqlite', '.last-snapshot', 'inkling-bad-date.sqlite']; + const r = applyGfsRetention(files, NOW); + expect(r.keep).toEqual([]); + expect(r.remove).toEqual([]); + }); + + it('keeps future-dated files (clock skew safety)', () => { + const files = names('2030-01-01'); + const r = applyGfsRetention(files, NOW); + expect(r.keep).toContain('inkling-2030-01-01.sqlite'); + expect(r.remove).toEqual([]); + }); + + it('a file kept by any rule is in keep, never in both lists', () => { + const files = names('2026-04-26', '2026-04-13', '2026-03-23', '2026-03-01'); + const r = applyGfsRetention(files, NOW); + const intersection = r.keep.filter((f) => r.remove.includes(f)); + expect(intersection).toEqual([]); + }); +}); +``` + +- [ ] **Step 6: Run, expect fail** + +Run: `npx vitest run tests/unit/backupRotation.test.ts` +Expected: FAIL — applyGfsRetention returns empty arrays. + +- [ ] **Step 7: Implement applyGfsRetention** + +Replace the stub in `src/main/services/backupRotation.ts`: + +```typescript +const BACKUP_FILENAME_REGEX = /^inkling-(\d{4}-\d{2}-\d{2})\.sqlite$/; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; +const DAILY_WINDOW_DAYS = 14; +const WEEKLY_WINDOW_COUNT = 4; +const MONTHLY_WINDOW_COUNT = 6; + +export function parseBackupFilename(name: string): string | null { + const m = BACKUP_FILENAME_REGEX.exec(name); + if (!m) return null; + const iso = m[1]!; + const d = new Date(iso + 'T00:00:00Z'); + if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== iso) return null; + return iso; +} + +export interface RetentionResult { + keep: string[]; + remove: string[]; +} + +function isoDateUtc(d: Date): string { + return d.toISOString().slice(0, 10); +} + +function startOfDayUtc(d: Date): Date { + const x = new Date(d); + x.setUTCHours(0, 0, 0, 0); + return x; +} + +function isWithinDailyWindow(fileDate: Date, now: Date): boolean { + const today = startOfDayUtc(now); + const oldest = new Date(today.getTime() - (DAILY_WINDOW_DAYS - 1) * ONE_DAY_MS); + return fileDate >= oldest && fileDate <= today; +} + +function isWithinWeeklyWindow(fileDate: Date, now: Date): boolean { + // UTC-based Monday detection. UTCDay: 0=Sun, 1=Mon..6=Sat + if (fileDate.getUTCDay() !== 1) return false; + const today = startOfDayUtc(now); + const oldest = new Date(today.getTime() - WEEKLY_WINDOW_COUNT * 7 * ONE_DAY_MS); + return fileDate >= oldest && fileDate <= today; +} + +function isWithinMonthlyWindow(fileDate: Date, now: Date): boolean { + if (fileDate.getUTCDate() !== 1) return false; + const today = startOfDayUtc(now); + // months ago: difference in calendar months + const monthsAgo = + (today.getUTCFullYear() - fileDate.getUTCFullYear()) * 12 + + (today.getUTCMonth() - fileDate.getUTCMonth()); + return monthsAgo >= 0 && monthsAgo < MONTHLY_WINDOW_COUNT; +} + +export function applyGfsRetention(filenames: string[], now: Date): RetentionResult { + const keep: string[] = []; + const remove: string[] = []; + for (const name of filenames) { + const iso = parseBackupFilename(name); + if (iso === null) continue; // unrecognized — ignore (no-op) + const fileDate = new Date(iso + 'T00:00:00Z'); + if (fileDate > startOfDayUtc(now)) { + keep.push(name); // future-dated — clock skew safety + continue; + } + const survives = + isWithinDailyWindow(fileDate, now) || + isWithinWeeklyWindow(fileDate, now) || + isWithinMonthlyWindow(fileDate, now); + if (survives) keep.push(name); + else remove.push(name); + } + return { keep, remove }; +} +``` + +- [ ] **Step 8: Run all tests for this file, expect pass** + +Run: `npx vitest run tests/unit/backupRotation.test.ts` +Expected: PASS — all parseBackupFilename + applyGfsRetention tests green. + +- [ ] **Step 9: Commit** + +```bash +git add src/main/services/backupRotation.ts tests/unit/backupRotation.test.ts +git commit -m "feat(backup): GFS retention policy (pure) + +14 daily + 4 weekly (Mondays) + 6 monthly (1st). Future-dated files +preserved (clock skew). Unrecognized filenames ignored (no delete)." +``` + +--- + +## Task 2: BackupService — snapshot() + +**Files:** +- Create: `src/main/services/BackupService.ts` +- Test: `tests/unit/BackupService.test.ts` + +- [ ] **Step 1: Write failing tests for snapshot()** + +```typescript +// tests/unit/BackupService.test.ts +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import Database from 'better-sqlite3'; +import { mkdtempSync, rmSync, existsSync, readdirSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { runMigrations } from '@main/db/migrations/index.js'; +import { BackupService } from '@main/services/BackupService.js'; + +describe('BackupService.snapshot', () => { + let dir: string; + let db: Database.Database; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'inkling-backup-')); + db = new Database(':memory:'); + runMigrations(db); + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES (?, ?, 'pending', ?, ?)` + ).run('n1', 'hello', '2026-04-26T00:00:00Z', '2026-04-26T00:00:00Z'); + }); + + afterEach(() => { + db.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + it('writes inkling-YYYY-MM-DD.sqlite (KST date) to backupDir', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); // 21:00 KST + const r = await svc.snapshot(); + expect(r.path).toBe(join(dir, 'inkling-2026-04-26.sqlite')); + expect(existsSync(r.path)).toBe(true); + expect(r.bytes).toBeGreaterThan(0); + }); + + it('uses KST date even when UTC date differs (around midnight)', async () => { + // 2026-04-26 23:30 UTC = 2026-04-27 08:30 KST + const svc = new BackupService(db, dir, () => new Date('2026-04-26T23:30:00Z')); + const r = await svc.snapshot(); + expect(r.path).toBe(join(dir, 'inkling-2026-04-27.sqlite')); + }); + + it('overwrites same-day backup atomically (no partial files left)', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.snapshot(); + await svc.snapshot(); + const files = readdirSync(dir).filter((f) => f.startsWith('inkling-')); + expect(files).toEqual(['inkling-2026-04-26.sqlite']); + // No leftover .tmp files + expect(readdirSync(dir).some((f) => f.endsWith('.tmp'))).toBe(false); + }); + + it('snapshot file is a valid SQLite DB containing the source row', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.snapshot(); + const restored = new Database(r.path, { readonly: true }); + const row = restored.prepare('SELECT id, raw_text FROM notes').get() as + | { id: string; raw_text: string } + | undefined; + expect(row?.id).toBe('n1'); + expect(row?.raw_text).toBe('hello'); + restored.close(); + }); + + it('creates backupDir if it does not exist', async () => { + const fresh = join(dir, 'nested', 'backups'); + expect(existsSync(fresh)).toBe(false); + const svc = new BackupService(db, fresh, () => new Date('2026-04-26T12:00:00Z')); + await svc.snapshot(); + expect(existsSync(fresh)).toBe(true); + }); + + it('snapshot file is not zero bytes (regression: empty backup)', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.snapshot(); + expect(statSync(r.path).size).toBeGreaterThan(100); + }); +}); +``` + +- [ ] **Step 2: Run, expect fail** + +Run: `npx vitest run tests/unit/BackupService.test.ts` +Expected: FAIL — `Cannot find module '@main/services/BackupService.js'` + +- [ ] **Step 3: Implement BackupService.snapshot** + +```typescript +// src/main/services/BackupService.ts +import type Database from 'better-sqlite3'; +import { mkdir, rename, stat, readdir, unlink } from 'node:fs/promises'; +import { join } from 'node:path'; +import { applyGfsRetention } from './backupRotation.js'; + +const KST_OFFSET_MS = 9 * 60 * 60 * 1000; + +function toKstDateKey(d: Date): string { + const k = new Date(d.getTime() + KST_OFFSET_MS); + return k.toISOString().slice(0, 10); +} + +export interface SnapshotResult { + path: string; + bytes: number; +} + +export interface RotateResult { + kept: string[]; + removed: string[]; +} + +export class BackupService { + constructor( + private db: Database.Database, + private backupDir: string, + private now: () => Date = () => new Date() + ) {} + + async snapshot(): Promise { + await mkdir(this.backupDir, { recursive: true }); + const dateKey = toKstDateKey(this.now()); + const finalPath = join(this.backupDir, `inkling-${dateKey}.sqlite`); + const tmpPath = `${finalPath}.tmp`; + await this.db.backup(tmpPath); + await rename(tmpPath, finalPath); + const st = await stat(finalPath); + return { path: finalPath, bytes: st.size }; + } + + async rotate(): Promise { + let entries: string[]; + try { + entries = await readdir(this.backupDir); + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') return { kept: [], removed: [] }; + throw e; + } + const decision = applyGfsRetention(entries, this.now()); + for (const name of decision.remove) { + await unlink(join(this.backupDir, name)); + } + return { kept: decision.keep, removed: decision.remove }; + } +} +``` + +- [ ] **Step 4: Run snapshot tests, expect pass** + +Run: `npx vitest run tests/unit/BackupService.test.ts -t snapshot` +Expected: PASS — all 6 snapshot tests green. + +- [ ] **Step 5: Commit** + +```bash +git add src/main/services/BackupService.ts tests/unit/BackupService.test.ts +git commit -m "feat(backup): atomic SQLite snapshot to inkling-YYYY-MM-DD.sqlite + +KST date filename, tmp+rename atomic write, mkdir on demand." +``` + +--- + +## Task 3: BackupService — runDaily() with marker + +**Files:** +- Modify: `src/main/services/BackupService.ts` +- Modify: `tests/unit/BackupService.test.ts` + +- [ ] **Step 1: Write failing tests for runDaily** + +Append to `tests/unit/BackupService.test.ts`: + +```typescript +import { readFileSync, writeFileSync } from 'node:fs'; + +describe('BackupService.runDaily', () => { + let dir: string; + let db: Database.Database; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'inkling-backup-')); + db = new Database(':memory:'); + runMigrations(db); + }); + + afterEach(() => { + db.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + it('snapshots when marker is absent', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.runDaily(); + expect(r.snapshotted).toBe(true); + expect(existsSync(join(dir, '.last-snapshot'))).toBe(true); + expect(existsSync(join(dir, 'inkling-2026-04-26.sqlite'))).toBe(true); + }); + + it('skips when marker shows today already snapshotted', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.runDaily(); // first + const r = await svc.runDaily(); // second same day + expect(r.snapshotted).toBe(false); + expect(r.reason).toMatch(/already/); + }); + + it('snapshots again when marker shows different date', async () => { + // Pre-seed marker as yesterday + const dir2 = dir; + await new BackupService(db, dir2, () => new Date('2026-04-25T12:00:00Z')).runDaily(); + const svc = new BackupService(db, dir2, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.runDaily(); + expect(r.snapshotted).toBe(true); + expect(existsSync(join(dir2, 'inkling-2026-04-26.sqlite'))).toBe(true); + expect(existsSync(join(dir2, 'inkling-2026-04-25.sqlite'))).toBe(true); + }); + + it('runs rotation after snapshot', async () => { + // Pre-create an old file that should be rotated out + const ancient = join(dir, 'inkling-2024-01-01.sqlite'); + writeFileSync(ancient, 'fake'); + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.runDaily(); + expect(r.snapshotted).toBe(true); + expect(r.removed).toContain('inkling-2024-01-01.sqlite'); + expect(existsSync(ancient)).toBe(false); + }); + + it('marker contains ISO date matching the snapshot file', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.runDaily(); + const marker = readFileSync(join(dir, '.last-snapshot'), 'utf8').trim(); + expect(marker).toBe('2026-04-26'); + }); + + it('lastSnapshotAt returns null when marker absent', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + expect(await svc.lastSnapshotAt()).toBeNull(); + }); + + it('lastSnapshotAt returns marker date when present', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.runDaily(); + expect(await svc.lastSnapshotAt()).toBe('2026-04-26'); + }); +}); +``` + +- [ ] **Step 2: Run, expect fail** + +Run: `npx vitest run tests/unit/BackupService.test.ts -t runDaily` +Expected: FAIL — `runDaily is not a function` + +- [ ] **Step 3: Implement runDaily + lastSnapshotAt** + +Replace `src/main/services/BackupService.ts` with this expanded version: + +```typescript +import type Database from 'better-sqlite3'; +import { mkdir, rename, stat, readdir, unlink, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { applyGfsRetention } from './backupRotation.js'; + +const KST_OFFSET_MS = 9 * 60 * 60 * 1000; +const MARKER_FILENAME = '.last-snapshot'; + +function toKstDateKey(d: Date): string { + const k = new Date(d.getTime() + KST_OFFSET_MS); + return k.toISOString().slice(0, 10); +} + +export interface SnapshotResult { + path: string; + bytes: number; +} + +export interface RotateResult { + kept: string[]; + removed: string[]; +} + +export interface DailyResult { + snapshotted: boolean; + reason?: string; + path?: string; + bytes?: number; + kept?: string[]; + removed?: string[]; +} + +export class BackupService { + constructor( + private db: Database.Database, + private backupDir: string, + private now: () => Date = () => new Date() + ) {} + + async snapshot(): Promise { + await mkdir(this.backupDir, { recursive: true }); + const dateKey = toKstDateKey(this.now()); + const finalPath = join(this.backupDir, `inkling-${dateKey}.sqlite`); + const tmpPath = `${finalPath}.tmp`; + await this.db.backup(tmpPath); + await rename(tmpPath, finalPath); + const st = await stat(finalPath); + return { path: finalPath, bytes: st.size }; + } + + async rotate(): Promise { + let entries: string[]; + try { + entries = await readdir(this.backupDir); + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') return { kept: [], removed: [] }; + throw e; + } + const decision = applyGfsRetention(entries, this.now()); + for (const name of decision.remove) { + await unlink(join(this.backupDir, name)); + } + return { kept: decision.keep, removed: decision.remove }; + } + + async lastSnapshotAt(): Promise { + try { + const raw = await readFile(join(this.backupDir, MARKER_FILENAME), 'utf8'); + return raw.trim() || null; + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw e; + } + } + + async runDaily(): Promise { + const today = toKstDateKey(this.now()); + const last = await this.lastSnapshotAt(); + if (last === today) { + return { snapshotted: false, reason: 'already snapshotted today' }; + } + const snap = await this.snapshot(); + await writeFile(join(this.backupDir, MARKER_FILENAME), today, 'utf8'); + const rot = await this.rotate(); + return { + snapshotted: true, + path: snap.path, + bytes: snap.bytes, + kept: rot.kept, + removed: rot.removed + }; + } +} +``` + +- [ ] **Step 4: Run all BackupService tests, expect pass** + +Run: `npx vitest run tests/unit/BackupService.test.ts` +Expected: PASS — both snapshot + runDaily groups (≥ 13 tests). + +- [ ] **Step 5: Run full test suite to verify no regressions** + +Run: `npm test` +Expected: PASS — original 52 + new tests (≈ 65+). + +- [ ] **Step 6: Run typecheck** + +Run: `npm run typecheck` +Expected: PASS — 0 errors. + +- [ ] **Step 7: Commit** + +```bash +git add src/main/services/BackupService.ts tests/unit/BackupService.test.ts +git commit -m "feat(backup): runDaily() with .last-snapshot marker + rotate after snapshot + +Skips when marker matches today's KST date. Marker written after +successful snapshot, before rotation. lastSnapshotAt() exposed for UI." +``` + +--- + +## Task 4: Wire BackupService into main process + +**Files:** +- Modify: `src/main/index.ts` +- Modify: `src/main/tray.ts` + +- [ ] **Step 1: Modify main/index.ts to instantiate BackupService** + +In `src/main/index.ts`, add the import block alongside existing service imports (after `import { MediaGc }`): + +```typescript +import { BackupService } from './services/BackupService.js'; +``` + +Inside `app.whenReady().then(async () => { ... })`, after `const gc = new MediaGc(db, store);` block, add: + +```typescript + const backup = new BackupService(db, join(paths.profileDir, 'backups')); + void backup.runDaily() + .then((r) => logger.info('backup.daily', { ...r } as Record)) + .catch((e) => logger.warn('backup.daily.failed', { reason: String(e) })); +``` + +(`join` is already imported at the top of the file from the autostart-init logic added in v0.2.0.) + +- [ ] **Step 2: Add before-quit hook** + +Inside `app.whenReady().then(...)`, **after** the `gc` and `backup` initialization, add: + +```typescript + let backupOnQuitDone = false; + app.on('before-quit', (e) => { + if (backupOnQuitDone) return; + e.preventDefault(); + backup.runDaily() + .then((r) => logger.info('backup.beforeQuit', { ...r } as Record)) + .catch((e2) => logger.warn('backup.beforeQuit.failed', { reason: String(e2) })) + .finally(() => { + backupOnQuitDone = true; + app.isQuitting = true; + app.quit(); + }); + }); +``` + +Then **remove** the existing top-level `app.on('before-quit', ...)` line at the bottom of the file: + +```typescript +// REMOVE THIS LINE: +app.on('before-quit', () => { app.isQuitting = true; app.quit(); }); +``` + +(The new hook absorbs `app.isQuitting = true` setting and replaces the trivial one.) + +- [ ] **Step 3: Pass runBackup callback to createTray** + +In `src/main/index.ts`, modify the `createTray` invocation: + +```typescript + // BEFORE: + // createTray( + // () => createInboxWindow(), + // () => showQuickCapture() + // ); + + // AFTER: + createTray( + () => createInboxWindow(), + () => showQuickCapture(), + async () => { + try { + const r = await backup.runDaily(); + new Notification({ + title: 'Inkling', + body: r.snapshotted + ? `백업 완료 — ${r.removed?.length ?? 0}개 정리` + : `오늘 백업이 이미 있습니다`, + silent: true + }).show(); + } catch (e) { + logger.warn('backup.manual.failed', { reason: String(e) }); + new Notification({ + title: 'Inkling', + body: '백업을 만들지 못했습니다.', + silent: true + }).show(); + } + } + ); +``` + +- [ ] **Step 4: Modify tray.ts to accept and use the new callback** + +Replace `src/main/tray.ts` with: + +```typescript +import electron from 'electron'; +import type { Tray as TrayType, MenuItemConstructorOptions } from 'electron'; +const { app, Tray, Menu, nativeImage } = electron; + +let tray: TrayType | null = null; + +function buildMenu( + showInbox: () => void, + showCapture: () => void, + runBackup: () => void +) { + const items: MenuItemConstructorOptions[] = [ + { label: '구출한 메모 보기', click: showInbox }, + { label: '기억 구출하기', click: showCapture }, + { type: 'separator' }, + { label: '지금 백업', click: runBackup } + ]; + if (app.isPackaged) { + const { openAtLogin } = app.getLoginItemSettings(); + items.push({ + label: '윈도우 시작 시 자동 실행', + type: 'checkbox', + checked: openAtLogin, + click: (item) => { + app.setLoginItemSettings({ + openAtLogin: item.checked, + args: ['--hidden'] + }); + } + }); + items.push({ type: 'separator' }); + } else { + items.push({ type: 'separator' }); + } + items.push({ label: '종료', click: () => { app.isQuitting = true; app.quit(); } }); + return Menu.buildFromTemplate(items); +} + +export function createTray( + showInbox: () => void, + showCapture: () => void, + runBackup: () => void +): TrayType { + const icon = nativeImage.createEmpty(); + tray = new Tray(icon); + tray.setToolTip('Inkling'); + tray.setContextMenu(buildMenu(showInbox, showCapture, runBackup)); + tray.on('click', showInbox); + return tray; +} +``` + +- [ ] **Step 5: Run typecheck** + +Run: `npm run typecheck` +Expected: PASS — 0 errors. Verifies tray callback signature change is consistent. + +- [ ] **Step 6: Run unit tests** + +Run: `npm test` +Expected: PASS — 65+ tests, no regressions. + +- [ ] **Step 7: Run e2e smoke** + +Run: `npm run test:e2e` +Expected: PASS — 1/1. Tray "지금 백업" entry doesn't break inbox empty-state assertion. + +- [ ] **Step 8: Commit** + +```bash +git add src/main/index.ts src/main/tray.ts +git commit -m "feat(backup): wire BackupService — whenReady + before-quit + tray + +Instantiate BackupService at app.whenReady, run daily snapshot then +again before quit (synchronous-blocking via preventDefault). Tray menu +gets '지금 백업' entry that triggers manual runDaily with native +toast feedback." +``` + +--- + +## Task 5: Promote F6-L1 in feedback collection + +**Files:** +- Modify: `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` +- Create: `docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md` (extracted spec) + +- [ ] **Step 1: Create extracted spec file** + +Create `docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md`: + +```markdown +# F6-L1 Local Snapshot Spec (Promoted) + +**Extracted from:** `2026-04-25-dogfood-feedback.md` F6 §"L1 — 로컬 원자 스냅샷" +**Plan:** `docs/superpowers/plans/2026-04-26-f6-l1-local-snapshot.md` +**Status:** 🚀 promoted — implemented 2026-04-26 + +## 결정 (mini-brainstorm 결과) + +| 결정 항목 | 값 | 근거 | +|----------|-----|------| +| 백업 위치 | `/backups/` | 프로필 단위 묶음, 코드 단순. 외부 디렉터리/사용자 지정 경로는 후속. | +| 파일명 | `inkling-YYYY-MM-DD.sqlite` (KST 날짜) | 인간 가독 + 정렬 친화 | +| 마커 | `/backups/.last-snapshot` (ISO 날짜 본문) | 같은 날 중복 백업 방지 | +| 트리거 | `app.whenReady` 시 1회 + `before-quit` 1회 + 트레이 "지금 백업" | 슬라이스 §3 외부 dep 없이 만족 | +| 보존 | 14 daily + 4 weekly (월요일) + 6 monthly (1일) | 사용자 결정 | +| 원자성 | tmp + rename | 파셜 파일 방지 | + +## 범위 (PR 안에 포함) + +- `src/main/services/backupRotation.ts` (pure GFS function) +- `src/main/services/BackupService.ts` (snapshot + rotate + runDaily + lastSnapshotAt) +- `src/main/index.ts` 수정 (whenReady wiring + before-quit hook 합치기 + createTray 콜백 추가) +- `src/main/tray.ts` 수정 (지금 백업 메뉴) +- `tests/unit/backupRotation.test.ts` +- `tests/unit/BackupService.test.ts` + +## 후속 (별 spec 또는 후속 항목 후보) + +- 외부 디렉터리 백업 (예: OneDrive 폴더 지정) +- 미디어 포함 백업 +- 암호화 (SQLCipher / age) +- 백업 무결성 검증 (PRAGMA integrity_check) +- F6-L2 git sync 와 연결 (백업 디렉터리도 sync 대상에 포함할지) +``` + +- [ ] **Step 2: Update dogfood-feedback.md F6 status to promoted** + +In `docs/superpowers/specs/2026-04-25-dogfood-feedback.md`, replace the F6 section header: + +```markdown +## F6. 메모 데이터 백업 + 복원 (3-layer 권장) (🌱 raw) +``` + +with: + +```markdown +## F6. 메모 데이터 백업 + 복원 (3-layer 권장) (🔬 drafting — L1 promoted) + +**진행 상태:** +- L1 (로컬 스냅샷) — 🚀 promoted → `docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md` +- L2 (git sync) — 🌱 raw, 7번 항목으로 예정 +- L3 (import) — 🌱 raw, 3번 항목으로 예정 (F5 후) +``` + +(Keep the rest of F6 content as-is — only the header line + a small status block change.) + +- [ ] **Step 3: Run final verification** + +Run: `npm run typecheck && npm test && npm run test:e2e` +Expected: All green. Typecheck 0 errors, ≥ 65 unit tests pass, e2e 1/1. + +- [ ] **Step 4: Commit promotion** + +```bash +git add docs/superpowers/specs/2026-04-25-dogfood-feedback.md docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md +git commit -m "docs(spec): promote F6-L1 local snapshot + +Extracted to its own spec, dogfood-feedback.md F6 header reflects +L1 promoted status while L2/L3 remain raw." +``` + +- [ ] **Step 5: Final commit — link plan to spec** + +(Already done if the plan file exists at `docs/superpowers/plans/2026-04-26-f6-l1-local-snapshot.md`. If not, create it from this document.) + +--- + +## Self-Review Notes + +**Spec coverage check:** + +- ✅ BackupService + db.backup() wrapper — Tasks 2, 3 +- ✅ GFS rotation 14·4·6 — Task 1 +- ✅ 트레이 "지금 백업" — Task 4 +- ✅ before-quit hook — Task 4 +- ✅ `.last-snapshot` 마커 — Task 3 +- ✅ 단위 테스트 (로테이션) — Task 1 +- ✅ Out (외부 디렉터리·암호화·미디어 포함) — explicitly excluded, deferred to spec §"후속" +- ✅ 슬라이스 §1.3 silent invariant ("데이터 손실 0회") — F6-L1 ships first satisfies this + +**Type consistency check:** + +- `SnapshotResult { path, bytes }` — used in Task 2 + Task 3 (runDaily returns `path`/`bytes` on success) +- `RotateResult { kept, removed }` — used in Task 2 + Task 3 (runDaily returns `kept`/`removed` on success) +- `DailyResult { snapshotted, reason?, path?, bytes?, kept?, removed? }` — Task 3 only +- `applyGfsRetention(filenames, now): RetentionResult` — Task 1, called by `BackupService.rotate()` in Task 2 +- `parseBackupFilename(name): string | null` — Task 1, used internally by Task 1 only +- `now: () => Date` injected — pattern matches `ContinuityService` constructor signature + +**Risk:** `db.backup(tmpPath)` returns a Promise in better-sqlite3 12.x. Verified by `package.json` pinning to 12.9.0 and the @types/better-sqlite3 7.6.11 declaration includes `backup()`. If the API surface differs in this exact version, Task 2 Step 4 will fail loudly during test run, providing fast feedback. + +**No placeholders. No TBD. No "implement later".** Every step has either a code block or an exact command. diff --git a/docs/superpowers/specs/2026-04-25-dogfood-feedback.md b/docs/superpowers/specs/2026-04-25-dogfood-feedback.md index cb2cf2e..8e999a9 100644 --- a/docs/superpowers/specs/2026-04-25-dogfood-feedback.md +++ b/docs/superpowers/specs/2026-04-25-dogfood-feedback.md @@ -159,6 +159,649 @@ H1 이 미달이면 본 항목 ❌ rejected. --- +## F3. "구출" 카피가 한국어로 어색함 (🌱 raw) + +**발견:** 2026-04-26 dogfood 첫날, 메모 1건 캡처 후 OS 토스트 알림에서 "방금 하나의 업무 기억을 구출했습니다" 문구가 떴을 때. + +### 관찰 + +`구출(rescue)` 은 한국어 일상어로 거의 쓰이지 않는다. 인질·재난 맥락에서 주로 쓰이는 어휘라서 "메모 한 줄 적었더니 구출됐다" 라는 표현이 어색하게 들린다. 영어 원문 의도(rescue a thought before you lose it) 를 직역한 결과로 추정. + +현재 "구출" 등장 표면 (코드 경로 기준): + +| # | 표면 | 위치 | 문구 | +|---|------|------|------| +| 1 | OS 토스트 (회전 카피 4종 중 3번째) | `src/main/services/NotificationService.ts:6` | `방금 하나의 업무 기억을 구출했습니다.` | +| 2 | 트레이 메뉴 | `src/main/tray.ts:13` | `기억 구출하기` | +| 3 | 트레이 메뉴 | `src/main/tray.ts:12` | `구출한 메모 보기` | +| 4 | Inbox 빈 상태 | `src/renderer/inbox/App.tsx:44` | `첫 기억을 구출해보세요.` | +| 5 | QuickCapture 힌트 | `src/renderer/quickcapture/App.tsx:68` | `Ctrl+Enter 구출 · Esc 취소 · 이미지 붙여넣기` | + +추가 영향 표면 (코드 외): +- `package.json` `description: "Inkling — local-first 기억 구출 도구"` +- `tests/e2e/smoke.spec.ts:29` 의 단언 `await expect(inbox.getByText('첫 기억을 구출해보세요.')).toBeVisible()` +- `docs/superpowers/specs/2026-04-24-inkling-vertical-slice-design.md` §5.5 의 카피 테이블 (519~523행) +- `docs/superpowers/strategy/strategy.md` 의 §1, §3 — "메모 작성 → 기억 구출" 으로 핵심 행동을 재정의한 **전략적 선언** 그 자체 + +### 제안 방향 + +**카피 변경 + strategy 문서의 어휘 결정 재검토를 한 항목으로 묶음.** + +대체 후보 (전략 의도 보존도 vs 자연스러움 트레이드오프): + +| 후보 | 의도 보존 | 자연스러움 | 비고 | +|------|----------|-----------|------| +| **꺼내 놓기** | 중 | 상 | "머릿속에서 꺼낸다" 직관, 기존 strategy "꺼내는 1회 행동" 표현과 일치 | +| **잡아두기** | 중 | 상 | "잊기 전에 잡아둔다" 어감, 가벼움 | +| **한 줄 던지기** | 상 | 상 | 슬라이스 §1.1 "3초 안에 던지고" 표현 그대로. 캡처 행위에 적합 | +| **남겨두기** | 하 | 상 | 평이함, 의도성 약함 | +| **적어두기/메모하기** | 하 | 상 | 전략 의도 상실 ("메모 작성" 회귀) | +| **챙겨두기** | 중 | 상 | 알림 카피에 자연 ("하나 챙겨뒀습니다") | +| **(현행) 구출** | 상 | 하 | 어색함 | + +권장 1차안 — **표면별 다른 동사 허용** (한 단어로 통일 강제 안 함): + +| 표면 | 권장 카피 | +|------|----------| +| 토스트 회전 카피 #3 | `방금 한 줄 잡아뒀습니다.` 또는 `머릿속에서 한 줄 꺼내뒀습니다.` | +| 트레이: 새 메모 | `한 줄 적기` 또는 `빠르게 한 줄` | +| 트레이: Inbox 열기 | `보관한 메모 보기` 또는 `Inkling 열기` | +| 빈 상태 | `첫 한 줄을 던져보세요.` 또는 `머릿속에 떠다니는 한 줄을 적어보세요.` | +| QC 힌트 | `Ctrl+Enter 저장 · Esc 취소 · 이미지 붙여넣기` | + +핵심: "구출" 의 행동적 의미("정리·작성이 아니라 머릿속에서 꺼내기만") 는 보존하되, 직역체 한 단어로 강제하지 않고 표면별 자연 문맥에 맞춰 동사를 분배. + +### 결정 대기 + +1. **Strategy 문서 §1, §3 의 "기억 구출" 선언을 함께 수정할 것인가?** — "구출" 은 단순 UI 카피가 아니라 전략 어휘다. 카피만 바꾸면 strategy 와 코드가 어긋난다. 같이 바꾸려면 strategy.md §1·§3·§7 갱신 + "꺼내기" / "한 줄" 같은 새 키워드 정의. +2. 단일 동사로 통일할 것인가, 표면별 자연어로 분배할 것인가? — 통일은 브랜딩 자산, 분배는 자연스러움. +3. 제품명 표어 ("local-first 기억 구출 도구") 는 어디까지 따라가는가? — `package.json description`, GitHub README 첫 문단, 향후 onboarding 문구 모두 영향. +4. e2e smoke 의 단언 문구 변경 시 카피 freeze 관행을 도입할지 — 카피 변경마다 e2e fix 가 따라오면 마찰 큼. 단언을 더 약하게 (`getByRole('main')` 등) 잡거나 카피 상수 import 가 대안. +5. 캡처-보상 카피 4종 중 #1, #2, #4 (`이 생각은 이제 Inkling이 들고 있습니다.` / `나중에 찾을 수 있게 보관했습니다.` / `기록 완료. 이제 잊어도 됩니다.`) 는 그대로 유지해도 자연스러우니 #3 만 교체로 충분한가? 아니면 전체 톤 재정렬? + +### 가설·측정 + +| # | 가설 | 측정 | +|---|------|------| +| H1 | 본인 dogfood 1주 누적 시 "구출" 표현에 대한 위화감 토스트/메뉴 노출당 ≥ 1회 보고 | 일일 dogfood 로그의 카피 마찰 항목 | +| H2 | 권장안 적용 시 카피 위화감 보고 0건/주로 감소 | 동일 로그 | +| H3 | 외부인(가족·동료) 1명에게 권장안 vs 현행 블라인드 비교 시 권장안이 자연스럽다고 답함 | 정성 1회 | + +### 범위 + +- **In:** + - `NotificationService.REWARD_COPIES` 의 #3 교체 + - 트레이 메뉴 라벨 2개 + - Inbox 빈 상태 + QuickCapture 힌트 + - `package.json description` + - smoke spec 단언 문구 동기화 + - 카피 테이블 (`2026-04-24-inkling-vertical-slice-design.md` §5.5) + - **strategy.md §1·§3·§7 의 "기억 구출" 어휘 결정** 동시 갱신 여부 결정 +- **Out:** + - 회전 카피 4종 전면 재작성 (필요하면 별 항목으로 분리) + - onboarding 흐름 (슬라이스 외) + - 로고·앱 아이콘 텍스트 + - 다국어 (영어 카피) + +### 영향 + +- **Schema:** 없음 +- **코드:** + - `src/main/services/NotificationService.ts` — 회전 카피 한 줄 + - `src/main/tray.ts` — 메뉴 라벨 2곳 + - `src/renderer/inbox/App.tsx` — 빈 상태 문구 + - `src/renderer/quickcapture/App.tsx` — 힌트 + - `package.json` — description + - `tests/e2e/smoke.spec.ts` — 단언 문구 (또는 셀렉터 약화) +- **문서:** + - `docs/superpowers/specs/2026-04-24-inkling-vertical-slice-design.md` 카피 테이블 + - `docs/superpowers/strategy/strategy.md` — 결정 대기 #1 결과에 따라 부분 또는 전면 + - 본 문서 — promoted 시 별 spec 으로 추출 + +### 메모 + +이 항목은 strategy 문서와의 어휘 일관성 결정이 함께 묶여 있어서, 단순 카피 PR 로 끝낼지 / strategy 재검토 미니 spec 으로 승격할지 결정 대기 #1 의 답이 promoted 경로를 좌우함. drafting 단계에서 결정 대기 #1 부터 답하고 ready-for-spec 으로 넘기는 순서 권장. + +--- + +## F4. 떠오른 순간 → "Inkling!" 자동 연상 만들기 (🌱 raw) + +**발견:** 2026-04-26 dogfood 시작 직전 사고 실험. 슬라이스 v0.4 와 strategy.md §3 가 **이미 알고 있는 contextual cue** (회의 후, 퇴근 전, 디버깅 후) 의 if-then 만 다루고, **ambient/spontaneous 떠오름** (샤워, 산책, 대화 중, 자기 전) 은 사각지대. + +### 관찰 + +캡처 자체의 마찰은 거의 0 — `Ctrl+Shift+J` + 한 줄 + Enter = 3초. 하지만 dogfood 의 진짜 실패 모드는 **"3초가 너무 길다"** 가 아니라 **"그 3초가 머릿속에 안 떠오른다"** 다. 떠오름 → Inkling 연상 사이의 인지적 다리가 약하면, 메모 자체가 시도되지 않는다. + +strategy.md 가 다루는 cue 는: + +| 위치 | 내용 | 강한 cue 여부 | +|------|------|--------------| +| §3 회의형 | "회의가 끝나면" | ✅ 외부 신호 (Zoom 종료, 자리 일어남) | +| §3 퇴근 회고형 | "업무 종료 10분 전" | ✅ 시계·전등·동료 | +| §3 디버깅형 | "오류를 해결하면" | ✅ 테스트 PASS 화면 | +| §3 학습형 | "새로 배운 것이 있으면" | ⚠ 내부 인식, 약함 | +| §3 요청 관리형 | "누군가 업무를 요청하면" | ✅ 메시지/대화 | + +남은 사각지대는 **외부 신호 없이 머릿속에서만 떠오르는 생각** — 샤워, 운전, 산책, 자기 직전, 책 읽다가, 잡담 중. 본인 dogfood 에서 가장 풍부한 메모 후보가 이 영역에서 발생할 가능성이 큰데 (RTX 4070 owner profile, 퇴근 후 사이드 프로젝트형 사고 패턴), 슬라이스가 cue 를 안 만들어주면 그냥 잊혀짐. + +### 제안 방향 + +**6개 메커니즘 후보** — 각각 Inkling 의 좁은 실행 가능 단위로 변환. + +#### A. 습관 쌓기 (Habit Stacking · BJ Fogg, James Clear) + +기존 강한 자동 행동 직후에 새 행동을 붙임. "X 하면 Y 한다" — Y 가 X 의 부산물처럼 느껴지게. + +| 강한 기존 자동 행동 | 붙일 Inkling 행동 | +|--------------------|-------------------| +| 노트북 덮음 / 잠금 (Win+L) | 30초 짧은 캡처 윈도우 자동 호출 | +| 휴대폰을 책상에 엎어놓음 | (휴대폰 앱이 생기면) 트리거 | +| 커피·물 가지러 일어남 | 타임랩스 카운트로 "자리 떴음 → 돌아왔을 때 한 줄" 프롬프트 | +| 메신저 알림 dismiss | 트레이 미니 입력 — Quick Capture 단축 | + +슬라이스 가능: **잠금 화면 진입 직후 3초 캡처 윈도우**. Windows API `SystemEvents.SessionSwitch` (lock/unlock) 로 unlock 시 한 번만 가벼운 토스트 ("머릿속에 남은 한 줄?"). 토스트 클릭 = QC. 무시 = 사라짐. + +#### B. 실행 의도의 확장 (Gollwitzer, ambient 영역) + +§3 의 if-then 을 "내적 인식" 트리거로 확장. + +``` +"무언가 해야 한다는 생각이 들면 → 그 자리에서 Ctrl+Shift+J" +"어제 그거 어떻게 됐지? 라는 생각이 들면 → 그 자리에서 Ctrl+Shift+J" +"이거 까먹을 것 같다는 느낌이 들면 → 그 자리에서 Ctrl+Shift+J" +``` + +내적 cue 의 약점은 인식 자체가 안 일어난다는 점이지만, 1~2주 의식적 반복 후 자동화 가능 (Lally 18~254일 범위, 본 사례는 행동이 단순해서 짧은 쪽). 온보딩에서 사용자에게 **본인의 ambient 떠오름 패턴** 을 1개 직접 적게 하고 그걸 if-then 으로 변환. + +#### C. 환경 앵커 (Cue Salience · 시각·물리적 단서) + +생각이 떠오를 때 시야에 Inkling 이 있으면 연상 강화. + +| 앵커 | 비용 | 슬라이스 적합도 | +|------|------|---------------| +| 잠금화면 배경에 Ctrl+Shift+J 안내 | 0 (사용자 셀프) | 외부 | +| 책상 모니터에 작은 펜 그림 스티커 | 0 (사용자 셀프) | 외부 | +| 작업 표시줄 트레이 아이콘 (이미 있음) | 0 | 슬라이스 ✅ | +| 트레이 아이콘 색을 "오늘 미캡처 시간" 에 따라 변화 | 작음 | 슬라이스 후속 | +| 윈도우 위젯 (Windows 11 위젯 보드) | 큼 | 슬라이스 외 | + +가장 가벼운 즉시 적용: **트레이 아이콘에 색·뱃지** — 오늘 캡처 0건이면 점선 동그라미, 1건 이상이면 실선. 사용자가 무의식 중에 시야 끝에서 "비어있다" 신호를 받음. + +#### D. 무작위 부드러운 알림 (Spaced retrieval · Variable interval) + +랜덤 간격으로 "지금 머릿속에 떠다니는 한 줄?" 토스트. variable interval reinforcement 가 fixed interval 보다 행동 강화에 강하다는 행동주의 결과 응용. 단 **거슬리면 즉시 망함** — 회복 친화 톤 강제. + +| 변수 | 권장 1차 | +|------|---------| +| 빈도 | 평일 09~18 KST 사이 2~3회, 분포는 90~180분 사이 무작위 | +| 작업 컨텍스트 가드 | full-screen 앱 감지 시 skip (회의·발표 중 보호) | +| 카피 톤 | "지금 한 줄 던져두면 좋을 게 있나요?" (강요 없음) | +| Off-toggle | 트레이 메뉴 1클릭 | +| 안티패턴 | 캡처 0/일 자체에 대한 죄책감 유발 카피 — 슬라이스 §1.1 위반 | + +슬라이스 적합도: **중**. Hotkey + Tray 만으로도 충분하다는 §3 결정과 충돌 가능. dogfood 1주 후 본인 캡처 빈도 데이터 본 뒤 결정. + +#### E. Zeigarnik 효과 활용 (미완 텐션 → 외재화) + +미완 과제는 인지 자원을 점유하며 회상이 더 잘 된다는 Zeigarnik 1927. Inkling 의 "이제 잊어도 됩니다" 보상이 이미 이 방향이지만, **떠올림 → 캡처** 다리에서는 활용 안 됨. 강화 방향: + +- 메인 토스트 카피 1개를 **사용 전 priming** 으로 재배치: 앱 첫 설치 직후 / 매일 첫 캡처 직후 짧게 "머릿속에 떠다니면 잡아두세요" 주입. 반복 노출이 ambient 떠오름 시 자동 reactivation. +- inbox 빈 상태 카피를 **유발 어휘** 로 변경: "오늘 머릿속에서 그냥 흘러간 생각 1개만 적어보세요" — 사용자에게 "흘러갔던 게 분명 있었지" 회상을 자극. + +비용 0, 슬라이스 내 적용 가능. F3 (구출 카피 재검토) 와 자연스럽게 묶임. + +#### F. 정체성 고리 (Identity-based habit · James Clear) + +"메모 잘하는 사람이 되자" 가 아니라 "나는 머릿속을 비워두는 사람" 이라는 자기개념. 매 캡처마다 정체성 강화 카피 1개: + +``` +"오늘 7번째 비웠습니다." (count visible) +"한 주 누적 23개. 잊을 자유 23번." +"이번 주 다시 본 메모 4건 — 외부 기억이 일하고 있음." +``` + +이미 §4.2 능력감 보상에 일부 있음. 차이는 "기억력" 이 아니라 "비움/외재화" 정체성으로 frame. 트레이 메뉴 또는 inbox 헤더에 작은 카운터. + +### 결정 대기 + +1. **슬라이스 외부 vs 후속 미니 spec**: A·D 는 새 IPC + Windows API + 알림 스케줄링이 필요해 슬라이스 §3 ("Hotkey + Tray + Notification 만") 와 충돌. C·E·F 는 슬라이스 내 적용 가능. 어디까지 슬라이스에서 시도하고 어디서부터 별 spec 으로 분기? +2. **무작위 알림 (D) 의 거슬림 임계점**: 본인 dogfood 1주 누적 데이터 (캡처 빈도 중간값) 를 보기 전에 결정 미루기. 데이터 없이 도입하면 anti-pattern 될 위험. +3. **메커니즘 결합 vs 격리 시험**: 6개 동시 적용 시 어느 게 효과 있었는지 분리 불가. dogfood 단계에서 격리 A/B 가능한가? 본인 단일 사용자라 통계 의미 약함 — 정성 라벨링으로 대체. +4. **strategy.md 와의 관계**: 본 항목이 strategy.md §3 ambient cue 절을 신규 §3.6 으로 추가할 만큼 무거운가, 아니면 specs/ 만으로 충분한가? F3 와 동일한 "strategy 동반 갱신 여부" 결정 패턴. +5. **외부 의존 (Win API) 도입의 슬라이스 의의**: A 의 SessionSwitch hook 은 슬라이스에서 한 번도 열어본 적 없는 native API. 도입 시 §7 dependency invariant 에 추가, 그 비용 vs cue 효과 가치 비교 필요. + +### 가설·측정 + +| # | 가설 | 측정 | +|---|------|------| +| H1 | E (Zeigarnik 카피 재배치) 만 적용해도 본인 dogfood 1주 캡처 빈도 ≥ 30% 증가 | 캡처/일 카운트, 적용 전후 1주씩 | +| H2 | C (트레이 뱃지 0/N 표시) 가 시각적 cue 로 작동하여 12~18시 사이 "지금 비어있네" 자각 사건 ≥ 2회/주 | 본인 라벨링 | +| H3 | D (무작위 알림) 가 캡처 빈도를 늘리지만 거슬림 점수 (1~5) 가 평균 ≤ 2 | 정성 점수 | +| H4 | F (정체성 카피) 가 dogfood 종료 시 "Inkling 을 계속 쓰고 싶다" 점수에 양의 영향 | exit 인터뷰 | +| H5 | B (ambient if-then 온보딩) 의 사용자 작성 if-then 1~2개가 1주 후에도 회상 가능 | 본인 self-report | + +### 범위 + +- **In (슬라이스 가능 — 가벼운 적용):** + - C 의 일부 — 트레이 아이콘 색·뱃지 (오늘 캡처 0/≥1) + - E — Zeigarnik priming 카피 1줄 + 빈 상태 카피 재작성 (F3 와 묶기) + - F — 트레이/Inbox 헤더의 정체성 카피 1줄 (count 표시) +- **In (슬라이스 후속 미니 spec):** + - A — 잠금/잠금해제 시 부드러운 캡처 프롬프트 + - B — 온보딩에 ambient if-then 1~2개 작성 단계 추가 + - D — 무작위 알림 + 가드 (full-screen, off-toggle) +- **Out:** + - 음성 ("Hey Inkling") + - 휴대폰 앱 + - 위젯 보드 통합 + - 외부 캘린더 cue 연동 + +### 영향 + +- **Schema:** 없음 (모두 클라이언트 사이드 또는 카피) +- **코드:** + - **C 트레이 뱃지:** `tray.ts` — 이미지 동적 갱신, `repo.countToday(): number` 또는 `getInbox` 필터로 오늘 카운트 조회. 10분 간격 또는 새 노트 생성 IPC 신호로 갱신. + - **E priming 카피:** `NotificationService.REWARD_COPIES`, `inbox/App.tsx` 빈 상태, `quickcapture/App.tsx` 힌트 + - **F 정체성 카피:** `inbox/App.tsx` 헤더 영역 (count + 한 줄), 또는 트레이 tooltip + - **A 후속:** `electron.powerMonitor` 의 `lock-screen`/`unlock-screen` 이벤트, 새 `LockHookService` + - **D 후속:** 새 `RandomPromptService` — setInterval + jitter, full-screen 감지는 `screen.getDisplayNearestPoint` 또는 native 호출 +- **문서:** + - 본 항목 promoted 시 `2026-04-26-cue-strengthening.md` (가칭) 으로 추출 + - strategy.md 결정 #4 결과에 따라 §3.6 신설 또는 본 spec 만으로 종결 +- **테스트:** + - C 의 단위 테스트 — 카운트 0/N 분기, 날짜 경계 KST + - E·F 카피 변경에 따른 e2e smoke 단언 동기화 (F3 와 묶음) + +### 비고 + +본 항목은 슬라이스 §1.3 의 종료 조건 ("본인 2주 dogfood 완주") 을 **달성하기 위한 메타-행동 설계**다. 즉 slice 의 "기능 엣지" 가 아니라 "사용자가 slice 를 안 잊고 쓰게 하는 메타 레이어". 우선순위는 H1 (E 만 1주 시도) → 데이터 확인 → C·F 추가 → A·D 검토 순서 권장. 6개 메커니즘 동시 도입은 신호 분리 불가로 안티패턴. + +--- + +## F5. 마크다운 일괄 export (RAG 활용 가정) (🌱 raw) + +**발견:** 2026-04-26 dogfood 시작 직전 사고 실험. 슬라이스 v0.4 는 노트가 SQLite + 로컬 미디어 폴더에만 존재. 외부 도구 (Obsidian, RAG 파이프라인, 로컬 LLM 컨텍스트, 검색 엔진) 로 빼낼 통로가 0. + +### 관찰 + +스키마에 이미 export 에 필요한 모든 정보가 있다 (`m001_initial.ts:7~57`): +- `notes` — id, raw_text, ai_{title,summary}, *_edited_by_user, user_intent, intent_prompted_at, created_at, updated_at, ai_provider, ai_generated_at +- `note_tags` (+`tags`) — 태그 + source(ai/user) +- `media` — note 첨부 이미지 메타 (`rel_path` 가 MediaStore 의 프로필 디렉터리 기준) +- (F1 promoted 시) `due_date`, `due_date_edited_by_user` + +내보낼 자산은 두 종류: +1. **노트 본문 + 메타데이터** — 텍스트 +2. **첨부 이미지** — 바이너리 파일 (현재는 `/media/`) + +RAG 파이프라인 (LangChain, LlamaIndex, ChromaDB, 로컬 embedding) 의 표준 입력은 **YAML frontmatter 가 붙은 단일-노트-단일-파일 마크다운** + 안정 ID + 카테고리/태그 메타. 본 export 는 이 형식에 정렬해야 후속 의사결정 (RAG 도입, Confluence 동기화, Obsidian vault 사용) 모두 한 형식으로 흡수 가능. + +### 제안 방향 + +**1차 권장 — 디렉터리 트리 + frontmatter 마크다운 + index.jsonl + 미디어 동봉.** + +``` +inkling-export-2026-04-26/ + notes/ + 2026-04-25-014a3b9c-주간회고-PR-리뷰.md + 2026-04-25-02f17de8-새-디버깅-패턴.md + ... + media/ + 014a3b9c__1.png # MediaStore rel_path 평탄화 + 02f17de8__1.jpg + index.jsonl # RAG 친화 1줄=1노트 메타 + manifest.json # 스키마 버전, 내보낸 시각, 노트 수, 검증 해시 + README.md # 형식 설명, RAG 적용 가이드 +``` + +**노트 파일 포맷** (one file per note, RAG 친화): + +```markdown +--- +id: 014a3b9c-... +created_at: 2026-04-25T14:23:11+09:00 +updated_at: 2026-04-25T14:24:02+09:00 +title: 주간 회고 PR 리뷰 +title_source: ai # ai | user (edited 면 user) +summary: 회고 양식 통일을 위한 PR 리뷰 메모. +summary_source: ai +tags: + - { name: pr, source: ai } + - { name: review, source: user } +user_intent: 팀에서 회고 양식 통일 +intent_prompted_at: 2026-04-25T14:24:02+09:00 +due_date: 2026-05-01 # F1 promoted 시 +due_date_source: ai +ai_provider: local-ollama/gemma4:e4b +ai_generated_at: 2026-04-25T14:23:34+09:00 +images: + - rel: media/014a3b9c__1.png + mime: image/png + bytes: 152834 +inkling_export_version: 1 +--- + +# 주간 회고 PR 리뷰 + +> 회고 양식 통일을 위한 PR 리뷰 메모. + +내일 까지 PR 리뷰 마무리하고, 회고 양식은 팀에 공유. +오후 미팅 중에 떠올랐음. + +![](media/014a3b9c__1.png) +``` + +**index.jsonl** (RAG 인덱싱용 1줄=1노트): + +```json +{"id":"014a3b9c-...","path":"notes/2026-04-25-014a3b9c-주간회고-PR-리뷰.md","created_at":"2026-04-25T14:23:11+09:00","tags":["pr","review"],"due_date":"2026-05-01","embedding_text":"주간 회고 PR 리뷰\n\n내일 까지 PR 리뷰 마무리하고..."} +``` + +`embedding_text` 는 title + raw_text + tags 를 결합한 임베딩 입력 후보. 사용자가 별도 가공 없이 LangChain `JSONLoader` 또는 LlamaIndex `JSONReader` 로 바로 적재 가능. + +**파일명 컨벤션:** `YYYY-MM-DD-{id8}-{slugified-title}.md`. 충돌 회피 + 인간 가독 + 디렉터리 정렬 친화. +- `id8` = UUIDv7 의 처음 8자리. 시간 정렬 + 충돌 0. +- `slugified-title` = title 의 한글 보존, 공백→하이픈, 파일시스템 금지 문자 제거 (`/\\:*?"<>|`), 32자 제한. +- title 비어있으면 `untitled` 폴백. + +**트리거 (1차 권장):** 트레이 메뉴 → "마크다운으로 내보내기..." → Electron `dialog.showOpenDialog({ properties: ['openDirectory'] })` 로 사용자가 폴더 선택 → 진행 토스트 → 완료 토스트 (성공 시 노트 수 + "폴더 열기" 버튼). + +**증분 vs 전체:** 1차는 전체 덮어쓰기만. 증분(변경된 노트만 갱신)은 후속. + +### 결정 대기 + +1. **포맷 1차안 확정**: one-file-per-note + frontmatter + index.jsonl 트리플 vs 단일 monolithic .md vs 두 형식 동시 출력? → RAG 우선이면 트리플이 압도적이지만 사용자 선호 확인 필요. +2. **미디어 포함 기본값**: 항상 동봉 vs 사용자 선택 (체크박스). 슬라이스 §1.1 의 "raw_text 본문에 민감정보 가능" 정책 — 이미지가 스크린샷인 경우 export 가 의도치 않은 노출 통로가 될 수 있음. **기본 동봉 + export 시 다이얼로그에 "이미지 N개 포함됩니다" 명시** 가 안전. +3. **삭제된 노트 처리**: SQLite 에 soft-delete 컬럼이 없음. 현재는 hard delete. export 결과는 *현 시점* 스냅샷만 — 삭제 이력 없음. 충분한가, 별 issue 인가? +4. **필드 정책 — provenance 표현**: `title_source: ai|user` 같은 단일 enum vs `title: { value, source, edited_at }` 객체. RAG 파이프라인의 frontmatter parser 마다 다름 — 평탄한 enum 이 호환성 좋음. +5. **embedding_text 합성 규칙**: title + raw_text + tags 단순 결합 vs raw_text 만 (가장 untouched) vs title + summary + raw_text. 본인 RAG 사용 패턴 미정 — 1차는 raw_text 단독으로 시작 + 옵션화. +6. **파일명에서 raw_text vs ai_title 사용**: ai_title 사용이 가독성 좋지만 AI 변경 시 파일명도 변하는 안티패턴. **ai_title 사용 + 사용자가 수동 export 트리거 시점 기준** 으로 동결 (재 export 시 새 파일명). 파일명 안정성 vs 가독성 트레이드오프 명시. +7. **트리거 표면**: 트레이 메뉴만 vs Inbox 헤더 버튼도 추가 vs CLI 플래그 (`--export `). 자동화 사용자라면 CLI 가 매력. 슬라이스 후속 미니 spec 으로 분리 가능. +8. **export 형식 버전 정책**: `inkling_export_version: 1` 박아두고 후속 변경 시 마이그레이션 가이드 동봉. 처음부터 박는 게 깔끔. +9. **민감정보 표시 경고**: 본 사용자는 `dlsrks0734@gmail.com` 계정 본인 단일 사용자라 위험 낮지만, export 후 폴더가 어디 가는지에 따라 위험 발생. 트레이 export 다이얼로그에 "이 export 는 평문이며 raw_text 가 그대로 포함됩니다" 명시 필요. + +### 가설·측정 + +| # | 가설 | 측정 | +|---|------|------| +| H1 | dogfood 1주 후 본인이 export 한 마크다운 더미를 LlamaIndex 기본 markdown loader 로 직접 적재 가능 (사이즈 변환 0) | 실측 — 1회 시도 | +| H2 | export 결과의 frontmatter 가 Obsidian 의 frontmatter renderer 에도 호환 | 실측 — Obsidian 에 폴더 import 후 메타 표시 확인 | +| H3 | 노트당 평균 raw_text 길이 ≤ 200 토큰 → RAG chunking 불필요 | 표본 50건 토크나이저 통계 | +| H4 | export 누적 사이즈가 1MB / 100 노트 이하 (미디어 제외) | 측정 | +| H5 | 본인이 export → 외부 도구 적재 → 적어도 1번 의미 있는 회수 (검색·RAG·재방문) 발생, dogfood 2주 내 | 본인 라벨링 | + +### 범위 + +- **In:** + - `ExportService` 신규 — DB 쿼리 + 파일 쓰기 + 미디어 복사 + - 트레이 메뉴 항목 1개 추가 ("마크다운으로 내보내기...") + - Electron `dialog` 디렉터리 선택 + - frontmatter 합성 + 파일명 슬러그 + - `index.jsonl` + `manifest.json` + `README.md` 동시 생성 + - 미디어 평탄화 복사 (rel_path → `media/{id8}__{n}.{ext}`) + - 진행 상태 토스트 (노트 수 ≥ 100 시 진행률) + - 단위 테스트 — frontmatter 합성, 슬러그, JSON 직렬화 +- **Out (후속 미니 spec):** + - 증분 export + - 자동 export (cron / watch) + - CLI 플래그 + - import (역방향) + - 다중 형식 (CSV, JSON 단일 파일, OPML) + - 외부 SaaS 동기화 (Confluence, Notion) + - export 시 raw_text 마스킹·익명화 + +### 영향 + +- **Schema:** 없음 — 현 스키마로 충분 +- **신규 파일:** + - `src/main/services/ExportService.ts` + - `src/main/ipc/exportApi.ts` + - 테스트 `tests/unit/ExportService.spec.ts` +- **변경 파일:** + - `src/main/index.ts` — 등록 + - `src/main/tray.ts` — 메뉴 항목 추가 + - `src/preload/index.ts` — IPC expose +- **외부 의존:** + - 없음 — Node `fs/promises` + `path` + `node:crypto` (해시) 만 사용 + - YAML 직렬화는 frontmatter 가 단순 하므로 자체 구현 (외부 dep 추가 불필요) +- **로깅:** + - export 시작·완료·노트 수만 기록. raw_text·title·summary 미기록 (slice §1.1 invariant 4 그대로) +- **문서:** + - 본 항목 promoted 시 `2026-04-26-markdown-export.md` (가칭) 으로 추출 + - 추출 후 README 의 doc map 갱신 + - export 폴더 안의 `README.md` — RAG 적재 예시 코드 포함 + +### 비고 + +본 항목은 **읽기 전용** 이라 dogfood 안전성 영향 0 (raw_text 변경 없음, AI 호출 없음, 네트워크 0). 우선순위 측면에선 F1·F2·F3 보다 후순위지만 **F4 의 H5 (외부 도구로 회수) 평가 자체가 export 없이는 측정 불가** — 즉 F4-H5 = F5 dependency. F4 의 데이터 수집을 위해 F5 가 먼저 promoted 되는 경로도 있음. + +--- + +## F6. 메모 데이터 백업 + 복원 (3-layer 권장) (🔬 drafting — L1 promoted) + +**진행 상태:** +- L1 (로컬 스냅샷) — 🚀 promoted → `docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md` +- L2 (git sync) — 🌱 raw, 7번 항목으로 예정 +- L3 (import) — 🌱 raw, 3번 항목으로 예정 (F5 후) + +**발견:** 2026-04-26 dogfood 시작 직전 사고 실험. 슬라이스 v0.4 의 메모 데이터는 `%APPDATA%\Inkling\Inkling\profiles\default\` 단 한 위치에만 존재. 디스크 고장·실수 삭제·DB 손상·OS 재설치 = 총 손실. Strategy.md §1 의 "이제 잊어도 됩니다" 보상이 **데이터 영속성 신뢰** 위에 서 있어서, 이 신뢰가 깨지면 슬라이스 §1.3 의 종료 조건 ("본인 2주 dogfood 완주") 자체가 위협받음. + +### 관찰 + +현재 단일 실패 지점 (SPOF): +- `inkling.sqlite` (WAL 두 파일 포함) — 노트·태그·AI 메타·intent 전부 +- `media/` — 클립보드 이미지 바이너리 (DB 의 `rel_path` 와 짝) +- 부팅 시 `MediaGc` 가 DB 미참조 미디어를 정리 — DB 가 손상되면 미디어도 GC 사이클에서 사라질 수 있음 (위험 증폭) + +기존 부분 완화는 0: +- 자동 백업 0 +- 외부 동기화 0 +- import 경로 0 +- F5 (export) 가 promoted 되어도 단방향 + 수동 + +본인 dogfood 운영 환경 신호: +- 이미 `gitea.altair823.xyz` 자체 호스팅 중 — 사적 git remote 인프라가 있음 +- 프로젝트 메모리: Mac=업무 / Windows=개인+dogfood — 디바이스 전환 가능성 (단일 활성, 동시 X) +- RTX 4070 Windows = 메인 dogfood 머신, 디스크 1대 SSD 가정 → 디스크 고장 1회 = 전체 손실 + +### 제안 방향 + +**3-layer 다층 백업.** 각 layer 가 다른 위협 모델을 커버. + +| Layer | 위협 모델 | 비용 | 슬라이스 적합 | +|-------|----------|------|--------------| +| **L1 로컬 원자 스냅샷** | 실수 삭제, DB 손상, AI 마이그레이션 실패 | 작음 | ✅ 슬라이스 후속 가벼움 | +| **L2 git remote 마크다운 동기화** | 디스크 고장, 디바이스 이동, 버전 이력 필요 | 중 | 🔬 별도 미니 spec | +| **L3 전체 export/import** | OS 재설치, 디바이스 이주, 사용자 통제 백업 | 작음 (L1 + F5 위) | ✅ F5 위에 import 만 추가 | + +#### L1 — 로컬 원자 스냅샷 + +`better-sqlite3` 의 `db.backup(path)` API 사용. WAL 활성 상태에서도 안전한 원자적 복제 (파일 단순 cp 와 다름 — WAL 미반영분 누락 위험 없음). + +```ts +// 의사코드 +async function snapshot(): Promise { + const ts = format(new Date(), 'yyyy-MM-dd'); + const dest = join(profileDir, 'backups', `inkling-${ts}.sqlite`); + await db.backup(dest); + await rotate({ daily: 14, weekly: 4, monthly: 6 }); +} +``` + +**스케줄**: +- 앱 종료 직전 1회 (`before-quit`) +- 매일 첫 캡처 시 (`/backups/.last-snapshot` mtime 비교) +- 명시 트리거: 트레이 메뉴 "지금 백업" + +**저장 정책 — Grandfather-Father-Son**: +- 일일 14개 → 주간 4개 → 월간 6개. 누적 24개 안팎, 평균 사이즈 가정 시 < 50MB. +- `backups/` 는 미디어 미포함 (DB 만). 미디어는 L2 또는 L3 책임. + +**위협 미커버**: 디스크 자체 고장. SSD 가 죽으면 backups/ 도 같이 죽음. L2 가 이 위협 담당. + +#### L2 — git remote 동기화 (RECOMMENDED 핵심 layer) + +**핵심 결정: SQLite 바이너리를 push 하지 말고, F5 마크다운 트리를 push 한다.** + +| | SQLite 바이너리 | F5 마크다운 트리 | +|--|----------------|-----------------| +| diff 의미성 | 0 (전체 blob 변경) | ✅ 노트별 라인 diff | +| repo 사이즈 | 매 push 마다 풀 DB | 변경 노트만 | +| 멀티 디바이스 머지 | 불가 (binary conflict) | 가능 (텍스트 merge) | +| 외부 도구 호환 | 0 | RAG / Obsidian / grep 즉시 | +| F5 와 의 시너지 | 0 | F5 그대로 재사용 | + +→ **F5 의 export 형식을 git 추적 대상으로 그대로 사용**. F5 가 promoted 되면 F6-L2 는 그 위에 자동화 layer 만 얹는 구조. + +**아키텍처**: + +``` +[CaptureService / NoteRepository] + │ (write) + ▼ + inkling.sqlite ← Layer 0 (primary) + │ + │ (DB write 후 dirty 마크) + ▼ + /sync/ ← Git working tree (L2) + ├── notes/ ← F5 형식 마크다운 + ├── media/ + ├── index.jsonl + └── manifest.json + │ + ▼ (BackgroundSyncWorker, 5분 주기 또는 dirty=true 후 30초 debounce) + git add . && git commit -m "..." && git push +``` + +**커밋 메시지 컨벤션** (자동 생성): + +``` +chore(notes): +3 ~1 -0 (2026-04-26T14:23+09:00) + +added: 01H89aab... 주간 회고 PR 리뷰 +added: 01H89bcd... ... +modified: 01H78xyz... 어제 회의 메모 +``` + +기존 inkling 본 저장소 commit 스타일과 분리되며, "automated note sync" 임이 명확. + +**Auth & 보안**: +- Personal Access Token 또는 SSH key. Electron `safeStorage` API (OS keychain 백엔드 — Windows 는 DPAPI) 로 평문 미저장. +- 토큰은 절대 로그/오류 메시지에 노출 금지 (slice §1.1 invariant 4 확장). +- repo 는 **반드시 private** — 평문 raw_text 노출 위험. 처음 설정 시 다이얼로그에 굵은 경고. + +**Conflict 정책 — single-active-device 가정**: +- push 가 거부되면 (다른 디바이스가 먼저 push) → `git pull --rebase` → 자동 머지 시도 +- 머지 실패 (같은 노트 양쪽 수정) → 트레이 알림 + 수동 해결 다이얼로그. 노트별 "내 버전 / 원격 버전 / 둘 다 보존" 3-way 선택 +- 본인 dogfood = 단일 활성 디바이스라 거의 발생 안 함 — 멀티 디바이스 시나리오 정식 지원은 L2 의 v2 + +**Repo 초기화**: +- 첫 설정 시 사용자가 빈 remote URL 입력 → 앱이 `git init` + 초기 export + 첫 커밋 + push +- 또는 기존 repo URL 입력 → clone → 검증 (이전 manifest 호환성) → 동기화 시작 + +**미디어 정책**: +- 평문 push 가 default — 텍스트 노트와 함께 미디어도 git 에 올라감 +- repo 사이즈 폭발 위험 → 토큰 옵션: "이미지 제외" 토글 또는 Git LFS (선택). 1차는 옵션 X, 단순 push, 사이즈 모니터링만. +- 이미지 제외 시 frontmatter 의 `images` 항목은 보존하되 파일은 미포함 → 복원 시 placeholder 표시 + +#### L3 — 수동 전체 export / import + +- **export**: F5 가 그대로 담당. 변경 없음. +- **import**: 신규. F5 형식 폴더를 읽고 DB 에 upsert. 충돌 정책: + - id 충돌 + 본문 동일 → skip + - id 충돌 + 본문 상이 → 사용자 선택 (덮어쓰기 / skip / 양쪽 보존하며 새 id 생성) + - id 신규 → insert + - 미디어 → MediaStore 에 복사 +- 트레이 메뉴 "백업에서 복원..." → 폴더 선택 → 미리보기 (n개 신규, m개 변경, k개 충돌) → 확인 → 적용 + +### 결정 대기 + +1. **3-layer 동시 도입 vs 단계적**: L1 → L3 → L2 순서가 비용·위험 단조 증가라 권장. L1 만으로도 SPOF 완화의 80% 커버. +2. **L2 sync 단위**: 매 변경 vs 5분 debounce vs 종료 시 1회 vs 명시 동기화만. 실시간일수록 데이터 손실 윈도우 작지만 git push 빈도 폭발 + 네트워크 마찰. **5분 debounce + 종료 시 즉시 push** 가 1차 권장. +3. **L2 repo 분리**: 기존 `gitea.altair823.xyz/altair823-org/inkling` (소스 코드) 와 분리된 별 repo (예: `altair823-org/inkling-data`) — **반드시 분리**. 데이터·코드 라이프사이클 다름, 외부 협업자에게 데이터 노출 위험. +4. **L2 충돌 시 정책 — slice §1.1 vs 사용자 선택**: 자동 "내 디바이스 우선" 가속 vs 매번 묻기. dogfood 단일 디바이스 가정으론 자동 OK, but defensive 차원에서 충돌 발생 시 1회 확인이 안전. +5. **media 의 git 추적**: 포함 vs 제외 vs LFS. 1차는 포함 + 사이즈 < 100MB 경고. 누적 시점에 후속 결정. +6. **L1 백업 위치**: `/backups/` (현 프로필 안) vs 별 디렉터리 (`%APPDATA%\Inkling\backups\`) vs 사용자 지정 외부 경로. 외부 경로 옵션이 OneDrive 등 클라우드 sync 폴더 이용 가능 — 거의 공짜 cloud backup. +7. **import 시 raw_text invariant 보호**: slice §1.1 "raw_text 불변" 은 *동일 id 내* 의미. import 가 같은 id 의 raw_text 를 다른 값으로 덮어쓰면 invariant 위반. 충돌 시 raw_text 다르면 **새 id 강제** 정책이 안전. +8. **L2 첫 설정의 UX 부담**: token 입력 + remote 검증 + 초기 push 가 dogfood 1일차 첫 인상에 마찰. 첫 설치 후 N 일 (예: 7일) 까지는 L1 만 켜두고 L2 는 트레이 메뉴 "원격 백업 설정" 으로 opt-in 권장. +9. **암호화 — local-first 라도 token 외 추가 보호 필요한가**: SQLite·미디어·git 모두 평문. 디스크 도난 시 노출. 1차는 평문 (slice §1.1 미적용 영역), 후속에 SQLCipher / age 암호화 검토. +10. **slice §7 strict-pin invariant 영향**: L1 은 `better-sqlite3.backup()` 만 사용 — 추가 dep 0. L2 는 `simple-git` 또는 `nodegit` 같은 git 바인딩 또는 child_process 로 git CLI 호출. CLI 호출이 dep 0 + 사용자 git 환경 재사용. **CLI 호출 권장**. + +### 가설·측정 + +| # | 가설 | 측정 | +|---|------|------| +| H1 | dogfood 2주 누적 동안 디스크 측 사건 (실수 삭제, DB 손상, 디스크 고장) ≥ 1회 발생할 정성 가능성 | 발생 시 라벨링 | +| H2 | L1 단독 만 도입해도 SPOF 발생 시 회복 가능 (백업으로 ≥ 95% 데이터 복원) | 복원 시뮬레이션 1회 (의도적 DB 삭제 후 복원) | +| H3 | L2 5분 debounce push 가 일평균 ≤ 30 commit. repo 사이즈 누적 < 100MB / 1년 | 로그 측정 | +| H4 | L2 commit 메시지 통계 (added·modified·deleted) 가 dogfood 활동 회고 자료로 가치 발생 | 정성 평가 | +| H5 | "이제 잊어도 됩니다" 보상의 신뢰도 — 백업이 있다는 인지가 capture 빈도 또는 심리적 부담 감소에 영향 | 본인 self-report | + +### 범위 + +- **In (L1 — 슬라이스 후속 가벼움):** + - `BackupService` 신규 — `db.backup()` 래핑 + 로테이션 + - 트레이 메뉴 "지금 백업" + 타임스탬프 표시 + - 종료·일일 1회 자동 트리거 + - `backups/` 디렉터리 — `.gitignore` 와 같은 .ignored 마커 고려 + - 단위 테스트 — 로테이션 GFS 정책 +- **In (L3 — F5 위에 import 만):** + - `ImportService` 신규 + - 충돌 미리보기 다이얼로그 + - 트레이 메뉴 "백업에서 복원..." +- **In (L2 — 별 spec, 가장 큼):** + - `SyncService` (BackgroundSyncWorker) + - F5 ExportService 의 incremental 모드 (변경 노트만) + - git CLI 래퍼 + safeStorage 토큰 관리 + - 설정 UI — remote URL, 토큰, 동기화 주기, 미디어 포함 여부, 충돌 정책 + - 충돌 해결 다이얼로그 + - 상태 표시 (트레이 아이콘 색·tooltip) +- **Out:** + - SQLCipher 암호화 + - 다중 활성 디바이스 실시간 sync + - 외부 SaaS (Dropbox API, Google Drive API) 직접 연동 + - Rsync 전송 + - SQLite WAL 의 logical replication + +### 영향 + +- **Schema:** 없음 +- **신규 파일 (L1 + L3):** + - `src/main/services/BackupService.ts` + - `src/main/services/ImportService.ts` + - `src/main/ipc/backupApi.ts` + - 테스트 `tests/unit/BackupService.spec.ts`, `ImportService.spec.ts` +- **신규 파일 (L2 별 spec):** + - `src/main/services/SyncService.ts` + - `src/main/services/GitClient.ts` (git CLI 래퍼) + - `src/main/services/CredentialStore.ts` (safeStorage 래퍼) + - 설정 UI (Settings 창 신설 — 슬라이스 §5 의 "Settings 창 없음" 결정 재검토 필요) +- **외부 의존:** + - L1: 0 + - L3: 0 + - L2: 사용자 머신의 git CLI 필요. README 사전 요구 항목 추가 +- **로깅:** + - 백업 시작·완료·사이즈만. 본문·파일명 미기록 + - 동기화 push 결과·conflict 발생만. 토큰·URL 일부 마스킹 +- **문서:** + - 본 항목 promoted 시 분리 권장: + - `2026-04-26-local-snapshot.md` (L1) + - `2026-04-26-import.md` (L3, F5 와 자매) + - `2026-04-26-git-sync.md` (L2) + - 또는 단일 `2026-04-26-backup-strategy.md` 로 통합 후 §A·§B·§C 로 분리 + +### 비고 + +본 항목과 F5 는 **완벽한 데이터 라이프사이클 그림** 의 두 절반: +- F5 = 외부 회수 (read 방향) +- F6 = 외부 백업 + 내부 복원 (write·sync 방향) + +L2 (git sync) 가 dogfood 본인의 기존 인프라 (gitea 자체 호스팅) 와 자연스럽게 맞물리는 점은 본 사용자에게 특히 강한 가치. 다른 사용자였다면 GitHub Actions 등 외부 서비스 의존이라 우선순위 낮을 수 있음. + +slice §1.3 종료 조건 ("크래시 0회") 와 별개로, **"데이터 손실 0회"** 가 silent invariant 로 추가되어야 함. 본 항목 → `slice spec §1.3` 추가 갱신 후보. + +--- + ## (다음 항목 자리) -새 피드백 추가 시 `## F3. 짧은 제목 (🌱 raw)` 헤더로 시작. 표준 슬롯 6개 채우거나 비워둔 채 시작 가능. +새 피드백 추가 시 `## F7. 짧은 제목 (🌱 raw)` 헤더로 시작. 표준 슬롯 6개 채우거나 비워둔 채 시작 가능. diff --git a/docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md b/docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md new file mode 100644 index 0000000..0df9e60 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-f6-l1-local-snapshot.md @@ -0,0 +1,40 @@ +# F6-L1 Local Snapshot Spec (Promoted) + +**Extracted from:** `2026-04-25-dogfood-feedback.md` F6 §"L1 — 로컬 원자 스냅샷" +**Plan:** `docs/superpowers/plans/2026-04-26-f6-l1-local-snapshot.md` +**Status:** 🚀 promoted — implemented 2026-04-26 + +## 결정 (mini-brainstorm 결과) + +| 결정 항목 | 값 | 근거 | +|----------|-----|------| +| 백업 위치 | `/backups/` | 프로필 단위 묶음, 코드 단순. 외부 디렉터리/사용자 지정 경로는 후속. | +| 파일명 | `inkling-YYYY-MM-DD.sqlite` (KST 날짜) | 인간 가독 + 정렬 친화 | +| 마커 | `/backups/.last-snapshot` (ISO 날짜 본문) | 같은 날 중복 백업 방지 | +| 트리거 | `app.whenReady` 시 1회 + `before-quit` 1회 + 트레이 "지금 백업" | 슬라이스 §3 외부 dep 없이 만족 | +| 보존 | 14 daily + 4 weekly (월요일, anchor + 4 prior = 최대 5 Mondays) + 6 monthly (1일) | 사용자 결정. plan 산문은 "4 weekly" 였으나 plan 테스트 케이스가 5 Mondays 를 요구 → 테스트 우선 채택. | +| 원자성 | tmp + rename + 실패 시 tmp 정리 | 파셜 파일 방지, 코드 review I1 반영 | + +## 범위 (PR 안에 포함됨) + +- `src/main/services/backupRotation.ts` (pure GFS function) +- `src/main/services/BackupService.ts` (snapshot + rotate + runDaily + lastSnapshotAt) +- `src/main/index.ts` 수정 (whenReady wiring + before-quit hook 통합 + createTray 콜백 추가) +- `src/main/tray.ts` 수정 (지금 백업 메뉴) +- `tests/unit/backupRotation.test.ts` (10 단위 테스트) +- `tests/unit/BackupService.test.ts` (14 단위 테스트) + +## 후속 (별 spec 또는 후속 항목 후보) + +- 외부 디렉터리 백업 (예: OneDrive 폴더 지정) +- 미디어 포함 백업 +- 암호화 (SQLCipher / age) +- 백업 무결성 검증 (PRAGMA integrity_check) +- F6-L2 git sync 와 연결 (백업 디렉터리도 sync 대상에 포함할지) +- KST 헬퍼 추출 (현재 ContinuityService + BackupService + recoveryToast 3개 인라인 — 다음 consumer 시 추출) +- WAL 모드 source DB 통합 검증 테스트 +- runDaily 마커의 read-then-write race 보호 (현재 단일 사용자라 사실상 무시 가능) +- 시작 시점 `*.sqlite.tmp` orphan sweep — Windows 강제 종료(`session-end`) 가 `db.backup()` 중간에 들어올 때 잔존 가능. `parseBackupFilename` 이 `.tmp` 를 거부하므로 rotation 에 영향 0 이지만 디스크 누수 차원 +- `rotate()` 부분 실패 허용 — `unlink` 한 파일이 antivirus 등으로 잠긴 경우 현재는 전체 abort. 파일별 try/catch + `partial_rotation` 로깅 권장 +- `before-quit` hook 이 `whenReady()` 안에 있어서 whenReady 도달 전 quit 시 미설치 — 보통은 OK (백업할 DB 자체가 없는 시점) 지만 코멘트 권장 +- **F1 마이그레이션 전 강제 스냅샷** — `openDb()` 가 `runMigrations()` 를 호출 후 `BackupService` 가 인스턴스화되므로 v2 마이그레이션 결함 시 첫 실행 직전 상태 회수 불가. F1 (Due Date) PR 시 마이그레이션 직전 스냅샷 단계 추가 권장 diff --git a/docs/superpowers/specs/2026-04-26-feedback-roadmap-design.md b/docs/superpowers/specs/2026-04-26-feedback-roadmap-design.md new file mode 100644 index 0000000..07fdcf0 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-feedback-roadmap-design.md @@ -0,0 +1,229 @@ +# Dogfood 피드백 로드맵 (F1~F6 → v0.2.1) 설계 + +**작성일:** 2026-04-26 +**저자:** 김태현 (dlsrks0734@gmail.com) +**문서 성격:** v0.2.0 dogfood 와 병렬로 진행되는 F1~F6 항목 8개의 순차 작업 로드맵. 본 문서는 **순서·범위·게이트** 만 정의하며, 각 항목 내부 설계는 항목별 mini-brainstorm + writing-plans 단계에서 결정. + +**선행 문서:** +- `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` (F1~F6 raw/drafting 수집) +- `docs/superpowers/specs/2026-04-24-inkling-vertical-slice-design.md` (slice v0.4 본문, §1·§5·§7) +- `docs/superpowers/strategy/strategy.md` (심리학 전략, F3·F4-E 동반 갱신 대상) +- `docs/superpowers/strategy/dogfood-strategy.md` (dogfood 운영안) + +--- + +## 1. 결정 요약 + +| 결정 | 값 | 근거 | +|------|-----|------| +| 우선순위 기준 | **데이터 안전 우선** | dogfood 진행 중 손실 위험 즉시 차단. F6 의 raw 발견이 시기적으로 가장 무거운 신호. | +| 항목당 게이트 | **머지 + 테스트 통과** (typecheck + 52+ 단위 + e2e smoke) | 빠른 회전. 새 빌드는 누적 후 한 번에 cut. | +| 시작 항목 | **F6-L1 로컬 스냅샷** | 안전 우선 + 작은 범위 + 독립 (외부 dep 0). | +| 순서 | **데이터 라이프사이클 → 기능 → 카피 → 무거운 sync → cue 강화** (Option X) | 백업·export·import 안전망 완성 후 schema 변경 (F1) 진입. e2e 흔드는 카피 변경은 늦게 배치. | +| 다음 빌드 | **v0.2.1** (8개 모두 머지 후 단일 cut) | slice §7 strict-pin 컨벤션의 patch 증분. | +| F4-A·D | **deferred** — v0.2.1 dogfood soak 후 측정 데이터 기반 별도 brainstorm | 측정 인프라가 cut 시점부터 활성. 사전 결정 위험 회피. | +| Decision-pending 처리 | **항목별 mini-brainstorm** | 본 문서는 순서만, 항목 내부는 per-item. | + +--- + +## 2. 순차 작업 순서 + +``` +v0.2.0 ────────[ dogfood 동결, 병렬 진행 ]──────── + │ +개발 트랙 (main 직접 머지): │ + ① F6-L1 로컬 스냅샷 [작음, 안전 즉시] │ + ② F5 Export [중, 의존성 잠금해제] │ + ③ F6-L3 Import [작음, F5 직후] │ + ④ F1 Due Date [큼, migration v2] │ + ⑤ F2 태그 클릭 [작음, 독립] │ + ⑥ F3+F4-E 카피·strategy 정리 [중, e2e 영향] │ + ⑦ F6-L2 Git sync [가장 큼] │ + ⑧ F4-C·F cue 강화 [작음] │ + │ + ┌──────────┘ + ▼ + v0.2.1 cut (단일) + │ + ▼ + dogfood 재설치 + ≥ 1주 soak + │ + ▼ + F4-A·D (측정 후 별 brainstorm) +``` + +### 2.1 순서 결정 근거 (요약) + +1. **F6-L1 (1번)** — 안전 우선. SQLite `db.backup()` 만 사용, 외부 dep 0, 다른 항목 무영향. +2. **F5 (2번)** — F4-H5 측정 + F6-L3 import + F6-L2 git sync 의 dependency. 잠금해제 효율 1위. +3. **F6-L3 (3번)** — F5 의 역방향. 형식 동일, ImportService 만 추가. 데이터 라이프사이클 3종 세트 완성. +4. **F1 (4번)** — schema migration v2 가 백업·export·import 인프라 위에서 진행되어 회수 가능. migration 결함 시 v0.2.0 백업으로 복원 가능. +5. **F2 (5번)** — 작은 renderer 변경. F1 schema 변경과 충돌 없음. +6. **F3+F4-E (6번)** — strategy.md §1·§3·§7 의 "기억 구출" 어휘 결정 + e2e smoke 단언 변경 + Zeigarnik priming 카피. 한 PR 으로 묶음. e2e 흔드는 위치를 늦게 배치하여 다른 항목 머지 흐름 보호. +7. **F6-L2 (7번)** — git CLI 의존성 + safeStorage + 동기화 worker. 가장 무거움. F5/F6-L1/F6-L3 인프라 안정 후. +8. **F4-C·F (8번)** — 트레이 뱃지 + 정체성 카피. F3 카피 톤 결정 후 톤 통일. 가벼움. + +--- + +## 3. 항목당 범위 (In/Out) + +각 항목의 PR 범위 라인. 세부 결정 (decision-pending) 은 항목 시작 시 mini-brainstorm 에서. + +| # | 항목 | In (이 PR 범위) | Out (다음 항목 또는 후속) | +|---|------|----------------|--------------------------| +| 1 | **F6-L1** 로컬 스냅샷 | `BackupService` + `db.backup()` 래핑 + GFS 로테이션 (일 14·주 4·월 6) + 트레이 메뉴 "지금 백업" + 종료 hook (`before-quit`) + `.last-snapshot` 마커 + 단위 테스트 (로테이션) | 외부 디렉터리 백업, 암호화, 미디어 포함, 자동 복원 | +| 2 | **F5** Export | `ExportService` + frontmatter 마크다운 (one-file-per-note) + `index.jsonl` + `manifest.json` + `README.md` 동봉 + 미디어 평탄화 복사 + 트레이 메뉴 "내보내기..." + 폴더 선택 다이얼로그 + 단위 테스트 (frontmatter 합성, 슬러그) | 증분 export, CLI 플래그, watch-mode, CSV/JSON 형식 | +| 3 | **F6-L3** Import | `ImportService` + 충돌 미리보기 다이얼로그 (n 신규, m 변경, k 충돌) + 트레이 메뉴 "백업에서 복원..." + 단위 테스트 (id 충돌 정책) | git remote 통한 import, 충돌 자동해결, 마이그레이션 자동 | +| 4 | **F1** Due Date | migration v2 (`due_date TEXT`, `due_date_edited_by_user INTEGER`) + 규칙 파서 (정규식 + KST 변환) + zod 스키마 확장 + AI 프롬프트 `{{TODAY_KST}}` + NoteCard 라벨 슬롯 + EditableField 재사용 + 골든 픽스처 50건 | 음력·시각 단위·반복 일정, 만료 처리 정책, 별도 due 뷰 | +| 5 | **F2** 태그 클릭 | NoteCard 칩 onClick 변경 (필터) + ✕ 아이콘 추가 + 5초 undo 토스트 + zustand `tagFilter` + Inbox 헤더 필터 칩 | 다중 태그 필터, rename/merge, 자동완성 | +| 6 | **F3+F4-E** 카피 정리 | NotificationService 회전 카피 #3 교체 + 트레이 라벨 2개 + 빈 상태 + QC 힌트 + `package.json description` + e2e smoke 단언 동기화 + slice §5.5 카피 테이블 + strategy.md §1·§3·§7 어휘 갱신 + Zeigarnik priming 카피 1줄 | 회전 카피 4종 전면 재작성, onboarding, 다국어 | +| 7 | **F6-L2** Git sync | `SyncService` (BackgroundSyncWorker) + `GitClient` (CLI 래퍼) + `CredentialStore` (safeStorage 래퍼) + Settings 창 신설 (remote URL · token · 주기 · 미디어 포함 · 충돌 정책) + 5분 debounce + 종료 시 push + 충돌 다이얼로그 (3-way 선택) + 트레이 상태 표시 | 자동 conflict resolution, 다중 활성 디바이스 sync, LFS, 암호화 | +| 8 | **F4-C·F** cue 강화 | 트레이 아이콘 동적 갱신 (오늘 캡처 0/N 색·뱃지) + `repo.countToday()` + Inbox 헤더 정체성 카운터 + 카피 1줄 ("오늘 N번 비웠습니다") | F4-A 잠금 hook, F4-D 랜덤 알림 (deferred) | + +### 3.1 공통 게이트 (모든 항목) + +각 항목 머지 전 필수: + +- `npm run typecheck` 통과 (현재 0 에러) +- `npm test` 통과 (현재 52/52, 항목 신규 단위 추가) +- `npm run test:e2e` 통과 (현재 1/1) +- 항목 신규 단위 테스트 ≥ 1개 (TDD) +- main 머지 + dogfood-feedback.md 상태 🚀 promoted + 별 spec 분기 + +--- + +## 4. 항목당 작업 흐름 + +``` +[항목 N 시작] + │ + ├─ mini-brainstorm ← 본 항목의 decision-pending 답변 + │ - F-spec 의 "결정 대기" 슬롯 채움 + │ - dogfood-feedback.md 의 상태 🌱 → 🔬 → 📝 전이 + │ + ├─ writing-plans ← TDD 구현 계획 + │ - test-driven-development 스킬 사용 + │ - 한 항목 = 한 plan + │ + ├─ 구현 (executing-plans 또는 직접) + │ - 브랜치: feat/F- (예: feat/F1-due-date) + │ - 게이트 통과 후 main 머지 + │ - 단일 PR 또는 main 직접 push (작은 항목 한정) + │ + ├─ dogfood-feedback.md 갱신 + │ - 상태 → 🚀 promoted + │ - 별 spec 분기 → docs/superpowers/specs/2026-MM-DD-.md + │ - 본 문서엔 1줄 요약 + 링크만 남김 + │ + └─ 다음 항목 시작 +``` + +### 4.1 Cross-cutting 정책 + +| 영역 | 정책 | +|------|------| +| **버전 관리** | 8개 모두 머지될 때까지 `package.json` `0.2.0` 유지. v0.2.1 cut 은 8번 후 단일. | +| **브랜치 전략** | `feat/F-` 단명. main 머지 후 삭제. 작은 항목 (F6-L1, F2, F4-C·F) 은 main 직접 push 도 허용 (sandbox 정책 따름). | +| **테스트 추가 정책** | 항목당 최소 단위 1개. e2e smoke 영향 시 단언 동기화. integration (Ollama) 은 AI 호출 영향 시만. | +| **Slice invariant 위반 시** | 본 로드맵 결과로 invariant 변경 — slice spec §7 도 PR 안에 동봉 수정. | +| **F4-A·D deferred** | v0.2.1 dogfood soak (≥ 1주) 후 측정 데이터 보고 별도 brainstorm 진입. 본 로드맵 범위 외. | +| **dogfood-feedback.md 라이프사이클** | 항목 promoted 시 본 문서엔 1줄 + 링크. raw/drafting 항목은 그대로 누적. | +| **신규 dependency** | slice §7 strict-pin 그대로. 신규 dep 도입 시 PR 안에 §7.2 갱신 동봉. F6-L2 의 git CLI 는 시스템 의존이라 README 사전 요구 추가. | +| **로깅 정책** | slice §1.1 invariant 4 (raw_text/title/summary/intent 미기록) 유지. F5/F6 export·sync 로그도 노트 본문 미기록, ID·길이·해시 prefix 만. | +| **Strategy.md 동반 갱신** | F3+F4-E 항목 (6번) 에서만. 다른 항목은 strategy.md 미수정. | + +--- + +## 5. v0.2.1 Cut 단계 + +8번 항목 머지 후: + +``` +[v0.2.0 dogfood 환경에서] +1. 트레이 → "지금 백업" 1회 클릭 ← F6-L1 첫 실증, 백업 1회 보장 +2. 트레이 → "내보내기..." 1회 ← F5 첫 실증, 외부 백업 이중화 +3. Inkling 종료 (트레이 → 종료) ← 설치 충돌 회피 + +[빌드 머신에서] +4. package.json version: 0.2.0 → 0.2.1 +5. CHANGELOG.md (신설) 또는 git tag 메시지에 누적 + * 결정 대기: CHANGELOG.md 신설 vs git tag 메시지 ─ 8번 항목 직전 mini-brainstorm 에서 결정 +6. npm run dist +7. dist/Inkling Setup 0.2.1.exe 검증 + +[dogfood 머신에서] +8. Setup 0.2.1.exe 실행 → 같은 폴더에 설치 (위치 변경 시 side-by-side 위험) +9. 첫 실행 → migration v2 자동 적용 (F1 due_date 컬럼 추가) +10. 트레이 → "백업에서 복원..." 메뉴 존재 확인 (F6-L3 회로 통) +11. ≥ 1주 soak 시작 +``` + +### 5.1 업그레이드 안전망 + +| 위험 | 완화 | +|------|------| +| migration v2 결함으로 DB 손상 | **2가지 복원 경로 보유**: (a) 단계 1 의 F6-L1 백업은 v0.2.0 schema 라 복원하려면 v0.2.0 인스톨러 재설치 후 백업 파일 교체 → 다시 v0.2.1 업그레이드 (migration 재시도 또는 fix 적용된 v0.2.2 대기). (b) 단계 2 의 F5 export 는 schema-agnostic 마크다운이라 v0.2.1 의 F6-L3 import 로 직접 복원 가능. (b) 가 더 빠른 회복 경로. | +| 설치 위치 변경 → side-by-side 잔존 | 설치 마법사에서 같은 폴더 선택 (기본값) | +| 앱 실행 중 설치 실패 | 단계 3 에서 종료 | +| 자동시작 토글 상태 손실 | `HKCU\...\Run` + `.autostart-init` 보존됨 (data dir 손대지 않음) | +| electron-updater 미설정 | 본 로드맵 범위 외. 사용자가 수동 다운로드 (gitea release 호스팅 후속 검토) | + +--- + +## 6. 측정 + +### 6.1 로드맵 측정 + +| 메트릭 | 임계값 | 측정 방법 | +|--------|--------|----------| +| 항목 평균 PR 사이즈 | < 800 lines diff | git log 통계 | +| 항목 평균 머지 간격 | < 5일 | git log 시간차 | +| 회귀 테스트 추가 | 항목당 ≥ 1개 단위 테스트 | tests/unit 카운트 | +| dogfood-feedback.md 상태 전이 | 8/8 모두 🚀 promoted | grep | +| v0.2.1 cut 후 1주차 데이터 손실 | 0회 | 본인 라벨링 | +| typecheck/test 회귀 | 0회 (모든 항목 통과) | CI · 로컬 | + +### 6.2 silent invariant 후보 + +본 로드맵 결과로 slice §1.3 종료 조건에 다음을 추가 권장: + +> **"데이터 손실 0회"** — F6-L1 출시 후 모든 dogfood 사이클에 대해 데이터 손실 사건 0회. 발생 시 즉시 silent invariant 위반으로 간주. + +이 추가는 F6-L1 항목 머지 시 slice spec §1.3 동봉 수정. + +--- + +## 7. 본 로드맵의 종료 조건 + +**모두 만족해야 종결**: + +1. F1·F2·F3+F4-E·F5·F6-L1·F6-L3·F6-L2·F4-C·F 8개 항목 모두 main 머지 +2. 8개 모두 dogfood-feedback.md 에서 🚀 promoted 상태 + 별 spec 파일 분기 +3. `package.json description`, README, slice spec §5.5 카피 테이블, strategy.md §1·§3·§7 동봉 갱신 완료 +4. v0.2.1 cut → dogfood 머신 재설치 → migration v2 적용 확인 → 첫 실행 정상 + 트레이 메뉴 6개 항목 (지금 백업·내보내기·복원·자동시작·구출·종료, 카피 변경 반영) 동작 확인 +5. ≥ 1주 dogfood soak 완료 (데이터 손실 0회 확인) + +5 가 끝나면 본 로드맵 종결, F4-A·D 별 brainstorm 진입. + +--- + +## 8. 미결정 항목 (각 항목 시작 시 답변) + +본 로드맵은 순서만 정의했고, 각 F-spec 의 결정 대기 슬롯은 항목 시작 시 mini-brainstorm 에서 답함. 본 문서는 그 결정들을 미리 잠그지 않음. + +특히 **다음 결정들은 빨리 마주치게 됨**: + +- F5: 포맷 1차안 확정 (one-file-per-note + frontmatter + index.jsonl 트리플 가정), 미디어 포함 기본값, embedding_text 합성 규칙 +- F6-L1: 백업 위치 (profileDir 안 vs 별 디렉터리 vs 사용자 지정 외부) +- F1: false positive 처리, due 만료 시 시각 표시 정책, 라벨 슬롯 위치 +- F3+F4-E: strategy.md §1·§3·§7 동반 갱신 범위, 단일 동사 통일 vs 표면별 분배 +- F6-L2: 첫 설정 UX 부담 vs opt-in, repo 분리 (`-data` 별 repo) +- 모든 항목: CHANGELOG.md 신설 vs git tag 메시지 (8번 항목 직전 mini-brainstorm) + +--- + +## 9. 변경 이력 + +| 일자 | 변경 | +|------|------| +| 2026-04-26 | 초안 — F1~F6 의 8개 항목 순차 로드맵, 데이터 안전 우선 (Option A), 머지+테스트 게이트 (Option A), 데이터 라이프사이클 우선 순서 (Option X), v0.2.1 단일 cut | diff --git a/package-lock.json b/package-lock.json index c8f9f6d..f11fc30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.2.0", "dependencies": { "better-sqlite3": "12.9.0", - "electron": "41.3.0", "electron-log": "5.2.0", "react": "19.2.5", "react-dom": "19.2.5", @@ -24,6 +23,8 @@ "@types/react": "19.0.0", "@types/react-dom": "19.0.0", "@vitejs/plugin-react": "5.1.4", + "electron": "41.3.0", + "electron-builder": "26.8.1", "electron-vite": "5.0.0", "typescript": "6.0.3", "undici": "8.1.0", @@ -329,10 +330,132 @@ "node": ">=6.9.0" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -350,6 +473,336 @@ "global-agent": "^3.0.0" } }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/node-abi": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", + "integrity": "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -792,6 +1245,19 @@ "node": ">=18" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -842,6 +1308,84 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/@playwright/test": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", @@ -1258,6 +1802,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1277,6 +1822,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -1344,6 +1890,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -1363,6 +1910,16 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -1377,30 +1934,62 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.0.tgz", "integrity": "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.8.0" } }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", @@ -1425,15 +2014,25 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/yauzl": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1574,6 +2173,287 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1584,6 +2464,61 @@ "node": ">=12" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1656,9 +2591,23 @@ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, "license": "MIT", "optional": true }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1721,11 +2670,96 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, "license": "MIT", "engines": { "node": "*" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -1740,6 +2774,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" @@ -1749,6 +2784,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -1763,6 +2799,20 @@ "node": ">=8" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001790", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz", @@ -1794,16 +2844,90 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone-response": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -1812,6 +2936,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1819,6 +3003,72 @@ "dev": true, "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1830,6 +3080,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1883,6 +3134,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1892,6 +3144,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1910,6 +3163,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1924,6 +3178,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1937,13 +3201,199 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, "license": "MIT", "optional": true }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron": { "version": "41.3.0", "resolved": "https://registry.npmjs.org/electron/-/electron-41.3.0.tgz", "integrity": "sha512-2Q5aeocmFdeheZGDUTrAvSR3t+n0c3d104AJWWEnt7syJU0tE4VdibMYaPtQ47QuXSoUf0/xSsfUUvu/uSXIfg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1958,6 +3408,83 @@ "node": ">= 12.20.55" } }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-log": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.2.0.tgz", @@ -1967,6 +3494,61 @@ "node": ">= 14" } }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.344", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", @@ -2004,10 +3586,49 @@ } } }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, "node_modules/electron/node_modules/@types/node": { "version": "24.12.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2017,6 +3638,14 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/end-of-stream": { @@ -2032,17 +3661,25 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -2051,8 +3688,8 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" } @@ -2064,10 +3701,40 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, "license": "MIT", "optional": true }, @@ -2127,6 +3794,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2165,10 +3833,18 @@ "node": ">=12.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -2185,10 +3861,36 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" @@ -2218,6 +3920,63 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2228,6 +3987,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -2238,6 +3998,13 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -2253,6 +4020,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2263,10 +4040,60 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -2284,10 +4111,64 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/global-agent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -2306,6 +4187,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, "license": "ISC", "optional": true, "bin": { @@ -2319,6 +4201,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2336,8 +4219,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">= 0.4" }, @@ -2349,6 +4232,7 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -2374,12 +4258,24 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2389,16 +4285,107 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -2408,6 +4395,51 @@ "node": ">=10.19.0" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -2428,6 +4460,18 @@ ], "license": "BSD-3-Clause" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2440,6 +4484,67 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2447,6 +4552,19 @@ "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2464,12 +4582,21 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, "license": "ISC", "optional": true }, @@ -2490,6 +4617,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -2499,15 +4627,31 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2537,6 +4681,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2546,15 +4691,78 @@ "node": ">=10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -2564,6 +4772,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -2574,6 +4819,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2625,6 +4871,111 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/node-releases": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", @@ -2632,10 +4983,27 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2648,6 +5016,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -2678,6 +5047,43 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2690,10 +5096,26 @@ "dev": true, "license": "MIT" }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -2748,6 +5170,21 @@ "node": ">=18" } }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, "node_modules/postcss": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", @@ -2777,6 +5214,36 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -2804,15 +5271,52 @@ "node": ">=10" } }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -2823,10 +5327,21 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2881,6 +5396,19 @@ "node": ">=0.10.0" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -2895,16 +5423,46 @@ "node": ">= 6" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, "license": "MIT" }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -2913,10 +5471,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -2996,6 +5580,33 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3006,6 +5617,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3015,6 +5627,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, "license": "MIT", "optional": true }, @@ -3022,6 +5635,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -3034,6 +5648,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -3041,6 +5678,13 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3086,6 +5730,70 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3096,10 +5804,22 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, "license": "BSD-3-Clause", "optional": true }, @@ -3110,6 +5830,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", @@ -3126,6 +5856,34 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3139,6 +5897,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" @@ -3147,6 +5906,36 @@ "node": ">= 8.0" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -3175,6 +5964,110 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3219,6 +6112,36 @@ "node": ">=14.0.0" } }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3235,6 +6158,7 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { @@ -3272,12 +6196,14 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "dev": true, "license": "MIT" }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -3314,6 +6240,23 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3333,6 +6276,22 @@ "uuid": "dist-node/bin/uuid" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/vite": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", @@ -3997,6 +6956,22 @@ } } }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -4014,12 +6989,50 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -4027,16 +7040,59 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 3375d61..2dc1858 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,8 @@ "name": "inkling", "version": "0.2.0", "private": true, + "description": "Inkling — local-first 기억 구출 도구", + "author": "altair823 ", "main": "out/main/index.js", "scripts": { "dev": "electron-vite dev", @@ -18,11 +20,37 @@ "test:watch": "vitest", "test:integration": "INKLING_INTEGRATION=1 vitest run tests/integration", "test:e2e": "playwright test", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "predist": "npm run rebuild:electron && npm run build", + "dist": "electron-builder --win --x64", + "predist:dir": "npm run rebuild:electron && npm run build", + "dist:dir": "electron-builder --dir --win --x64" + }, + "build": { + "appId": "xyz.altair823.inkling", + "productName": "Inkling", + "files": [ + "out/**/*", + "package.json" + ], + "asarUnpack": [ + "**/*.node" + ], + "win": { + "target": [ + { "target": "nsis", "arch": ["x64"] } + ] + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "deleteAppDataOnUninstall": false, + "shortcutName": "Inkling" + } }, "dependencies": { "better-sqlite3": "12.9.0", - "electron": "41.3.0", "electron-log": "5.2.0", "react": "19.2.5", "react-dom": "19.2.5", @@ -37,6 +65,8 @@ "@types/react": "19.0.0", "@types/react-dom": "19.0.0", "@vitejs/plugin-react": "5.1.4", + "electron": "41.3.0", + "electron-builder": "26.8.1", "electron-vite": "5.0.0", "typescript": "6.0.3", "undici": "8.1.0", diff --git a/src/main/index.ts b/src/main/index.ts index ee042f7..aa69ea0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,6 +1,8 @@ import electron from 'electron'; const { app, BrowserWindow, Notification } = electron; import '@shared/types'; +import { existsSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; import { initLogger, logger } from './logger.js'; import { resolveProfilePaths } from './paths.js'; import { openDb } from './db/index.js'; @@ -22,12 +24,30 @@ import { } from './windows/quickCaptureWindow.js'; import { createTray } from './tray.js'; import { MediaGc } from './services/MediaGc.js'; +import { BackupService } from './services/BackupService.js'; + +const HIDDEN_ARG = '--hidden'; +const startedHidden = process.argv.includes(HIDDEN_ARG); app.whenReady().then(async () => { initLogger(); - logger.info('app.start', { platform: process.platform, version: app.getVersion() }); + logger.info('app.start', { + platform: process.platform, + version: app.getVersion(), + packaged: app.isPackaged, + hidden: startedHidden + }); const paths = resolveProfilePaths('default'); + + if (app.isPackaged && process.platform === 'win32') { + const initFlag = join(paths.profileDir, '.autostart-init'); + if (!existsSync(initFlag)) { + app.setLoginItemSettings({ openAtLogin: true, args: [HIDDEN_ARG] }); + writeFileSync(initFlag, new Date().toISOString()); + logger.info('autostart.enabled.firstRun'); + } + } const db = openDb(paths.dbFile); const repo = new NoteRepository(db); const store = new MediaStore(paths.profileDir); @@ -73,21 +93,59 @@ app.whenReady().then(async () => { }); if (!reg.ok) logger.warn('hotkey.register.failed', { reason: reg.reason }); - createInboxWindow(); + if (!startedHidden) { + createInboxWindow(); + } createQuickCaptureWindow(); - createTray( - () => createInboxWindow(), - () => showQuickCapture() - ); - await worker.loadFromDb(); const gc = new MediaGc(db, store); void gc.run().then((r) => logger.info('media.gc', { ...r } as Record)); + const backup = new BackupService(db, join(paths.profileDir, 'backups')); + void backup.runDaily() + .then((r) => logger.info('backup.daily', { ...r } as Record)) + .catch((e) => logger.warn('backup.daily.failed', { reason: String(e) })); + + let backupOnQuitDone = false; + app.on('before-quit', (e) => { + if (backupOnQuitDone) return; + e.preventDefault(); + backup.runDaily() + .then((r) => logger.info('backup.beforeQuit', { ...r } as Record)) + .catch((e2) => logger.warn('backup.beforeQuit.failed', { reason: String(e2) })) + .finally(() => { + backupOnQuitDone = true; + app.isQuitting = true; + app.quit(); + }); + }); + + createTray( + () => createInboxWindow(), + () => showQuickCapture(), + async () => { + try { + const r = await backup.runDaily(); + new Notification({ + title: 'Inkling', + body: r.snapshotted + ? `백업 완료 — ${r.removed?.length ?? 0}개 정리` + : `오늘 백업이 이미 있습니다`, + silent: true + }).show(); + } catch (e) { + logger.warn('backup.manual.failed', { reason: String(e) }); + new Notification({ + title: 'Inkling', + body: '백업을 만들지 못했습니다.', + silent: true + }).show(); + } + } + ); + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createInboxWindow(); }); }); - -app.on('before-quit', () => { app.isQuitting = true; }); diff --git a/src/main/services/BackupService.ts b/src/main/services/BackupService.ts new file mode 100644 index 0000000..6e60a0b --- /dev/null +++ b/src/main/services/BackupService.ts @@ -0,0 +1,105 @@ +import type Database from 'better-sqlite3'; +import { mkdir, rename, stat, readdir, unlink, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { applyGfsRetention } from './backupRotation.js'; + +const KST_OFFSET_MS = 9 * 60 * 60 * 1000; +const MARKER_FILENAME = '.last-snapshot'; + +function toKstDateKey(d: Date): string { + const k = new Date(d.getTime() + KST_OFFSET_MS); + return k.toISOString().slice(0, 10); +} + +export interface SnapshotResult { + path: string; + bytes: number; +} + +export interface RotateResult { + kept: string[]; + removed: string[]; +} + +export interface DailyResult { + snapshotted: boolean; + reason?: string; + path?: string; + bytes?: number; + kept?: string[]; + removed?: string[]; +} + +export class BackupService { + constructor( + private db: Database.Database, + private backupDir: string, + private now: () => Date = () => new Date() + ) {} + + /** + * Atomic snapshot of the SQLite DB to `inkling-YYYY-MM-DD.sqlite` (KST). + * + * NOT safe for concurrent calls — two parallel calls race on the same + * tmp/final path. Callers should serialize via {@link runDaily}, which + * gates on the `.last-snapshot` marker. + */ + async snapshot(): Promise { + await mkdir(this.backupDir, { recursive: true }); + const dateKey = toKstDateKey(this.now()); + const finalPath = join(this.backupDir, `inkling-${dateKey}.sqlite`); + const tmpPath = `${finalPath}.tmp`; + try { + await this.db.backup(tmpPath); + await rename(tmpPath, finalPath); + } catch (e) { + await unlink(tmpPath).catch(() => { /* tmp may not exist */ }); + throw e; + } + const st = await stat(finalPath); + return { path: finalPath, bytes: st.size }; + } + + async rotate(): Promise { + let entries: string[]; + try { + entries = await readdir(this.backupDir); + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') return { kept: [], removed: [] }; + throw e; + } + const decision = applyGfsRetention(entries, this.now()); + for (const name of decision.remove) { + await unlink(join(this.backupDir, name)); + } + return { kept: decision.keep, removed: decision.remove }; + } + + async lastSnapshotAt(): Promise { + try { + const raw = await readFile(join(this.backupDir, MARKER_FILENAME), 'utf8'); + return raw.trim() || null; + } catch (e: unknown) { + if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw e; + } + } + + async runDaily(): Promise { + const today = toKstDateKey(this.now()); + const last = await this.lastSnapshotAt(); + if (last === today) { + return { snapshotted: false, reason: 'already snapshotted today' }; + } + const snap = await this.snapshot(); + await writeFile(join(this.backupDir, MARKER_FILENAME), today, 'utf8'); + const rot = await this.rotate(); + return { + snapshotted: true, + path: snap.path, + bytes: snap.bytes, + kept: rot.kept, + removed: rot.removed + }; + } +} diff --git a/src/main/services/backupRotation.ts b/src/main/services/backupRotation.ts new file mode 100644 index 0000000..e6631b0 --- /dev/null +++ b/src/main/services/backupRotation.ts @@ -0,0 +1,77 @@ +const BACKUP_FILENAME_REGEX = /^inkling-(\d{4}-\d{2}-\d{2})\.sqlite$/; +const ONE_DAY_MS = 24 * 60 * 60 * 1000; +const DAILY_WINDOW_DAYS = 14; +const WEEKLY_WINDOW_PRIOR_MONDAYS = 4; +const MONTHLY_WINDOW_COUNT = 6; + +export function parseBackupFilename(name: string): string | null { + const m = BACKUP_FILENAME_REGEX.exec(name); + if (!m) return null; + const iso = m[1]!; + const d = new Date(iso + 'T00:00:00Z'); + if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== iso) return null; + return iso; +} + +export interface RetentionResult { + keep: string[]; + remove: string[]; +} + +function startOfDayUtc(d: Date): Date { + const x = new Date(d); + x.setUTCHours(0, 0, 0, 0); + return x; +} + +function isWithinDailyWindow(fileDate: Date, today: Date): boolean { + const oldest = new Date(today.getTime() - (DAILY_WINDOW_DAYS - 1) * ONE_DAY_MS); + return fileDate >= oldest && fileDate <= today; +} + +function isWithinWeeklyWindow(fileDate: Date, today: Date): boolean { + // UTC-based Monday detection. UTCDay: 0=Sun, 1=Mon..6=Sat + if (fileDate.getUTCDay() !== 1) return false; + // Weekly window: anchor on the most recent Monday on/before `today`, then reach back + // WEEKLY_WINDOW_PRIOR_MONDAYS * 7 days from that anchor. Effective semantic: + // up to 5 distinct Mondays kept (the anchor Monday + 4 prior). The plan's commit + // header says "4 weekly Mondays" but the plan's test case at + // backupRotation.test.ts requires 03-23 (5th Monday from 2026-04-26) to be kept, + // so the test is treated as source of truth. + const dayOfWeek = today.getUTCDay(); // 0=Sun..6=Sat + const daysSinceMonday = (dayOfWeek + 6) % 7; // Mon=0, Sun=6 + const lastMonday = new Date(today.getTime() - daysSinceMonday * ONE_DAY_MS); + const oldest = new Date(lastMonday.getTime() - WEEKLY_WINDOW_PRIOR_MONDAYS * 7 * ONE_DAY_MS); + return fileDate >= oldest && fileDate <= today; +} + +function isWithinMonthlyWindow(fileDate: Date, today: Date): boolean { + if (fileDate.getUTCDate() !== 1) return false; + // months ago: difference in calendar months + const monthsAgo = + (today.getUTCFullYear() - fileDate.getUTCFullYear()) * 12 + + (today.getUTCMonth() - fileDate.getUTCMonth()); + return monthsAgo >= 0 && monthsAgo < MONTHLY_WINDOW_COUNT; +} + +export function applyGfsRetention(filenames: string[], now: Date): RetentionResult { + const keep: string[] = []; + const remove: string[] = []; + const today = startOfDayUtc(now); + for (const name of filenames) { + const iso = parseBackupFilename(name); + if (iso === null) continue; // unrecognized — ignore (no-op) + const fileDate = new Date(iso + 'T00:00:00Z'); + if (fileDate > today) { + keep.push(name); // future-dated — clock skew safety + continue; + } + const survives = + isWithinDailyWindow(fileDate, today) || + isWithinWeeklyWindow(fileDate, today) || + isWithinMonthlyWindow(fileDate, today); + if (survives) keep.push(name); + else remove.push(name); + } + return { keep, remove }; +} diff --git a/src/main/tray.ts b/src/main/tray.ts index 270dc43..d8f4fac 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,20 +1,50 @@ import electron from 'electron'; -import type { Tray as TrayType } from 'electron'; +import type { Tray as TrayType, MenuItemConstructorOptions } from 'electron'; const { app, Tray, Menu, nativeImage } = electron; let tray: TrayType | null = null; -export function createTray(showInbox: () => void, showCapture: () => void): TrayType { - const icon = nativeImage.createEmpty(); - tray = new Tray(icon); - tray.setToolTip('Inkling'); - const menu = Menu.buildFromTemplate([ +function buildMenu( + showInbox: () => void, + showCapture: () => void, + runBackup: () => void +) { + const items: MenuItemConstructorOptions[] = [ { label: '구출한 메모 보기', click: showInbox }, { label: '기억 구출하기', click: showCapture }, { type: 'separator' }, - { label: '종료', click: () => { app.isQuitting = true; app.quit(); } } - ]); - tray.setContextMenu(menu); + { label: '지금 백업', click: runBackup } + ]; + if (app.isPackaged) { + const { openAtLogin } = app.getLoginItemSettings(); + items.push({ + label: '윈도우 시작 시 자동 실행', + type: 'checkbox', + checked: openAtLogin, + click: (item) => { + app.setLoginItemSettings({ + openAtLogin: item.checked, + args: ['--hidden'] + }); + } + }); + items.push({ type: 'separator' }); + } else { + items.push({ type: 'separator' }); + } + items.push({ label: '종료', click: () => { app.isQuitting = true; app.quit(); } }); + return Menu.buildFromTemplate(items); +} + +export function createTray( + showInbox: () => void, + showCapture: () => void, + runBackup: () => void +): TrayType { + const icon = nativeImage.createEmpty(); + tray = new Tray(icon); + tray.setToolTip('Inkling'); + tray.setContextMenu(buildMenu(showInbox, showCapture, runBackup)); tray.on('click', showInbox); return tray; } diff --git a/tests/unit/BackupService.test.ts b/tests/unit/BackupService.test.ts new file mode 100644 index 0000000..4a0ad8e --- /dev/null +++ b/tests/unit/BackupService.test.ts @@ -0,0 +1,171 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import Database from 'better-sqlite3'; +import { mkdtempSync, rmSync, existsSync, readdirSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { runMigrations } from '@main/db/migrations/index.js'; +import { BackupService } from '@main/services/BackupService.js'; +import { readFileSync, writeFileSync } from 'node:fs'; + +describe('BackupService.snapshot', () => { + let dir: string; + let db: Database.Database; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'inkling-backup-')); + db = new Database(':memory:'); + runMigrations(db); + db.prepare( + `INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at) + VALUES (?, ?, 'pending', ?, ?)` + ).run('n1', 'hello', '2026-04-26T00:00:00Z', '2026-04-26T00:00:00Z'); + }); + + afterEach(() => { + db.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + it('writes inkling-YYYY-MM-DD.sqlite (KST date) to backupDir', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); // 21:00 KST + const r = await svc.snapshot(); + expect(r.path).toBe(join(dir, 'inkling-2026-04-26.sqlite')); + expect(existsSync(r.path)).toBe(true); + expect(r.bytes).toBeGreaterThan(0); + }); + + it('uses KST date even when UTC date differs (around midnight)', async () => { + // 2026-04-26 23:30 UTC = 2026-04-27 08:30 KST + const svc = new BackupService(db, dir, () => new Date('2026-04-26T23:30:00Z')); + const r = await svc.snapshot(); + expect(r.path).toBe(join(dir, 'inkling-2026-04-27.sqlite')); + }); + + it('overwrites same-day backup atomically (no partial files left)', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.snapshot(); + await svc.snapshot(); + const files = readdirSync(dir).filter((f) => f.startsWith('inkling-')); + expect(files).toEqual(['inkling-2026-04-26.sqlite']); + // No leftover .tmp files + expect(readdirSync(dir).some((f) => f.endsWith('.tmp'))).toBe(false); + }); + + it('snapshot file is a valid SQLite DB containing the source row', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.snapshot(); + const restored = new Database(r.path, { readonly: true }); + const row = restored.prepare('SELECT id, raw_text FROM notes').get() as + | { id: string; raw_text: string } + | undefined; + expect(row?.id).toBe('n1'); + expect(row?.raw_text).toBe('hello'); + restored.close(); + }); + + it('creates backupDir if it does not exist', async () => { + const fresh = join(dir, 'nested', 'backups'); + expect(existsSync(fresh)).toBe(false); + const svc = new BackupService(db, fresh, () => new Date('2026-04-26T12:00:00Z')); + await svc.snapshot(); + expect(existsSync(fresh)).toBe(true); + }); + + it('snapshot file is not zero bytes (regression: empty backup)', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.snapshot(); + expect(statSync(r.path).size).toBeGreaterThan(100); + }); + + it('cleans up orphan .tmp file when db.backup() fails', async () => { + // Inject a fake db whose backup() rejects to simulate disk-full / IO-error. + const fakeDb = { + backup: async (tmpPath: string) => { + // Simulate a partial write before the failure (real failures may leave bytes). + await import('node:fs/promises').then((fs) => + fs.writeFile(tmpPath, 'partial-bytes') + ); + throw new Error('simulated disk full'); + } + } as unknown as Database.Database; + const svc = new BackupService(fakeDb, dir, () => new Date('2026-04-26T12:00:00Z')); + await expect(svc.snapshot()).rejects.toThrow('simulated disk full'); + // No leftover .tmp file + const remaining = readdirSync(dir); + expect(remaining.filter((f) => f.endsWith('.tmp'))).toEqual([]); + // No final file either (write never completed) + expect(remaining.filter((f) => f.endsWith('.sqlite'))).toEqual([]); + }); +}); + +describe('BackupService.runDaily', () => { + let dir: string; + let db: Database.Database; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'inkling-backup-')); + db = new Database(':memory:'); + runMigrations(db); + }); + + afterEach(() => { + db.close(); + rmSync(dir, { recursive: true, force: true }); + }); + + it('snapshots when marker is absent', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.runDaily(); + expect(r.snapshotted).toBe(true); + expect(existsSync(join(dir, '.last-snapshot'))).toBe(true); + expect(existsSync(join(dir, 'inkling-2026-04-26.sqlite'))).toBe(true); + }); + + it('skips when marker shows today already snapshotted', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.runDaily(); // first + const r = await svc.runDaily(); // second same day + expect(r.snapshotted).toBe(false); + expect(r.reason).toMatch(/already/); + }); + + it('snapshots again when marker shows different date', async () => { + // Pre-seed marker as yesterday + const dir2 = dir; + await new BackupService(db, dir2, () => new Date('2026-04-25T12:00:00Z')).runDaily(); + const svc = new BackupService(db, dir2, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.runDaily(); + expect(r.snapshotted).toBe(true); + expect(existsSync(join(dir2, 'inkling-2026-04-26.sqlite'))).toBe(true); + expect(existsSync(join(dir2, 'inkling-2026-04-25.sqlite'))).toBe(true); + }); + + it('runs rotation after snapshot', async () => { + // Pre-create an old file that should be rotated out + const ancient = join(dir, 'inkling-2024-01-01.sqlite'); + writeFileSync(ancient, 'fake'); + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + const r = await svc.runDaily(); + expect(r.snapshotted).toBe(true); + expect(r.removed).toContain('inkling-2024-01-01.sqlite'); + expect(existsSync(ancient)).toBe(false); + }); + + it('marker contains ISO date matching the snapshot file', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.runDaily(); + const marker = readFileSync(join(dir, '.last-snapshot'), 'utf8').trim(); + expect(marker).toBe('2026-04-26'); + }); + + it('lastSnapshotAt returns null when marker absent', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + expect(await svc.lastSnapshotAt()).toBeNull(); + }); + + it('lastSnapshotAt returns marker date when present', async () => { + const svc = new BackupService(db, dir, () => new Date('2026-04-26T12:00:00Z')); + await svc.runDaily(); + expect(await svc.lastSnapshotAt()).toBe('2026-04-26'); + }); +}); diff --git a/tests/unit/backupRotation.test.ts b/tests/unit/backupRotation.test.ts new file mode 100644 index 0000000..d981b6d --- /dev/null +++ b/tests/unit/backupRotation.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect } from 'vitest'; +import { parseBackupFilename, applyGfsRetention } from '@main/services/backupRotation.js'; + +describe('parseBackupFilename', () => { + it('extracts ISO date from valid filename', () => { + expect(parseBackupFilename('inkling-2026-04-26.sqlite')).toBe('2026-04-26'); + }); + + it('returns null for non-matching filename', () => { + expect(parseBackupFilename('something-else.sqlite')).toBeNull(); + expect(parseBackupFilename('inkling-2026-13-99.sqlite')).toBeNull(); + expect(parseBackupFilename('.last-snapshot')).toBeNull(); + }); + + it('returns null for date that JS would silently coerce (roundtrip lock-in)', () => { + // Without the toISOString roundtrip check, JS coerces 2026-02-30 to 2026-03-02. + // This test locks in the roundtrip-validation contract. + expect(parseBackupFilename('inkling-2026-02-30.sqlite')).toBeNull(); + }); +}); + +describe('applyGfsRetention', () => { + // KST-naive logic — caller passes UTC `now`. Filenames are KST date keys. + const NOW = new Date('2026-04-26T12:00:00Z'); // 2026-04-26 21:00 KST + + function names(...dates: string[]): string[] { + return dates.map((d) => `inkling-${d}.sqlite`); + } + + it('keeps files within last 14 days (daily window)', () => { + const files = names( + '2026-04-26', '2026-04-25', '2026-04-20', '2026-04-13', '2026-04-12' + ); + const r = applyGfsRetention(files, NOW); + // 14 day window from 2026-04-26 reaches back to 2026-04-13 inclusive. + expect(r.keep).toContain('inkling-2026-04-26.sqlite'); + expect(r.keep).toContain('inkling-2026-04-25.sqlite'); + expect(r.keep).toContain('inkling-2026-04-20.sqlite'); + expect(r.keep).toContain('inkling-2026-04-13.sqlite'); + expect(r.remove).toContain('inkling-2026-04-12.sqlite'); + }); + + it('keeps last 4 Mondays beyond the 14 day window', () => { + // Mondays in 2026: 04-13, 04-06, 03-30, 03-23, 03-16, 03-09 + const files = names( + '2026-04-13', // within 14-day, also a Monday + '2026-04-06', // outside 14-day, but a Monday in last 4 weeks + '2026-03-30', // a Monday in last 4 weeks + '2026-03-23', // a Monday in last 4 weeks + '2026-03-16', // a Monday more than 4 weeks ago — REMOVE unless month-1 + '2026-03-09' // a Monday more than 4 weeks ago — REMOVE + ); + const r = applyGfsRetention(files, NOW); + expect(r.keep).toContain('inkling-2026-04-06.sqlite'); + expect(r.keep).toContain('inkling-2026-03-30.sqlite'); + expect(r.keep).toContain('inkling-2026-03-23.sqlite'); + expect(r.remove).toContain('inkling-2026-03-16.sqlite'); + expect(r.remove).toContain('inkling-2026-03-09.sqlite'); + }); + + it('weekly window is inclusive at oldest boundary', () => { + // 2026-03-23 is exactly 4*7 days before anchor Monday 2026-04-20. + // Locks in the boundary semantic explicitly. + const r = applyGfsRetention(names('2026-03-23', '2026-03-16'), NOW); + expect(r.keep).toContain('inkling-2026-03-23.sqlite'); + expect(r.remove).toContain('inkling-2026-03-16.sqlite'); + }); + + it('keeps month-firsts within last 6 months', () => { + // Last 6 month-firsts from 2026-04-26: 2026-04-01, 2026-03-01, 2026-02-01, + // 2026-01-01, 2025-12-01, 2025-11-01 + const files = names( + '2026-04-01', // within 14-day already + '2026-03-01', // outside 14-day, outside 4-week-Monday — keep via month rule + '2026-02-01', + '2026-01-01', + '2025-12-01', + '2025-11-01', + '2025-10-01' // outside 6-month window — REMOVE + ); + const r = applyGfsRetention(files, NOW); + expect(r.keep).toContain('inkling-2026-03-01.sqlite'); + expect(r.keep).toContain('inkling-2026-02-01.sqlite'); + expect(r.keep).toContain('inkling-2025-11-01.sqlite'); + expect(r.remove).toContain('inkling-2025-10-01.sqlite'); + }); + + it('ignores files that do not match backup pattern', () => { + const files = ['random.sqlite', 'inkling.sqlite', '.last-snapshot', 'inkling-bad-date.sqlite']; + const r = applyGfsRetention(files, NOW); + expect(r.keep).toEqual([]); + expect(r.remove).toEqual([]); + }); + + it('keeps future-dated files (clock skew safety)', () => { + const files = names('2030-01-01'); + const r = applyGfsRetention(files, NOW); + expect(r.keep).toContain('inkling-2030-01-01.sqlite'); + expect(r.remove).toEqual([]); + }); + + it('a file kept by any rule is in keep, never in both lists', () => { + const files = names('2026-04-26', '2026-04-13', '2026-03-23', '2026-03-01'); + const r = applyGfsRetention(files, NOW); + const intersection = r.keep.filter((f) => r.remove.includes(f)); + expect(intersection).toEqual([]); + }); +});