ideaco 1.1.5

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.
Files changed (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
package/bin/ideaco.js ADDED
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import net from 'net';
6
+ import { spawn, spawnSync } from 'child_process';
7
+ import { fileURLToPath } from 'url';
8
+ import chalk from 'chalk';
9
+ import { createCliT } from './i18n.js';
10
+ import { Jimp } from 'jimp';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const ROOT = path.resolve(__dirname, '..');
15
+ const PORT = Number(process.env.IDEACO_PORT || 9999);
16
+ const HOME_DIR = path.join(ROOT, '.ideaco');
17
+ const PID_FILE = path.join(HOME_DIR, 'server.pid');
18
+ const PORT_FILE = path.join(HOME_DIR, 'server.port');
19
+ const LOG_FILE = path.join(HOME_DIR, 'server.log');
20
+ const DATA_DIR = path.join(HOME_DIR, 'data');
21
+ const WORKSPACE_DIR = path.join(HOME_DIR, 'workspace');
22
+ const BANNER_FILE = path.join(DATA_DIR, 'banner.ans');
23
+ const t = createCliT();
24
+
25
+ const args = process.argv.slice(2);
26
+ const command = args[0] || 'help';
27
+
28
+ function ensureDirs() {
29
+ fs.mkdirSync(DATA_DIR, { recursive: true });
30
+ fs.mkdirSync(WORKSPACE_DIR, { recursive: true });
31
+ fs.mkdirSync(HOME_DIR, { recursive: true });
32
+ }
33
+
34
+ function isPidRunning(pid) {
35
+ if (!pid) return false;
36
+ try {
37
+ process.kill(pid, 0);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function readPid() {
45
+ if (!fs.existsSync(PID_FILE)) return null;
46
+ const raw = fs.readFileSync(PID_FILE, 'utf8').trim();
47
+ const pid = Number(raw);
48
+ return Number.isFinite(pid) ? pid : null;
49
+ }
50
+
51
+ function writePid(pid) {
52
+ fs.writeFileSync(PID_FILE, String(pid));
53
+ }
54
+
55
+ function clearPid() {
56
+ if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
57
+ }
58
+
59
+ function readPort() {
60
+ if (!fs.existsSync(PORT_FILE)) return null;
61
+ const raw = fs.readFileSync(PORT_FILE, 'utf8').trim();
62
+ const port = Number(raw);
63
+ return Number.isFinite(port) ? port : null;
64
+ }
65
+
66
+ function writePort(port) {
67
+ fs.writeFileSync(PORT_FILE, String(port));
68
+ }
69
+
70
+ function clearPort() {
71
+ if (fs.existsSync(PORT_FILE)) fs.unlinkSync(PORT_FILE);
72
+ }
73
+
74
+ function isPortOpen(port) {
75
+ return new Promise((resolve) => {
76
+ const client = net.createConnection({ port, host: '127.0.0.1' }, () => {
77
+ client.end();
78
+ resolve(true);
79
+ });
80
+ client.on('error', () => resolve(false));
81
+ });
82
+ }
83
+
84
+ function waitForPort(port, retries = 60) {
85
+ return new Promise((resolve, reject) => {
86
+ let attempts = 0;
87
+ const check = async () => {
88
+ attempts += 1;
89
+ const open = await isPortOpen(port);
90
+ if (open) return resolve();
91
+ if (attempts >= retries) return reject(new Error(t('cli.startTimeout')));
92
+ setTimeout(check, 500);
93
+ };
94
+ check();
95
+ });
96
+ }
97
+
98
+ function sleep(ms) {
99
+ return new Promise(resolve => setTimeout(resolve, ms));
100
+ }
101
+
102
+ async function getBannerContent(title) {
103
+ const bulbLines = [
104
+ ' ████████ ',
105
+ ' ██████████████████ ',
106
+ ' ████████████████████████ ',
107
+ ' ████████████████████████████ ',
108
+ ' ████████████████████████████████ ',
109
+ ' ██████████████████████████████████ ',
110
+ ' ██████████████████████████████████ ',
111
+ ' ██████████████████████████████████ ',
112
+ ' ██████████████████████████████████ ',
113
+ ' ████████████████████████████████ ',
114
+ ' ████████████████████████████ ',
115
+ ' ████████████████████████ ',
116
+ ' ░░░░░░░░░░░░░░░░ ',
117
+ ' ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ',
118
+ ' ░▒▒▒▒▒▒▒▒▒▒ ',
119
+ ' ░▒▒▒▒▒▒▒▒ ',
120
+ ];
121
+ const bulbColor = '#ffef99';
122
+ const baseColors = ['#a3a3a3', '#bdbdbd', '#d4d4d4', '#e5e5e5'];
123
+ const baseTint = '#c9c9c9';
124
+ const width = bulbLines[0].length;
125
+ const padCenter = (text) => {
126
+ const visible = text.length;
127
+ if (visible >= width) return text;
128
+ const padLeft = Math.floor((width - visible) / 2);
129
+ const padRight = width - visible - padLeft;
130
+ return `${' '.repeat(padLeft)}${text}${' '.repeat(padRight)}`;
131
+ };
132
+ const softenLeft = (line, softChar = '░', widthSoft = 3) => {
133
+ const chars = Array.from(line);
134
+ const first = chars.findIndex(ch => ch !== ' ');
135
+ if (first === -1) return line;
136
+ for (let i = first; i < Math.min(first + widthSoft, chars.length); i += 1) {
137
+ if (chars[i] !== ' ') chars[i] = softChar;
138
+ }
139
+ return chars.join('');
140
+ };
141
+
142
+ let content = '';
143
+ for (let i = 0; i < bulbLines.length; i += 1) {
144
+ const isBase = i >= bulbLines.length - 4;
145
+ const baseIndex = i - (bulbLines.length - 4);
146
+ const color = isBase ? (baseColors[baseIndex] || baseTint) : bulbColor;
147
+ const softened = softenLeft(bulbLines[i], isBase ? '░' : '░', isBase ? 4 : 3);
148
+ const line = isBase ? chalk.hex(color)(softened) : chalk.bold.hex(color)(softened);
149
+ content += line + '\n';
150
+ }
151
+ const brandLine = chalk.bold.hex('#22d3ee')(padCenter('IdeaCo Console'));
152
+ const separator = chalk.hex('#2dd4bf')('─'.repeat(width));
153
+ // Note: Title with port is dynamic, so we don't include it in static banner file if possible,
154
+ // but for consistency with previous behavior, let's just return the graphic part.
155
+ content += brandLine + '\n' + separator + '\n';
156
+ return content;
157
+ }
158
+
159
+ async function printBootBanner(port) {
160
+ const title = t('cli.startBoot', { port });
161
+ let banner = '';
162
+
163
+ // Try to generate dynamic banner from public/logo.png if exists
164
+ const dynamicLogoPath = path.join(ROOT, 'public', 'logo.png');
165
+ if (fs.existsSync(dynamicLogoPath)) {
166
+ const dynamicBanner = await generateBannerFromImage(dynamicLogoPath);
167
+ if (dynamicBanner) {
168
+ banner = dynamicBanner;
169
+ // Also update the cached file
170
+ try {
171
+ ensureDirs();
172
+ if (fs.existsSync(BANNER_FILE)) fs.unlinkSync(BANNER_FILE);
173
+ fs.writeFileSync(BANNER_FILE, dynamicBanner);
174
+ } catch (e) {}
175
+ }
176
+ }
177
+
178
+ // Fallback to cached banner or default
179
+ if (!banner) {
180
+ if (fs.existsSync(BANNER_FILE)) {
181
+ banner = fs.readFileSync(BANNER_FILE, 'utf8');
182
+ } else {
183
+ banner = await getBannerContent(title);
184
+ // Write default banner to file for user customization
185
+ try {
186
+ ensureDirs();
187
+ fs.writeFileSync(BANNER_FILE, banner);
188
+ } catch (e) {
189
+ // ignore
190
+ }
191
+ }
192
+ }
193
+
194
+ // Print banner
195
+ console.log(banner);
196
+
197
+ // Always print port info below banner
198
+ const width = 40; // Approx width of banner
199
+ const padCenter = (text) => {
200
+ // Strip ANSI codes for length calculation
201
+ const visible = text.replace(/\x1b\[[0-9;]*m/g, '').length;
202
+ if (visible >= width) return text;
203
+ const padLeft = Math.floor((width - visible) / 2);
204
+ const padRight = width - visible - padLeft;
205
+ return `${' '.repeat(padLeft)}${text}${' '.repeat(padRight)}`;
206
+ };
207
+ console.log(chalk.hex('#7dd3fc')(padCenter(title)));
208
+ }
209
+
210
+ async function generateBannerFromImage(absPath) {
211
+ try {
212
+ const image = await Jimp.read(absPath);
213
+
214
+ // Resize to reasonable width for terminal (e.g. 40 chars)
215
+ const targetWidth = 40;
216
+ // Jimp resize(w, h) - if h is undefined or auto, it maintains aspect ratio
217
+ image.resize({ w: targetWidth });
218
+
219
+ const width = image.width;
220
+ const height = image.height;
221
+
222
+ let ansi = '';
223
+
224
+ const intToRGBA = (i) => {
225
+ return {
226
+ r: (i >>> 24) & 0xff,
227
+ g: (i >>> 16) & 0xff,
228
+ b: (i >>> 8) & 0xff,
229
+ a: i & 0xff
230
+ };
231
+ };
232
+
233
+ // Use upper half block (▀) to combine two vertical pixels
234
+ for (let y = 0; y < height; y += 2) {
235
+ for (let x = 0; x < width; x++) {
236
+ const topColor = image.getPixelColor(x, y);
237
+ const botColor = (y + 1 < height) ? image.getPixelColor(x, y + 1) : 0x00000000;
238
+
239
+ const top = intToRGBA(topColor);
240
+ const bot = intToRGBA(botColor);
241
+
242
+ const isTopTrans = top.a < 128;
243
+ const isBotTrans = bot.a < 128;
244
+
245
+ // Reset styles first
246
+ // \x1b[0m resets everything.
247
+ // We need to set FG and BG color for the block.
248
+ // ▀ (upper half block) uses FG color for top half, BG color for bottom half.
249
+
250
+ if (isTopTrans && isBotTrans) {
251
+ ansi += '\x1b[0m ';
252
+ } else if (!isTopTrans && isBotTrans) {
253
+ // Top visible, bottom transparent -> use upper block with FG=top
254
+ ansi += `\x1b[38;2;${top.r};${top.g};${top.b}m\x1b[49m▀`;
255
+ } else if (isTopTrans && !isBotTrans) {
256
+ // Top transparent, bottom visible -> use lower block with FG=bottom?
257
+ // Or use upper block with FG=default (transparent?) and BG=bottom.
258
+ // \x1b[39m resets FG to default. \x1b[48;2;...m sets BG.
259
+ // ▀ with BG=bottom makes the bottom half colored. Top half takes FG (default).
260
+ // But default FG is usually white/gray, not transparent.
261
+ // Better use ▄ (lower half block) with FG=bottom.
262
+ ansi += `\x1b[38;2;${bot.r};${bot.g};${bot.b}m\x1b[49m▄`;
263
+ } else {
264
+ // Both visible -> ▀ with FG=top, BG=bottom
265
+ ansi += `\x1b[38;2;${top.r};${top.g};${top.b}m\x1b[48;2;${bot.r};${bot.g};${bot.b}m▀`;
266
+ }
267
+ }
268
+ ansi += '\x1b[0m\n';
269
+ }
270
+ return ansi;
271
+ } catch (err) {
272
+ console.error(chalk.red('Failed to process image:'), err);
273
+ return null;
274
+ }
275
+ }
276
+
277
+ async function runBanner(imagePath) {
278
+ if (!imagePath) {
279
+ console.log(chalk.red('Please provide an image path.'));
280
+ console.log(chalk.yellow('Usage: ideaco banner <path/to/image.png>'));
281
+ return;
282
+ }
283
+
284
+ const absPath = path.resolve(process.cwd(), imagePath);
285
+ if (!fs.existsSync(absPath)) {
286
+ console.log(chalk.red(`Image not found: ${absPath}`));
287
+ return;
288
+ }
289
+
290
+ console.log(chalk.cyan(`Processing image: ${absPath}...`));
291
+
292
+ const ansi = await generateBannerFromImage(absPath);
293
+ if (ansi) {
294
+ ensureDirs();
295
+ try {
296
+ if (fs.existsSync(BANNER_FILE)) fs.unlinkSync(BANNER_FILE);
297
+ } catch (e) {
298
+ // ignore
299
+ }
300
+ fs.writeFileSync(BANNER_FILE, ansi);
301
+ console.log(chalk.green('Banner updated successfully!'));
302
+ console.log(chalk.gray(`Saved to: ${BANNER_FILE}`));
303
+ console.log('Run `ideaco start` to see it.');
304
+ }
305
+ }
306
+
307
+ function findAvailablePort(startPort) {
308
+ return new Promise((resolve) => {
309
+ const server = net.createServer();
310
+ server.listen(startPort, '127.0.0.1', () => {
311
+ const port = server.address().port;
312
+ server.close(() => resolve(port));
313
+ });
314
+ server.on('error', () => {
315
+ resolve(findAvailablePort(startPort + 1));
316
+ });
317
+ });
318
+ }
319
+
320
+ function getNextBin() {
321
+ const binName = process.platform === 'win32' ? 'next.cmd' : 'next';
322
+ return path.join(ROOT, 'node_modules', '.bin', binName);
323
+ }
324
+
325
+ function getElectronBin() {
326
+ const binName = process.platform === 'win32' ? 'electron.cmd' : 'electron';
327
+ return path.join(ROOT, 'node_modules', '.bin', binName);
328
+ }
329
+
330
+ function ensureBuild() {
331
+ const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
332
+ const result = spawnSync(npmBin, ['run', 'build'], { cwd: ROOT, stdio: 'inherit' });
333
+ if (result.status !== 0) {
334
+ process.exit(result.status ?? 1);
335
+ }
336
+ }
337
+
338
+ function ensureDependencies() {
339
+ const nextPkg = path.join(ROOT, 'node_modules', 'next', 'package.json');
340
+ if (fs.existsSync(nextPkg)) return;
341
+ const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
342
+ console.log(t('cli.installDeps'));
343
+ const result = spawnSync(npmBin, ['install', '--omit=dev'], { cwd: ROOT, stdio: 'inherit' });
344
+ if (result.status !== 0) {
345
+ console.log(t('cli.installDepsFailed'));
346
+ process.exit(result.status ?? 1);
347
+ }
348
+ console.log(t('cli.installDepsDone'));
349
+ }
350
+
351
+ async function startServer() {
352
+ ensureDirs();
353
+ const existingPid = readPid();
354
+ if (existingPid && isPidRunning(existingPid)) {
355
+ console.log(chalk.yellow(t('cli.alreadyRunning', { pid: existingPid })));
356
+ console.log(chalk.cyan('Restarting service...'));
357
+ await stopServer();
358
+ // Allow some time for port release
359
+ await new Promise(r => setTimeout(r, 1000));
360
+ }
361
+ ensureDependencies();
362
+ ensureBuild();
363
+ const logFd = fs.openSync(LOG_FILE, 'a');
364
+ const nextBin = getNextBin();
365
+ const port = await findAvailablePort(PORT);
366
+ await printBootBanner(port);
367
+ const child = spawn(nextBin, ['start', '-p', String(port)], {
368
+ cwd: ROOT,
369
+ env: {
370
+ ...process.env,
371
+ PORT: String(port),
372
+ HOSTNAME: '127.0.0.1',
373
+ IDEACO_DATA_DIR: DATA_DIR,
374
+ IDEACO_WORKSPACE_DIR: WORKSPACE_DIR,
375
+ },
376
+ stdio: ['ignore', logFd, logFd],
377
+ detached: true,
378
+ });
379
+ writePid(child.pid);
380
+ writePort(port);
381
+ child.unref();
382
+ try {
383
+ await waitForPort(port);
384
+ console.log(t('cli.startSuccess', { pid: child.pid, url: `http://127.0.0.1:${port}` }));
385
+ } catch (err) {
386
+ try { process.kill(child.pid, 'SIGTERM'); } catch {}
387
+ clearPid();
388
+ clearPort();
389
+ console.log(t('cli.startFailed', { error: err.message }));
390
+ }
391
+ }
392
+
393
+ async function stopServer() {
394
+ const pid = readPid();
395
+ if (!pid || !isPidRunning(pid)) {
396
+ clearPid();
397
+ clearPort();
398
+ console.log(t('cli.notRunning'));
399
+ return;
400
+ }
401
+ try {
402
+ process.kill(pid, 'SIGTERM');
403
+ } catch {
404
+ clearPid();
405
+ clearPort();
406
+ console.log(t('cli.stopped'));
407
+ return;
408
+ }
409
+ const start = Date.now();
410
+ while (Date.now() - start < 5000) {
411
+ if (!isPidRunning(pid)) {
412
+ clearPid();
413
+ clearPort();
414
+ console.log(t('cli.stopped'));
415
+ return;
416
+ }
417
+ await new Promise(r => setTimeout(r, 200));
418
+ }
419
+ try { process.kill(pid, 'SIGKILL'); } catch {}
420
+ clearPid();
421
+ clearPort();
422
+ console.log(t('cli.stopped'));
423
+ }
424
+
425
+ async function ensureServerRunning() {
426
+ const pid = readPid();
427
+ const port = readPort() ?? PORT;
428
+ if (pid && isPidRunning(pid) && await isPortOpen(port)) return true;
429
+ await startServer();
430
+ const nextPort = readPort() ?? PORT;
431
+ return await isPortOpen(nextPort);
432
+ }
433
+
434
+ function openUrl(url) {
435
+ if (process.platform === 'darwin') {
436
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
437
+ return;
438
+ }
439
+ if (process.platform === 'win32') {
440
+ spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();
441
+ return;
442
+ }
443
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
444
+ }
445
+
446
+ async function openWeb() {
447
+ const ok = await ensureServerRunning();
448
+ if (!ok) {
449
+ console.log(t('cli.webUnavailable'));
450
+ return;
451
+ }
452
+ const port = readPort() ?? PORT;
453
+ const url = `http://127.0.0.1:${port}`;
454
+ openUrl(url);
455
+ console.log(t('cli.webOpened', { url }));
456
+ }
457
+
458
+ async function openElectron() {
459
+ ensureDependencies();
460
+ const electronBin = getElectronBin();
461
+ const child = spawn(electronBin, ['.'], {
462
+ cwd: ROOT,
463
+ stdio: 'inherit',
464
+ env: {
465
+ ...process.env,
466
+ NODE_ENV: 'production',
467
+ IDEACO_DISABLE_DEVTOOLS: '1',
468
+ },
469
+ });
470
+ child.on('exit', (code) => process.exit(code ?? 0));
471
+ }
472
+
473
+ function printHelp() {
474
+ console.log(`
475
+ ${t('cli.helpTitle')}
476
+ ${t('cli.helpStart')}
477
+ ${t('cli.helpStop')}
478
+ ${t('cli.helpUi')}
479
+ ${t('cli.helpBanner')}
480
+ ${t('cli.helpHelp')}
481
+ `);
482
+ }
483
+
484
+ async function main() {
485
+ if (command === 'start') return await startServer();
486
+ if (command === 'stop') return await stopServer();
487
+ if (command === 'web') return await openWeb();
488
+ if (command === 'banner') return await runBanner(args[1]);
489
+ if (command === 'ui') return await openElectron();
490
+ if (command === 'electron') return await openElectron();
491
+ return printHelp();
492
+ }
493
+
494
+ main();
package/deploy.sh ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ cd "$(dirname "$0")"
4
+
5
+ # Detect docker compose command
6
+ if docker compose version &>/dev/null; then
7
+ DC="docker compose"
8
+ elif command -v docker-compose &>/dev/null; then
9
+ DC="docker-compose"
10
+ else
11
+ echo "[✗] Docker Compose not found." >&2; exit 1
12
+ fi
13
+
14
+ $DC up -d --build
15
+ echo "[✓] Started at http://localhost:${PORT:-9999}"
@@ -0,0 +1,30 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ # Main application
5
+ app:
6
+ build:
7
+ context: .
8
+ dockerfile: Dockerfile
9
+ container_name: idea-unlimited
10
+ restart: unless-stopped
11
+ ports:
12
+ - "${PORT:-9999}:9999"
13
+ environment:
14
+ - NODE_ENV=production
15
+ volumes:
16
+ # Persist company data and workspace across restarts
17
+ - app-data:/app/data
18
+ - app-workspace:/app/workspace
19
+ healthcheck:
20
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/"]
21
+ interval: 30s
22
+ timeout: 10s
23
+ retries: 3
24
+ start_period: 20s
25
+
26
+ volumes:
27
+ app-data:
28
+ driver: local
29
+ app-workspace:
30
+ driver: local