Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
177 commits
Select commit Hold shift + click to select a range
9a48ab2
feat(api): add notebookSyncLog table to Drizzle schema
tomymaritano Mar 11, 2026
3d49703
feat(storage): add notebook sync tracking migration with triggers
tomymaritano Mar 11, 2026
b6c0d96
feat(desktop): add notebook sync methods to ApiClient
tomymaritano Mar 11, 2026
403642e
test(sync-core): add tree validation unit tests for notebook sync
tomymaritano Mar 11, 2026
628b16b
feat(api): add notebook sync pull/push endpoints with tree validation
tomymaritano Mar 11, 2026
a9fde10
feat(storage): add sync methods to SQLiteNotebookRepository
tomymaritano Mar 11, 2026
26401e3
feat(desktop): integrate notebook sync into SyncService
tomymaritano Mar 11, 2026
db02671
fix(desktop): fix Timestamp type casts and use static createNotebook …
tomymaritano Mar 11, 2026
535d53c
refactor(sync-core): extract shared tree validation to sync-core package
tomymaritano Mar 11, 2026
4055595
chore: format notebook sync code and fix unused variable
tomymaritano Mar 11, 2026
305bcb5
test(sync-core): add SyncQueue unit tests
tomymaritano Mar 11, 2026
fcd26b9
test(sync-core): add SyncEngine unit tests
tomymaritano Mar 11, 2026
a88f19d
style: format sync-core test files
tomymaritano Mar 11, 2026
488359e
feat: surface sync conflicts in status indicator
tomymaritano Mar 11, 2026
f34ab5e
feat(storage): add tag sync tracking migration (UUID + triggers)
tomymaritano Mar 11, 2026
4d2c4ac
feat(api): add tagSyncLog table to server schema
tomymaritano Mar 11, 2026
fff8cd8
feat(api): add tag sync pull/push endpoints
tomymaritano Mar 11, 2026
0782ea8
feat(storage): add tag sync repository methods
tomymaritano Mar 11, 2026
1375fcd
fix(storage): remove double-invocation on transaction calls
tomymaritano Mar 11, 2026
51e8112
feat(desktop): add tag sync API client methods
tomymaritano Mar 11, 2026
2e60230
feat(desktop): add IPC bridge for tag sync
tomymaritano Mar 11, 2026
26f2757
feat(desktop): integrate tag sync into sync cycle
tomymaritano Mar 11, 2026
043148c
fix: address PR review feedback for notebook sync
tomymaritano Mar 11, 2026
e8250b8
Merge pull request #117 from tomymaritano/feature/notebook-sync
tomymaritano Mar 11, 2026
c913465
Merge pull request #118 from tomymaritano/feature/sync-tests
tomymaritano Mar 11, 2026
26bccb5
Merge pull request #119 from tomymaritano/feature/conflict-ui-polish
tomymaritano Mar 11, 2026
31a880e
Merge remote-tracking branch 'origin/develop' into feature/tag-sync
tomymaritano Mar 11, 2026
63610e4
Merge pull request #120 from tomymaritano/feature/tag-sync
tomymaritano Mar 11, 2026
73f9938
chore: add CodeRabbit configuration
tomymaritano Mar 11, 2026
cd01862
feat(plugin-api): add PluginHookOptions type for remark/rehype regist…
tomymaritano Mar 11, 2026
e1eb092
feat: add safePluginWrapper for graceful plugin failure handling
tomymaritano Mar 11, 2026
cb9a82e
feat: pass metadata through plugin registration in PluginRegistry
tomymaritano Mar 11, 2026
aa2bbf6
feat(desktop): add CSS for plugin error block boundaries in preview
tomymaritano Mar 11, 2026
46ae6de
feat: hot-reload preview when toggling plugins on/off
tomymaritano Mar 11, 2026
fbb5448
feat(plugin-api): upgrade remark/rehype stores with metadata, priorit…
tomymaritano Mar 11, 2026
4a087e7
test(plugin-api): update tests for new metadata and safePluginWrapper
tomymaritano Mar 11, 2026
3902345
docs: add remark/rehype hooks enhancement design plan
tomymaritano Mar 11, 2026
47fec79
docs: add theme system enhancement design
tomymaritano Mar 11, 2026
614b26f
docs: add theme system implementation plan
tomymaritano Mar 11, 2026
06a147d
feat(plugin-api): add theme types with token whitelist and ThemeRegis…
tomymaritano Mar 11, 2026
f5f8f25
feat(plugin-api): add useThemeOverrides hook and registerTheme contex…
tomymaritano Mar 11, 2026
5fe153e
feat: add nativeTheme IPC sync between main, preload, and renderer
tomymaritano Mar 11, 2026
56ba2a5
feat: add theme settings schema, UI selector, and startup restore
tomymaritano Mar 11, 2026
1f61ed7
test: add theme token validation and ThemeRegistry store tests
tomymaritano Mar 11, 2026
bf58f4a
docs: add Data Access API design document
tomymaritano Mar 11, 2026
7bda4c1
docs: add Data Access API implementation plan
tomymaritano Mar 11, 2026
b44fc75
feat(plugin-api): add DataAPI types, query options, and DataAccessError
tomymaritano Mar 11, 2026
9e16eec
feat(plugin-api): add DataAPI interface, bridge, and createDataAPI fa…
tomymaritano Mar 11, 2026
78a20cd
test(plugin-api): add comprehensive tests for createDataAPI
tomymaritano Mar 11, 2026
3d16297
feat(plugin-api): wire DataAPI into PluginRegistry and PluginHost
tomymaritano Mar 11, 2026
d8f8ca6
feat(plugin-api): export DataAPI types and factory from barrel
tomymaritano Mar 11, 2026
589fe17
feat: wire DataAPI bridge to IPC in App.tsx and fire data events
tomymaritano Mar 11, 2026
9f6f229
feat(api): add device list and rename endpoints
tomymaritano Mar 11, 2026
48195ab
feat(api): add device revoke and revoke-others endpoints
tomymaritano Mar 11, 2026
8ea9877
fix(api): reject token refresh for revoked devices
tomymaritano Mar 11, 2026
016a524
feat(desktop): add device management IPC handlers
tomymaritano Mar 11, 2026
0cd1214
feat(desktop): expose devices IPC bridge in preload
tomymaritano Mar 11, 2026
308c229
feat(desktop): add device management UI in settings
tomymaritano Mar 11, 2026
d1c3cb7
test(api): scaffold test infrastructure with vitest and helpers
tomymaritano Mar 11, 2026
2cb58bd
test(api): add note sync pull endpoint tests
tomymaritano Mar 11, 2026
fcc9d96
test(api): add note sync push and conflict detection tests
tomymaritano Mar 11, 2026
9f137d3
test(api): add notebook sync and tree validation tests
tomymaritano Mar 11, 2026
36e1a5b
test(api): add tag sync and sync status endpoint tests
tomymaritano Mar 11, 2026
d104dd1
test: add encryption round-trip tests for AES-256-GCM
tomymaritano Mar 11, 2026
a6b3558
docs: add sync hardening design and implementation plan
tomymaritano Mar 11, 2026
0e435e2
feat(storage): add sync_history migration for sync cycle metrics
tomymaritano Mar 11, 2026
080cb68
feat(storage): add sync history repository methods
tomymaritano Mar 11, 2026
3646cb2
feat(api-client): add bandwidth tracking to request method
tomymaritano Mar 11, 2026
57d4303
feat(sync): record sync history with per-cycle metrics and bandwidth
tomymaritano Mar 11, 2026
8153c78
feat(ipc): expose sync history via IPC and preload bridge
tomymaritano Mar 11, 2026
fc9ab96
feat(sync): auto-resume sync on network reconnect with debounce
tomymaritano Mar 11, 2026
99b3f56
feat(ui): add sync history section to Settings with bandwidth display
tomymaritano Mar 11, 2026
f4e904c
style: fix Prettier formatting across theme system and sync files
tomymaritano Mar 11, 2026
1e1a44c
docs: add Phase 2 completion design (2.4 + 2.6)
tomymaritano Mar 11, 2026
5af6419
docs: add Phase 2 completion implementation plan (2.4 + 2.6)
tomymaritano Mar 11, 2026
71bf473
fix(plugins): add enum and range to pluginScanner config schema type
tomymaritano Mar 11, 2026
249a2ad
feat(plugin-api): add validateConfigValue for config schema enforcement
tomymaritano Mar 11, 2026
801a738
feat(plugins): validate config values before persisting
tomymaritano Mar 11, 2026
17c9ff2
feat(plugins): track load timing per plugin in runtime store
tomymaritano Mar 11, 2026
3dabece
feat(plugins): add dev-mode Plugin Inspector with load timings and er…
tomymaritano Mar 11, 2026
38a5170
fix: address CodeRabbit review comments on remark/rehype hooks
tomymaritano Mar 11, 2026
c099dbd
fix: address CodeRabbit review comments on theme system
tomymaritano Mar 11, 2026
b8a4e81
docs: add theme system implementation plan
tomymaritano Mar 11, 2026
e013638
feat(plugin-api): add theme types with token whitelist and ThemeRegis…
tomymaritano Mar 11, 2026
c87de4e
feat(plugin-api): add useThemeOverrides hook and registerTheme contex…
tomymaritano Mar 11, 2026
2c753b3
feat: add nativeTheme IPC sync between main, preload, and renderer
tomymaritano Mar 11, 2026
6948a26
feat: add theme settings schema, UI selector, and startup restore
tomymaritano Mar 11, 2026
ff0e19b
test: add theme token validation and ThemeRegistry store tests
tomymaritano Mar 11, 2026
35c346e
style: fix Prettier formatting across theme system and sync files
tomymaritano Mar 11, 2026
35f3804
docs: complete documentation update for v0.8
tomymaritano Mar 11, 2026
09cf396
feat: add docs links, bug reporting, and contributor onboarding
tomymaritano Mar 11, 2026
d773955
Merge pull request #122 from tomymaritano/feature/theme-system
tomymaritano Mar 11, 2026
bc424cb
style: fix prettier formatting
tomymaritano Mar 11, 2026
c8d8833
style: fix prettier formatting
tomymaritano Mar 11, 2026
d8846d9
style: fix prettier formatting
tomymaritano Mar 11, 2026
188c7d8
style: fix prettier formatting
tomymaritano Mar 12, 2026
d86c2ec
style: fix prettier formatting
tomymaritano Mar 12, 2026
d096658
style: fix prettier formatting
tomymaritano Mar 12, 2026
65abbd6
style: fix prettier formatting
tomymaritano Mar 12, 2026
d6783af
fix: address CodeRabbit review comments on PR #127
tomymaritano Mar 12, 2026
df64ff5
fix: address CodeRabbit review comments on PR #123
tomymaritano Mar 12, 2026
f9766d3
style: fix prettier formatting in docs
tomymaritano Mar 12, 2026
452f3bd
Merge pull request #128 from tomymaritano/feature/docs-update
tomymaritano Mar 12, 2026
3f7b49d
Merge remote-tracking branch 'origin/develop' into feature/remark-reh…
tomymaritano Mar 12, 2026
5d112ff
style: fix formatting after merge
tomymaritano Mar 12, 2026
3d41f25
Merge pull request #121 from tomymaritano/feature/remark-rehype-hooks…
tomymaritano Mar 12, 2026
5e3af25
Merge remote-tracking branch 'origin/develop' into feature/phase2-com…
tomymaritano Mar 12, 2026
4b08f07
Merge pull request #127 from tomymaritano/feature/phase2-completion
tomymaritano Mar 12, 2026
7344281
Merge branch 'develop' into feature/data-access-api
tomymaritano Mar 12, 2026
b429ab3
Merge pull request #123 from tomymaritano/feature/data-access-api
tomymaritano Mar 12, 2026
3ed08b4
Merge branch 'develop' into feature/device-management
tomymaritano Mar 12, 2026
cdd2db0
Merge pull request #124 from tomymaritano/feature/device-management
tomymaritano Mar 12, 2026
177091d
Merge remote-tracking branch 'origin/develop' into feature/sync-tests
tomymaritano Mar 12, 2026
9f2ba75
Merge pull request #125 from tomymaritano/feature/sync-tests
tomymaritano Mar 12, 2026
cbe39c8
Merge remote-tracking branch 'origin/develop' into feature/sync-harde…
tomymaritano Mar 12, 2026
38b8128
Merge pull request #126 from tomymaritano/feature/sync-hardening
tomymaritano Mar 12, 2026
592d889
fix: resolve CodeRabbit review issues across merged PRs
tomymaritano Mar 12, 2026
09ac09a
chore: add Husky pre-commit hooks and auto-merge workflow
tomymaritano Mar 12, 2026
5cd5520
fix: use gh pr merge --auto for automerge workflow
tomymaritano Mar 12, 2026
2ef5a9a
ci: level up CI pipeline and Husky hooks
tomymaritano Mar 12, 2026
a8bf258
fix: remove process.env.NODE_ENV check from browser-only plugin-api
tomymaritano Mar 12, 2026
0999107
Merge pull request #129 from tomymaritano/fix/coderabbit-issues
tomymaritano Mar 12, 2026
40c5216
Merge pull request #130 from tomymaritano/feature/husky-automerge
tomymaritano Mar 12, 2026
d65eb5c
feat: unify docs-site and marketing-site into single Next.js app (#132)
tomymaritano Mar 12, 2026
8dc7079
feat: plugin auto-disable + CLI commands (Phase 2.5 & 3.1) (#131)
tomymaritano Mar 12, 2026
024e46c
fix: wrangler-action pnpm monorepo compatibility (#134)
tomymaritano Mar 12, 2026
193bbce
fix: create Cloudflare Pages project before deploy (#136)
tomymaritano Mar 12, 2026
d5cd8ed
fix: use npx wrangler for Cloudflare Pages deploy (#139)
tomymaritano Mar 12, 2026
cfe513d
fix: enable Next.js static export for Cloudflare Pages (#141)
tomymaritano Mar 12, 2026
21be0a5
feat(web): full website redesign with shadcn/ui + Magic UI (#142)
tomymaritano Mar 12, 2026
b959a25
feat(web,desktop,api): website redesign + auth UX rethink (#148)
tomymaritano Mar 13, 2026
00e25bf
Merge remote-tracking branch 'origin/main' into develop
tomymaritano Mar 13, 2026
04e4e18
feat(desktop,api): complete Phase 1-3 roadmap implementation
tomymaritano Mar 13, 2026
21af479
feat(ai,plugins): complete Phase 4-5 — AI knowledge & extensibility
tomymaritano Mar 13, 2026
4ccab57
feat(ai): complete Phase 4-5 — AI command execution + plugin bridge (…
tomymaritano Mar 13, 2026
e908280
chore: Tailwind v4 canonical classes + tsconfig cleanup (#151)
tomymaritano Mar 13, 2026
8a1f5c8
feat(sync): connect auth → payment → sync flow with license gating (#…
tomymaritano Mar 13, 2026
a24a231
feat(ai-core): provider-agnostic AI architecture with streaming (#156)
tomymaritano Mar 14, 2026
64d7010
feat(release): automated release pipeline with semantic-release (#157)
tomymaritano Mar 14, 2026
f686ab6
fix(web): add cleanUrls to fix /auth/verify 404 (#164)
tomymaritano Mar 16, 2026
9d64ea9
feat(ai): add tool use (function calling) to AI assistant (#165)
tomymaritano Mar 18, 2026
a472580
feat(share): public notes API with metadata for portfolio consumption…
tomymaritano Mar 18, 2026
6e05279
Merge remote-tracking branch 'origin/main' into develop
tomymaritano Mar 19, 2026
b2f65aa
fix(web,api): render shared notes as markdown + handle subscription.c…
tomymaritano Mar 19, 2026
01b51cb
Merge branch 'main' into develop
tomymaritano Mar 19, 2026
f6ddcad
feat(desktop): share store + note list and editor UI improvements
tomymaritano Mar 19, 2026
41c811c
feat: AI context scoping, newsletter unsubscribe page, create-note sc…
tomymaritano Mar 19, 2026
078c8f4
feat(mcp): add MCP server for Claude Code integration
tomymaritano Mar 20, 2026
7030149
fix(ui): modernize AI panel sidebar with cleaner chat design
tomymaritano Mar 20, 2026
ad325d8
feat(ai): OpenAI + Ollama providers, secure key storage, Connect flow UI
tomymaritano Mar 20, 2026
1680e01
fix(ui): settings visual refresh — card layout, tighter controls, cle…
tomymaritano Mar 20, 2026
31c5474
feat(admin): add dashboard with app metrics and admin API
tomymaritano Mar 20, 2026
7fd0ee1
feat(dashboard): standalone layout with sidebar, improved UI, admin e…
tomymaritano Mar 20, 2026
2bf9c1e
Merge remote-tracking branch 'origin/main' into develop
tomymaritano Mar 20, 2026
20e101c
ci: add workflow_dispatch to build workflow
tomymaritano Mar 20, 2026
e80caf8
fix(mcp): add sql.js type declarations for CI build
tomymaritano Mar 20, 2026
e3b2ef8
fix(ci): bust corrupted electron-builder cache for Linux build
tomymaritano Mar 20, 2026
1e20308
fix(ci): clean fpm cache before Linux build to fix 7zip extraction error
tomymaritano Mar 21, 2026
fb0dafc
feat: fix cross-device sync with E2EE key hierarchy and docs cleanup …
tomymaritano Mar 29, 2026
3b01d4b
fix: address Codex review findings for sync and deep link (#178)
tomymaritano Mar 29, 2026
3f0bc7f
Merge remote-tracking branch 'origin/main' into develop
tomymaritano Mar 29, 2026
578ad70
fix: move deep link handlers into primary instance block
tomymaritano Mar 29, 2026
ab4a486
chore(deps): bump pnpm/action-setup from 4 to 5 (#182)
dependabot[bot] Apr 2, 2026
dc5bc73
chore(deps): bump actions/labeler from 5 to 6 (#183)
dependabot[bot] Apr 2, 2026
075995d
chore(deps): bump github/codeql-action from 3 to 4 (#181)
dependabot[bot] Apr 2, 2026
0e85b24
fix: comprehensive project audit — CI, security, types, and dependencies
tomymaritano Apr 22, 2026
472bc4e
fix: floating promises, Electron upgrade, and renderer tests
tomymaritano Apr 22, 2026
4e36788
feat: design system primitives, save indicator, toast system, and UX …
tomymaritano Apr 23, 2026
d2d763f
refactor: design system consistency, onboarding, and sync progress
tomymaritano Apr 23, 2026
52a2bbe
feat: advanced search filters and functional plugin marketplace
tomymaritano Apr 23, 2026
68c56b6
feat: high-quality table rendering and improved document export
tomymaritano Apr 23, 2026
2672cdc
Merge remote-tracking branch 'origin/main' into develop
tomymaritano Apr 23, 2026
4ace564
fix: address all PR review comments (security, a11y, UX, minor)
tomymaritano Apr 23, 2026
9bccc62
fix: resolve remaining PR review comments (encryption, sync, CI, prel…
tomymaritano Apr 23, 2026
a1f8422
fix: resolve CI failures (mcp-server build, ESLint projectService)
tomymaritano Apr 23, 2026
ef6c615
fix: address all inline and duplicate PR review findings
tomymaritano Apr 23, 2026
549339d
Merge remote-tracking branch 'origin/main' into develop
tomymaritano Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@
"Bash(npx vitest:*)",
"Bash(do echo:*)",
"Bash(do gh:*)",
"Bash(git tag:*)"
"Bash(git tag:*)",
"Bash(git stash:*)",
"Bash(ls /Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/NoteListFilterBar*)",
"Bash(ls /Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/*.module.css)",
"Bash(npm view:*)",
"Bash(git log:*)"
]
}
}
21 changes: 12 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
# Set via `wrangler secret put <KEY>` for deployed environments.
# For local dev, create packages/api/.dev.vars with these values.

TURSO_DATABASE_URL=libsql://your-db.turso.io
TURSO_AUTH_TOKEN=your_token_here
JWT_SECRET=your_secret_here # openssl rand -base64 32
RESEND_API_KEY=re_your_key_here # Resend email service
TURSO_DATABASE_URL=
TURSO_AUTH_TOKEN=
# Generate with: openssl rand -base64 32
JWT_SECRET=
# Resend email service API key
RESEND_API_KEY=
SITE_URL=https://readied.app

# ─── Stripe ─────────────────────────────────────────────────
STRIPE_SECRET_KEY=sk_test_your_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_secret_here
STRIPE_PRICE_MONTHLY=price_your_monthly_id_here
STRIPE_PRICE_ANNUAL=price_your_annual_id_here
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_PRICE_MONTHLY=
STRIPE_PRICE_ANNUAL=

# ─── Admin ──────────────────────────────────────────────────
ADMIN_TOKEN=your_admin_token_here # Token for /admin endpoints
# Token for /admin endpoints
ADMIN_TOKEN=

# ─── GitHub Actions (repo secrets) ──────────────────────────
# GH_TOKEN # PAT with repo scope (releases, PRs)
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"@types/react-dom": "^18.2.25",
"@types/turndown": "^5.0.6",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^39.8.5",
"electron": "^35.7.5",
"electron-builder": "^26.0.12",
"electron-devtools-installer": "^4.0.0",
"electron-vite": "^2.1.0",
Expand Down
62 changes: 39 additions & 23 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,7 +1249,13 @@ function registerDataHandlers(): void {
ipcMain.handle(
'data:exportNote',
async (_event: Electron.IpcMainInvokeEvent, content: string, suggestedName: string) => {
const safeName = suggestedName.replace(/[^a-zA-Z0-9\s-]/g, '').substring(0, 80) || 'note';
const safeName =
suggestedName
.normalize('NFC')
// eslint-disable-next-line no-control-regex
.replace(/[/\\:*?"<>|\x00-\x1f.]/g, '')
.substring(0, 80)
.trim() || 'note';
const { filePath, canceled } = await dialog.showSaveDialog({
title: 'Export Note',
defaultPath: join(app.getPath('documents'), `${safeName}.md`),
Expand All @@ -1262,7 +1268,7 @@ function registerDataHandlers(): void {
}

try {
writeFileSync(filePath, content, 'utf-8');
await writeFile(filePath, content, 'utf-8');
return { success: true, path: filePath };
} catch (error) {
return {
Expand Down Expand Up @@ -2298,13 +2304,17 @@ function registerPluginDiscoveryHandlers(): void {
const archivePath = filePaths[0];
const fileName = basename(archivePath).toLowerCase();

// Hoist tmpDir so it can be cleaned up in finally
let tmpDir: string | null = null;

try {
// Ensure plugins dir exists
await mkdir(paths.plugins, { recursive: true });

// Extract to a temp dir first, then move validated plugin folder
const tmpDir = join(paths.plugins, `__installing_${Date.now()}`);
await mkdir(tmpDir, { recursive: true });
tmpDir = join(paths.plugins, `__installing_${Date.now()}`);
const extractDir = tmpDir;
await mkdir(extractDir, { recursive: true });

await new Promise<void>((resolve, reject) => {
const cb = (error: Error | null) => {
Expand All @@ -2324,25 +2334,25 @@ function registerPluginDiscoveryHandlers(): void {
'-Path',
archivePath,
'-DestinationPath',
tmpDir,
extractDir,
],
cb
);
} else {
execFile('unzip', ['-o', archivePath, '-d', tmpDir], cb);
execFile('unzip', ['-o', archivePath, '-d', extractDir], cb);
}
} else {
execFile('tar', ['-xzf', archivePath, '-C', tmpDir], cb);
execFile('tar', ['-xzf', archivePath, '-C', extractDir], cb);
}
});

// Find the manifest.json — could be at root or one level deep
const entries = await readdir(tmpDir);
let pluginSourceDir = tmpDir;
const entries = await readdir(extractDir);
let pluginSourceDir = extractDir;

// If there's a single subdirectory, use that as the plugin root
if (entries.length === 1 && entries[0]) {
const candidatePath = join(tmpDir, entries[0]);
const candidatePath = join(extractDir, entries[0]);
const candidateStat = await stat(candidatePath);
if (candidateStat.isDirectory()) {
pluginSourceDir = candidatePath;
Expand All @@ -2352,20 +2362,17 @@ function registerPluginDiscoveryHandlers(): void {
// Validate: must have manifest.json
const manifestPath = join(pluginSourceDir, 'manifest.json');
if (!existsSync(manifestPath)) {
await rm(tmpDir, { recursive: true, force: true });
return { success: false, error: 'No manifest.json found in archive' };
}

const manifestRaw = await readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestRaw);
if (!manifest.id || !manifest.name) {
await rm(tmpDir, { recursive: true, force: true });
return { success: false, error: 'Invalid manifest: missing id or name' };
}

// Validate plugin ID - only allow alphanumeric, hyphens, underscores
if (!/^[a-zA-Z0-9_-]+$/.test(manifest.id)) {
await rm(tmpDir, { recursive: true, force: true });
return {
success: false,
error: 'Invalid plugin ID: must be alphanumeric with hyphens/underscores only',
Expand All @@ -2375,7 +2382,6 @@ function registerPluginDiscoveryHandlers(): void {
// Verify path doesn't escape plugins directory
const destDir = join(paths.plugins, manifest.id);
if (!normalize(destDir).startsWith(normalize(paths.plugins))) {
await rm(tmpDir, { recursive: true, force: true });
return { success: false, error: 'Invalid plugin ID: path traversal detected' };
}

Expand All @@ -2386,19 +2392,18 @@ function registerPluginDiscoveryHandlers(): void {

await rename(pluginSourceDir, destDir);

// Clean up temp dir (in case pluginSourceDir was a subdirectory)
if (pluginSourceDir !== tmpDir && existsSync(tmpDir)) {
await rm(tmpDir, { recursive: true, force: true });
}

return { success: true, pluginId: manifest.id, pluginName: manifest.name };
} catch (error) {
return { success: false, error: String(error) };
} finally {
if (tmpDir && existsSync(tmpDir)) {
await rm(tmpDir, { recursive: true, force: true }).catch(() => {});
}
}
});

// Install plugin from a remote URL (marketplace download)
ipcMain.handle('plugins:installFromUrl', async (_event, url: string, _pluginSlug: string) => {
ipcMain.handle('plugins:installFromUrl', async (_event, url: string, pluginSlug: string) => {
// Safety: only allow https URLs
if (!url.startsWith('https://')) {
return { success: false, error: 'Only HTTPS URLs are allowed' };
Expand Down Expand Up @@ -2429,9 +2434,9 @@ function registerPluginDiscoveryHandlers(): void {
return { success: false, error: 'Plugin archive exceeds maximum size of 50 MB' };
}

// Determine archive type from URL or content-type
const lowerUrl = url.toLowerCase();
const isZip = lowerUrl.endsWith('.zip') || lowerUrl.includes('.zip');
// Determine archive type from URL pathname
const urlPathname = new URL(url).pathname.toLowerCase();
const isZip = urlPathname.endsWith('.zip');
const archiveExt = isZip ? '.zip' : '.tar.gz';
const archivePath = join(tmpDir, `plugin${archiveExt}`);
await writeFile(archivePath, buffer);
Expand Down Expand Up @@ -2490,10 +2495,21 @@ function registerPluginDiscoveryHandlers(): void {

const manifestRaw = await readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestRaw);
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) {
return { success: false, error: 'Invalid manifest: not a JSON object' };
}
if (!manifest.id || !manifest.name) {
return { success: false, error: 'Invalid manifest: missing id or name' };
}

// Validate manifest.id matches the expected pluginSlug if provided
if (pluginSlug && pluginSlug.length > 0 && manifest.id !== pluginSlug) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove strict slug/id equality check in plugin install

This rejects downloads whenever manifest.id !== pluginSlug, but marketplace slug and plugin manifest id are not guaranteed to be the same identifier in this codebase (the UI already has slug/id reconciliation logic). As a result, valid marketplace bundles can be blocked from installation even though the existing id format and path traversal validations already protect filesystem safety.

Useful? React with 👍 / 👎.

return {
success: false,
error: `Manifest ID "${manifest.id}" does not match expected plugin "${pluginSlug}"`,
};
}

// Validate plugin ID - only allow alphanumeric, hyphens, underscores
if (!/^[a-zA-Z0-9_-]+$/.test(manifest.id)) {
return {
Expand Down
7 changes: 6 additions & 1 deletion apps/desktop/src/renderer/components/NoteEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,19 @@ export function NoteEditor({
}, [note?.id]);

useEffect(() => {
// Guard: if note switched, just sync the ref and bail out
if (trackedNoteIdRef.current !== note?.id) {
prevDirtyRef.current = isDirty;
return;
}
if (prevDirtyRef.current && !isDirty) {
// Transitioned from dirty to clean — save completed
setShowSaved(true);
if (savedTimerRef.current) clearTimeout(savedTimerRef.current);
savedTimerRef.current = setTimeout(() => setShowSaved(false), 1500);
}
prevDirtyRef.current = isDirty;
}, [isDirty]);
}, [isDirty, note?.id]);

// Cleanup saved timer on unmount
useEffect(() => {
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/renderer/components/NoteListFilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export function NoteListFilterBar({ sortBy, sortOrder, onSortChange }: NoteListF
.then(result => {
if (!cancelled) setTags(result);
})
.catch(() => {
// IPC call failed — leave tags empty
.catch((err: unknown) => {
console.error('Failed to load tags:', err);
});
return () => {
cancelled = true;
Expand Down Expand Up @@ -96,6 +96,7 @@ export function NoteListFilterBar({ sortBy, sortOrder, onSortChange }: NoteListF
key={opt.label}
type="button"
className={`${styles.pill} ${statusFilter === opt.value ? styles.pillActive : ''}`}
aria-pressed={statusFilter === opt.value}
onClick={() => handleStatusClick(opt.value)}
>
{opt.label}
Expand Down
35 changes: 28 additions & 7 deletions apps/desktop/src/renderer/components/UpdateBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type BannerState =
| { kind: 'available'; version: string }
| { kind: 'downloading'; version: string; percent: number }
| { kind: 'ready'; version: string }
| { kind: 'error'; version: string };
| { kind: 'error'; version: string; message?: string };

export function UpdateBanner() {
const [state, setState] = useState<BannerState>({ kind: 'hidden' });
Expand Down Expand Up @@ -45,11 +45,15 @@ export function UpdateBanner() {
);

cleanups.push(
window.readied.updates.onError(() => {
window.readied.updates.onError((err: { message: string }) => {
setState(prev =>
prev.kind === 'hidden'
? prev
: { kind: 'error', version: (prev as { version: string }).version }
: {
kind: 'error',
version: (prev as { version: string }).version,
message: err?.message,
}
);
setDismissed(false);
})
Expand All @@ -60,12 +64,27 @@ export function UpdateBanner() {

const handleDownload = useCallback(async () => {
try {
await window.readied.updates.startDownload();
} catch {
const result = await window.readied.updates.startDownload();
if (!result.ok) {
setState(prev =>
prev.kind === 'hidden'
? prev
: {
kind: 'error',
version: (prev as { version: string }).version,
message: 'Download failed',
}
);
}
} catch (err) {
setState(prev =>
prev.kind === 'hidden'
? prev
: { kind: 'error', version: (prev as { version: string }).version }
: {
kind: 'error',
version: (prev as { version: string }).version,
message: err instanceof Error ? err.message : undefined,
}
);
}
}, []);
Expand Down Expand Up @@ -93,7 +112,9 @@ export function UpdateBanner() {
)}
{state.kind === 'error' && (
<>
<span className={styles.text}>Download failed</span>
<span className={styles.text}>
Download failed{state.message ? `: ${state.message}` : ''}
</span>
<button className={styles.action} onClick={handleDownload}>
Retry
</button>
Expand Down
10 changes: 8 additions & 2 deletions apps/desktop/src/renderer/components/Welcome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* or skip straight into the app.
*/

import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import { Button } from '../ui/primitives';
import styles from './Welcome.module.css';

Expand All @@ -30,6 +30,12 @@ const features = [
] as const;

export function Welcome({ onComplete }: WelcomeProps) {
const primaryRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
primaryRef.current?.focus();
}, []);

const handleEscape = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
Expand Down Expand Up @@ -68,7 +74,7 @@ export function Welcome({ onComplete }: WelcomeProps) {
</div>

<div className={styles.actions}>
<Button variant="primary" onClick={() => onComplete(true)}>
<Button ref={primaryRef} variant="primary" onClick={() => onComplete(true)}>
Create Your First Note
</Button>
<Button variant="ghost" onClick={() => onComplete(false)}>
Expand Down
Loading
Loading