idea-manager 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/build-manifest.json +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +2 -2
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +2 -2
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/advisor-actions/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/archive/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/filesystem/tree/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/global-advisor/route.js +1 -1
- package/.next/server/app/api/global-advisor/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/global-memo/route.js +2 -2
- package/.next/server/app/api/global-memo/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/maintenance/route.js +2 -2
- package/.next/server/app/api/maintenance/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/advisor/route.js +1 -1
- package/.next/server/app/api/projects/[id]/advisor/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/apply-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/auto-distribute/route.js +2 -2
- package/.next/server/app/api/projects/[id]/auto-distribute/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/brainstorm/route.js +2 -2
- package/.next/server/app/api/projects/[id]/brainstorm/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/git-sync/route.js +2 -2
- package/.next/server/app/api/projects/[id]/git-sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/route.js +2 -2
- package/.next/server/app/api/projects/[id]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route.js +2 -2
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/chat/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route.js +2 -2
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/prompt/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/refine/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/[taskId]/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route.js +2 -2
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/reorder/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/[subId]/tasks/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/[id]/sub-projects/route.js +2 -2
- package/.next/server/app/api/projects/[id]/sub-projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/projects/route.js +2 -2
- package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/search/route.js +2 -2
- package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/sync/route.js +2 -2
- package/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/tasks/[taskId]/move/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
- package/.next/server/app/index.html +2 -2
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +10 -10
- package/.next/server/chunks/697.js +2 -2
- package/.next/server/pages/404.html +2 -2
- package/.next/server/pages/500.html +2 -2
- package/.next/static/chunks/app/{page-16b10bcc10c22811.js → page-835e6af14011d7a8.js} +9 -9
- package/bin/postinstall.js +27 -7
- package/package.json +1 -1
- package/src/cli.ts +46 -13
- package/src/components/dashboard/DashboardPanel.tsx +13 -0
- package/src/components/memo/GlobalMemoLayer.tsx +15 -0
- package/src/components/ui/FileTreeDrawer.tsx +1 -1
- package/src/lib/ai/client.ts +21 -7
- package/src/lib/auto-update.ts +22 -3
- package/src/lib/db/index.ts +29 -5
- package/src/lib/scheduler.ts +9 -3
- package/src/lib/sync/git.ts +17 -14
- package/src/lib/watcher.ts +4 -4
- /package/.next/static/{E6_zErdSfG_2YXZzHtQ34 → 8qtkIvEkJ-lmfHkJP6hlo}/_buildManifest.js +0 -0
- /package/.next/static/{E6_zErdSfG_2YXZzHtQ34 → 8qtkIvEkJ-lmfHkJP6hlo}/_ssgManifest.js +0 -0
package/bin/postinstall.js
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const isWindows = process.platform === 'win32';
|
|
4
7
|
|
|
5
8
|
try {
|
|
6
|
-
|
|
9
|
+
// Check if `im` is on PATH. Windows uses `where`, POSIX uses `command -v`.
|
|
10
|
+
execSync(isWindows ? 'where im' : 'command -v im', { stdio: 'ignore', shell: true });
|
|
7
11
|
} catch {
|
|
8
|
-
|
|
12
|
+
let npmPrefix = '';
|
|
13
|
+
try {
|
|
14
|
+
npmPrefix = execSync('npm prefix -g', { encoding: 'utf-8' }).trim();
|
|
15
|
+
} catch { /* fall through */ }
|
|
16
|
+
|
|
17
|
+
console.log('\n \u2713 idea-manager installed!\n');
|
|
18
|
+
console.log(' \u26A0 "im" command not found in PATH.\n');
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
if (isWindows) {
|
|
21
|
+
// On Windows, npm global prefix is the directory holding the .cmd wrappers (no "bin" subdir).
|
|
22
|
+
const dir = npmPrefix || '%APPDATA%\\npm';
|
|
23
|
+
console.log(` Add this directory to your PATH:\n`);
|
|
24
|
+
console.log(` ${dir}\n`);
|
|
25
|
+
console.log(' PowerShell (User PATH):');
|
|
26
|
+
console.log(` [Environment]::SetEnvironmentVariable('Path', "$env:Path;${dir}", 'User')\n`);
|
|
27
|
+
console.log(' Or via System Properties > Environment Variables.');
|
|
28
|
+
console.log(' Open a new terminal and run: im start\n');
|
|
29
|
+
} else {
|
|
30
|
+
const binDir = npmPrefix ? path.join(npmPrefix, 'bin') : '$(npm prefix -g)/bin';
|
|
31
|
+
console.log(' Add to your shell profile (~/.zshrc or ~/.bashrc):\n');
|
|
32
|
+
console.log(` export PATH="${binDir}:$PATH"\n`);
|
|
33
|
+
console.log(' Then open a new terminal and run: im start\n');
|
|
34
|
+
}
|
|
15
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idea-manager",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "AI-powered idea & task manager for developers. Brainstorm → structured tasks → AI agent execution. CodeMirror editor with slash commands, MCP Server, Claude/Gemini/Codex support. Local-first SQLite, cross-PC Git sync.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"idea-manager",
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Force UTF-8 output on Windows so Korean strings don't garble in cp949 terminals.
|
|
4
|
+
if (process.platform === 'win32') {
|
|
5
|
+
try {
|
|
6
|
+
process.stdout.setDefaultEncoding?.('utf8');
|
|
7
|
+
process.stderr.setDefaultEncoding?.('utf8');
|
|
8
|
+
} catch { /* non-critical */ }
|
|
9
|
+
}
|
|
10
|
+
|
|
3
11
|
import { Command } from 'commander';
|
|
4
12
|
import { ensureDb } from './lib/db';
|
|
5
13
|
import { startMcpServer } from './lib/mcp/server';
|
|
@@ -12,6 +20,7 @@ import { startWatcher } from './lib/watcher';
|
|
|
12
20
|
import { syncInit, syncPush, syncPull, syncStatus } from './lib/sync/index';
|
|
13
21
|
import { maybeAutoUpdate, respawnSelf } from './lib/auto-update';
|
|
14
22
|
import { spawn } from 'child_process';
|
|
23
|
+
import { readFileSync } from 'fs';
|
|
15
24
|
import path from 'path';
|
|
16
25
|
import { fileURLToPath } from 'url';
|
|
17
26
|
|
|
@@ -41,6 +50,9 @@ async function openAsApp(url: string) {
|
|
|
41
50
|
]
|
|
42
51
|
: platform === 'win32'
|
|
43
52
|
? [
|
|
53
|
+
{ bin: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', args: [`--app=${url}`] },
|
|
54
|
+
{ bin: 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', args: [`--app=${url}`] },
|
|
55
|
+
{ bin: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', args: [`--app=${url}`] },
|
|
44
56
|
{ bin: 'chrome', args: [`--app=${url}`] },
|
|
45
57
|
{ bin: 'msedge', args: [`--app=${url}`] },
|
|
46
58
|
]
|
|
@@ -51,7 +63,9 @@ async function openAsApp(url: string) {
|
|
|
51
63
|
];
|
|
52
64
|
|
|
53
65
|
for (const browser of browsers) {
|
|
54
|
-
|
|
66
|
+
// Skip absolute paths that don't exist (macOS .app, Windows Program Files)
|
|
67
|
+
const isAbsolute = path.isAbsolute(browser.bin);
|
|
68
|
+
if (isAbsolute && !fs.existsSync(browser.bin)) continue;
|
|
55
69
|
|
|
56
70
|
try {
|
|
57
71
|
const child = spawnChild(browser.bin, browser.args, {
|
|
@@ -73,10 +87,17 @@ async function openAsApp(url: string) {
|
|
|
73
87
|
|
|
74
88
|
const program = new Command();
|
|
75
89
|
|
|
90
|
+
function readPkgVersion(): string {
|
|
91
|
+
try {
|
|
92
|
+
const pkg = JSON.parse(readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')) as { version?: string };
|
|
93
|
+
return pkg.version ?? '0.0.0';
|
|
94
|
+
} catch { return '0.0.0'; }
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
program
|
|
77
98
|
.name('im')
|
|
78
|
-
.description('Idea Manager
|
|
79
|
-
.version(
|
|
99
|
+
.description('Idea Manager - Brainstorming to structured tasks with prompts')
|
|
100
|
+
.version(readPkgVersion());
|
|
80
101
|
|
|
81
102
|
program
|
|
82
103
|
.command('mcp')
|
|
@@ -135,23 +156,35 @@ program
|
|
|
135
156
|
} catch {
|
|
136
157
|
nextCli = path.join(PKG_ROOT, 'node_modules', 'next', 'dist', 'bin', 'next');
|
|
137
158
|
}
|
|
159
|
+
if (!fs.existsSync(nextCli)) {
|
|
160
|
+
console.error('\n ⚠ Next.js 바이너리를 찾을 수 없습니다. 의존성이 손상된 듯합니다.');
|
|
161
|
+
console.error(` 재설치: npm install -g idea-manager@latest\n`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
138
164
|
|
|
139
|
-
// Build if not already built
|
|
140
|
-
const
|
|
141
|
-
if (!fs.existsSync(
|
|
165
|
+
// Build if not already built (check BUILD_ID, not just .next dir existence)
|
|
166
|
+
const buildMarker = path.join(PKG_ROOT, '.next', 'BUILD_ID');
|
|
167
|
+
if (!fs.existsSync(buildMarker)) {
|
|
142
168
|
console.log('\n IM - First run: building... (this may take a minute)\n');
|
|
143
|
-
const buildResult = spawn(process.execPath, [nextCli, 'build'], {
|
|
169
|
+
const buildResult = spawn(process.execPath, [nextCli, 'build', '--webpack'], {
|
|
144
170
|
cwd: PKG_ROOT,
|
|
145
171
|
stdio: 'inherit',
|
|
146
172
|
env: { ...process.env, NODE_ENV: 'production' },
|
|
147
173
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
try {
|
|
175
|
+
await new Promise<void>((resolve, reject) => {
|
|
176
|
+
buildResult.on('exit', (code) => {
|
|
177
|
+
if (code !== 0) reject(new Error(`Build failed with code ${code}`));
|
|
178
|
+
else resolve();
|
|
179
|
+
});
|
|
180
|
+
buildResult.on('error', reject);
|
|
152
181
|
});
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error(`\n ⚠ 빌드 실패: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
console.error(` 글로벌 설치 시 devDependencies가 빠졌을 수 있습니다.`);
|
|
185
|
+
console.error(` 시도: cd "${PKG_ROOT}" && npm install\n`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
155
188
|
}
|
|
156
189
|
|
|
157
190
|
console.log(`\n IM - Idea Manager`);
|
|
@@ -96,6 +96,17 @@ export default function DashboardPanel() {
|
|
|
96
96
|
if (savedTab) setTab(savedTab);
|
|
97
97
|
}, [fetchData]);
|
|
98
98
|
|
|
99
|
+
// Sync memo when another instance (⌘M layer) saves
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const onSync = () => {
|
|
102
|
+
fetch('/api/global-memo').then(r => r.ok ? r.json() : null)
|
|
103
|
+
.then(d => { if (d?.content !== undefined) setMemoContent(d.content); })
|
|
104
|
+
.catch(() => {});
|
|
105
|
+
};
|
|
106
|
+
window.addEventListener('global-memo-updated', onSync);
|
|
107
|
+
return () => window.removeEventListener('global-memo-updated', onSync);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
99
110
|
const handleMemoChange = (value: string) => {
|
|
100
111
|
setMemoContent(value);
|
|
101
112
|
if (memoSaveTimer.current) clearTimeout(memoSaveTimer.current);
|
|
@@ -104,6 +115,8 @@ export default function DashboardPanel() {
|
|
|
104
115
|
method: 'PUT',
|
|
105
116
|
headers: { 'Content-Type': 'application/json' },
|
|
106
117
|
body: JSON.stringify({ content: value }),
|
|
118
|
+
}).then(() => {
|
|
119
|
+
window.dispatchEvent(new Event('global-memo-updated'));
|
|
107
120
|
});
|
|
108
121
|
}, 500);
|
|
109
122
|
};
|
|
@@ -34,10 +34,25 @@ export default function GlobalMemoLayer() {
|
|
|
34
34
|
method: 'PUT',
|
|
35
35
|
headers: { 'Content-Type': 'application/json' },
|
|
36
36
|
body: JSON.stringify({ content: value }),
|
|
37
|
+
}).then(() => {
|
|
38
|
+
window.dispatchEvent(new Event('global-memo-updated'));
|
|
37
39
|
}).catch(() => {});
|
|
38
40
|
}, 600);
|
|
39
41
|
}, []);
|
|
40
42
|
|
|
43
|
+
// Sync when other memo instance saves
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const onSync = () => {
|
|
46
|
+
if (!open) return;
|
|
47
|
+
fetch('/api/global-memo')
|
|
48
|
+
.then(r => r.ok ? r.json() : null)
|
|
49
|
+
.then(d => { if (d?.content !== undefined) setContent(d.content); })
|
|
50
|
+
.catch(() => {});
|
|
51
|
+
};
|
|
52
|
+
window.addEventListener('global-memo-updated', onSync);
|
|
53
|
+
return () => window.removeEventListener('global-memo-updated', onSync);
|
|
54
|
+
}, [open]);
|
|
55
|
+
|
|
41
56
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
42
57
|
const v = e.target.value;
|
|
43
58
|
setContent(v);
|
package/src/lib/ai/client.ts
CHANGED
|
@@ -37,6 +37,7 @@ export function runAgent(
|
|
|
37
37
|
cwd: options?.cwd || process.cwd(),
|
|
38
38
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
39
39
|
shell: process.platform === 'win32',
|
|
40
|
+
windowsHide: true,
|
|
40
41
|
env,
|
|
41
42
|
});
|
|
42
43
|
|
|
@@ -50,9 +51,26 @@ export function runAgent(
|
|
|
50
51
|
}, options.timeoutMs);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
-
proc.
|
|
55
|
-
|
|
54
|
+
// Spawn failure (e.g., binary not found) → reject instead of hanging.
|
|
55
|
+
proc.on('error', (err) => {
|
|
56
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
57
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
58
|
+
if (code === 'ENOENT') {
|
|
59
|
+
reject(new Error(`${config.name} CLI not found on PATH. Install it first.`));
|
|
60
|
+
} else {
|
|
61
|
+
reject(new Error(`${config.name} CLI error: ${err.message}`));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Write prompt to stdin and close it. Wrap in try/catch for broken pipe cases.
|
|
66
|
+
try {
|
|
67
|
+
proc.stdin?.write(prompt, 'utf8');
|
|
68
|
+
proc.stdin?.end();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
71
|
+
reject(new Error(`Failed to pipe prompt to ${config.name}: ${(err as Error).message}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
56
74
|
|
|
57
75
|
let buffer = '';
|
|
58
76
|
let resultText = '';
|
|
@@ -103,10 +121,6 @@ export function runAgent(
|
|
|
103
121
|
stderrText += chunk.toString();
|
|
104
122
|
});
|
|
105
123
|
|
|
106
|
-
proc.on('error', (err) => {
|
|
107
|
-
reject(new Error(`${config.name} CLI error: ${err.message}`));
|
|
108
|
-
});
|
|
109
|
-
|
|
110
124
|
proc.on('exit', (code, signal) => {
|
|
111
125
|
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
112
126
|
if (!useStreamJson && config.cleanOutput) {
|
package/src/lib/auto-update.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
5
5
|
function readInstalledVersion(pkgRoot: string): string {
|
|
@@ -120,9 +120,28 @@ export async function maybeAutoUpdate(pkgRoot: string): Promise<AutoUpdateResult
|
|
|
120
120
|
* replaces the old copy loaded in the current Node process. Sets
|
|
121
121
|
* IM_NO_AUTO_UPDATE=1 on the child to prevent an update-respawn loop.
|
|
122
122
|
*/
|
|
123
|
+
function resolveImBin(): string | null {
|
|
124
|
+
// After `npm install -g`, the im wrapper lives in npm's global bin.
|
|
125
|
+
try {
|
|
126
|
+
const prefix = execSync('npm prefix -g', { encoding: 'utf-8' }).trim();
|
|
127
|
+
// On Windows the wrapper is `<prefix>\im.cmd`; on POSIX it's `<prefix>/bin/im`.
|
|
128
|
+
const candidates = process.platform === 'win32'
|
|
129
|
+
? [join(prefix, 'im.cmd'), join(prefix, 'im.ps1'), join(prefix, 'im')]
|
|
130
|
+
: [join(prefix, 'bin', 'im')];
|
|
131
|
+
for (const c of candidates) {
|
|
132
|
+
if (existsSync(c)) return c;
|
|
133
|
+
}
|
|
134
|
+
} catch { /* fall through */ }
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
123
138
|
export function respawnSelf(): void {
|
|
124
139
|
const args = process.argv.slice(2);
|
|
125
|
-
const
|
|
140
|
+
const binPath = resolveImBin();
|
|
141
|
+
const cmd = binPath ?? 'im';
|
|
142
|
+
// On Windows, .cmd wrappers require shell:true (cmd.exe interprets them).
|
|
143
|
+
// On POSIX, prefer shell:false with an absolute path for reliability.
|
|
144
|
+
const child = spawn(cmd, args, {
|
|
126
145
|
stdio: 'inherit',
|
|
127
146
|
shell: process.platform === 'win32',
|
|
128
147
|
env: { ...process.env, IM_NO_AUTO_UPDATE: '1' },
|
package/src/lib/db/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { getDbPath } from '../utils/paths';
|
|
3
4
|
import { initSchema } from './schema';
|
|
4
5
|
import { initScheduler } from '../scheduler';
|
|
@@ -20,7 +21,17 @@ class DatabaseWrapper {
|
|
|
20
21
|
private save() {
|
|
21
22
|
if (!this.dirty) return;
|
|
22
23
|
const data = this.db.export();
|
|
23
|
-
|
|
24
|
+
// Atomic write: write to temp file, then rename. Prevents corruption
|
|
25
|
+
// if the process dies mid-write.
|
|
26
|
+
const tmpPath = `${this.dbPath}.tmp-${process.pid}`;
|
|
27
|
+
try {
|
|
28
|
+
fs.writeFileSync(tmpPath, Buffer.from(data));
|
|
29
|
+
fs.renameSync(tmpPath, this.dbPath);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Best-effort cleanup of orphan tmp
|
|
32
|
+
try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
24
35
|
this.dirty = false;
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -144,12 +155,25 @@ async function initAsync(): Promise<DatabaseWrapper> {
|
|
|
144
155
|
|
|
145
156
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
146
157
|
const initSqlJs = require('sql.js/dist/sql-wasm.js');
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
158
|
+
// Explicitly locate the .wasm next to sql-wasm.js — otherwise sql.js falls
|
|
159
|
+
// back to `__dirname` which can be wrong when invoked from MCP stdio mode
|
|
160
|
+
// (cwd may differ from PKG_ROOT).
|
|
161
|
+
let wasmDir: string;
|
|
162
|
+
try {
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
164
|
+
wasmDir = path.dirname(require.resolve('sql.js/dist/sql-wasm.js'));
|
|
165
|
+
} catch {
|
|
166
|
+
wasmDir = '';
|
|
167
|
+
}
|
|
168
|
+
const SQL = await initSqlJs(wasmDir ? {
|
|
169
|
+
locateFile: (file: string) => path.join(wasmDir, file),
|
|
170
|
+
} : undefined);
|
|
151
171
|
|
|
152
172
|
const dbPath = getDbPath();
|
|
173
|
+
// Ensure parent directory exists (first-run safety on fresh installs).
|
|
174
|
+
const dbDir = path.dirname(dbPath);
|
|
175
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
176
|
+
|
|
153
177
|
let db;
|
|
154
178
|
if (fs.existsSync(dbPath)) {
|
|
155
179
|
const fileBuffer = fs.readFileSync(dbPath);
|
package/src/lib/scheduler.ts
CHANGED
|
@@ -10,7 +10,12 @@ function formatDate(d: Date): string {
|
|
|
10
10
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function
|
|
13
|
+
function sendNotification(title: string, message: string) {
|
|
14
|
+
// macOS native notification via osascript. Other platforms get a console log.
|
|
15
|
+
if (process.platform !== 'darwin') {
|
|
16
|
+
console.log(`[${title}] ${message}`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
14
19
|
const escaped = message.replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
15
20
|
const script = `display notification "${escaped}" with title "${title}"`;
|
|
16
21
|
exec(`osascript -e '${script}'`, (err) => {
|
|
@@ -60,7 +65,7 @@ async function checkMorningNotification() {
|
|
|
60
65
|
if (activeTasks.count > 0) lines.push(`진행 중: ${activeTasks.count}개`);
|
|
61
66
|
if (problemTasks.count > 0) lines.push(`문제: ${problemTasks.count}개`);
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
sendNotification('IM - 오늘의 할 일', lines.join('\n'));
|
|
64
69
|
lastNotifiedDate = today;
|
|
65
70
|
} catch (err) {
|
|
66
71
|
console.error('[Scheduler] error:', err);
|
|
@@ -71,8 +76,9 @@ export function initScheduler() {
|
|
|
71
76
|
if (initialized) return;
|
|
72
77
|
initialized = true;
|
|
73
78
|
|
|
74
|
-
// Check every minute
|
|
79
|
+
// Check every minute. unref() so it doesn't pin the event loop for MCP stdio mode.
|
|
75
80
|
timer = setInterval(checkMorningNotification, 60 * 1000);
|
|
81
|
+
timer.unref?.();
|
|
76
82
|
|
|
77
83
|
// Also check immediately on startup
|
|
78
84
|
checkMorningNotification();
|
package/src/lib/sync/git.ts
CHANGED
|
@@ -2,24 +2,26 @@ import { execFile } from 'child_process';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (!arg.includes(' ') && !arg.includes('"')) return arg;
|
|
8
|
-
if (process.platform === 'win32') {
|
|
9
|
-
// cmd.exe uses "" to escape quotes
|
|
10
|
-
return `"${arg.replace(/"/g, '""')}"`;
|
|
11
|
-
}
|
|
12
|
-
return `"${arg.replace(/"/g, '\\"')}"`;
|
|
13
|
-
}
|
|
5
|
+
// Longer timeout for network-heavy operations (clone/push/pull over slow links).
|
|
6
|
+
const NETWORK_CMDS = new Set(['clone', 'push', 'pull', 'fetch']);
|
|
14
7
|
|
|
15
8
|
function exec(cmd: string, args: string[], cwd?: string): Promise<string> {
|
|
16
9
|
return new Promise((resolve, reject) => {
|
|
17
|
-
const
|
|
18
|
-
|
|
10
|
+
const isNetwork = args.some(a => NETWORK_CMDS.has(a));
|
|
11
|
+
const timeout = isNetwork ? 300000 : 30000; // 5min / 30s
|
|
12
|
+
// shell: false — Node handles arg escaping natively on all platforms.
|
|
13
|
+
// On Windows, execFile still resolves `.cmd`/`.bat` via PATHEXT for known binaries.
|
|
14
|
+
execFile(cmd, args, {
|
|
15
|
+
cwd,
|
|
16
|
+
timeout,
|
|
17
|
+
shell: process.platform === 'win32',
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
20
|
+
}, (err, stdout, stderr) => {
|
|
19
21
|
if (err) {
|
|
20
|
-
reject(new Error(stderr?.trim() || err.message));
|
|
22
|
+
reject(new Error(stderr?.toString().trim() || err.message));
|
|
21
23
|
} else {
|
|
22
|
-
resolve(stdout.trim());
|
|
24
|
+
resolve(stdout.toString().trim());
|
|
23
25
|
}
|
|
24
26
|
});
|
|
25
27
|
});
|
|
@@ -43,7 +45,8 @@ export function isGitRepo(dir: string): boolean {
|
|
|
43
45
|
|
|
44
46
|
export async function ghCreateRepo(name: string): Promise<string> {
|
|
45
47
|
// Create private repo and get URL
|
|
46
|
-
|
|
48
|
+
// --confirm was removed in gh 2.x; repo is created non-interactively by default with name+flag
|
|
49
|
+
const result = await exec('gh', ['repo', 'create', name, '--private']);
|
|
47
50
|
// Extract repo URL from output
|
|
48
51
|
const urlMatch = result.match(/https:\/\/github\.com\/\S+/);
|
|
49
52
|
if (urlMatch) return urlMatch[0];
|
package/src/lib/watcher.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import { ensureDb } from './db';
|
|
2
|
+
import { ensureDb, getDb } from './db';
|
|
3
3
|
import { runAgent } from './ai/client';
|
|
4
4
|
import { listProjects, getProject } from './db/queries/projects';
|
|
5
5
|
import { getSubProject } from './db/queries/sub-projects';
|
|
@@ -101,7 +101,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
|
|
|
101
101
|
const content = `[진행 중]\n${accumulated}`;
|
|
102
102
|
if (progressMsgId) {
|
|
103
103
|
// Update existing progress message
|
|
104
|
-
|
|
104
|
+
// getDb imported at top
|
|
105
105
|
const db = getDb();
|
|
106
106
|
db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(content, progressMsgId);
|
|
107
107
|
} else {
|
|
@@ -135,7 +135,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
|
|
|
135
135
|
|
|
136
136
|
// Replace progress message with final result
|
|
137
137
|
if (progressMsgId) {
|
|
138
|
-
|
|
138
|
+
// getDb imported at top
|
|
139
139
|
const db = getDb();
|
|
140
140
|
db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(result || '(no output)', progressMsgId);
|
|
141
141
|
} else {
|
|
@@ -152,7 +152,7 @@ async function executeTask(task: ITask, project: IProject, options: WatcherOptio
|
|
|
152
152
|
|
|
153
153
|
// Replace progress message with error
|
|
154
154
|
if (progressMsgId) {
|
|
155
|
-
|
|
155
|
+
// getDb imported at top
|
|
156
156
|
const db = getDb();
|
|
157
157
|
db.prepare('UPDATE task_conversations SET content = ? WHERE id = ?').run(`[error] ${errorMsg}`, progressMsgId);
|
|
158
158
|
} else {
|
|
File without changes
|
|
File without changes
|