feat(packaging): add electron-builder NSIS installer + Windows autostart

- electron-builder 26.8.1 (devDep, exact pin) with NSIS x64 target
- moved electron 41.3.0 to devDependencies (electron-builder requirement)
- new scripts: dist, dist:dir, predist runs rebuild:electron + build
- main: detect --hidden arg, skip inbox window on hidden launch
- main: first-run autostart enable on packaged Windows (.autostart-init flag)
- tray: 'Windows 시작 시 자동 실행' checkbox (packaged only)
- README: packaging section + Dev Mode requirement

Build verified: dist/Inkling Setup 0.2.0.exe (100MB), dist/win-unpacked/
runs better-sqlite3 native module from app.asar.unpacked.

Note: requires Windows Developer Mode ON (winCodeSign cache extraction
contains darwin symlinks that need SeCreateSymbolicLinkPrivilege).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
altair823
2026-04-25 23:45:56 +09:00
parent be24458450
commit 62a13ebf9f
5 changed files with 3163 additions and 16 deletions

View File

@@ -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

3064
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
"name": "inkling",
"version": "0.2.0",
"private": true,
"description": "Inkling — local-first 기억 구출 도구",
"author": "altair823 <dlsrks0734@gmail.com>",
"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",

View File

@@ -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';
@@ -23,11 +25,28 @@ import {
import { createTray } from './tray.js';
import { MediaGc } from './services/MediaGc.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,7 +92,9 @@ app.whenReady().then(async () => {
});
if (!reg.ok) logger.warn('hotkey.register.failed', { reason: reg.reason });
createInboxWindow();
if (!startedHidden) {
createInboxWindow();
}
createQuickCaptureWindow();
createTray(
() => createInboxWindow(),

View File

@@ -1,20 +1,39 @@
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;
function buildMenu(showInbox: () => void, showCapture: () => void) {
const items: MenuItemConstructorOptions[] = [
{ label: '구출한 메모 보기', click: showInbox },
{ label: '기억 구출하기', click: showCapture },
{ type: 'separator' }
];
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' });
}
items.push({ label: '종료', click: () => { app.isQuitting = true; app.quit(); } });
return Menu.buildFromTemplate(items);
}
export function createTray(showInbox: () => void, showCapture: () => void): TrayType {
const icon = nativeImage.createEmpty();
tray = new Tray(icon);
tray.setToolTip('Inkling');
const menu = Menu.buildFromTemplate([
{ label: '구출한 메모 보기', click: showInbox },
{ label: '기억 구출하기', click: showCapture },
{ type: 'separator' },
{ label: '종료', click: () => { app.isQuitting = true; app.quit(); } }
]);
tray.setContextMenu(menu);
tray.setContextMenu(buildMenu(showInbox, showCapture));
tray.on('click', showInbox);
return tray;
}