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:
21
README.md
21
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
|
||||
|
||||
3064
package-lock.json
generated
3064
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user