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.
- package/.dockerignore +33 -0
- package/.nvmrc +1 -0
- package/ARCHITECTURE.md +394 -0
- package/Dockerfile +50 -0
- package/LICENSE +29 -0
- package/README.md +206 -0
- package/bin/i18n.js +46 -0
- package/bin/ideaco.js +494 -0
- package/deploy.sh +15 -0
- package/docker-compose.yml +30 -0
- package/electron/main.cjs +986 -0
- package/electron/preload.cjs +14 -0
- package/electron/web-backends.cjs +854 -0
- package/jsconfig.json +8 -0
- package/next.config.mjs +34 -0
- package/package.json +134 -0
- package/postcss.config.mjs +6 -0
- package/public/demo/dashboard.png +0 -0
- package/public/demo/employee.png +0 -0
- package/public/demo/messages.png +0 -0
- package/public/demo/office.png +0 -0
- package/public/demo/requirement.png +0 -0
- package/public/logo.jpeg +0 -0
- package/public/logo.png +0 -0
- package/scripts/prepare-electron.js +67 -0
- package/scripts/release.js +76 -0
- package/src/app/api/agents/[agentId]/chat/route.js +70 -0
- package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
- package/src/app/api/agents/[agentId]/route.js +106 -0
- package/src/app/api/avatar/route.js +104 -0
- package/src/app/api/browse-dir/route.js +44 -0
- package/src/app/api/chat/route.js +265 -0
- package/src/app/api/company/factory-reset/route.js +43 -0
- package/src/app/api/company/route.js +82 -0
- package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
- package/src/app/api/departments/route.js +92 -0
- package/src/app/api/group-chat-loop/events/route.js +70 -0
- package/src/app/api/group-chat-loop/route.js +94 -0
- package/src/app/api/mailbox/route.js +100 -0
- package/src/app/api/messages/route.js +14 -0
- package/src/app/api/providers/[id]/configure/route.js +21 -0
- package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
- package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
- package/src/app/api/providers/route.js +11 -0
- package/src/app/api/requirements/route.js +242 -0
- package/src/app/api/secretary/route.js +65 -0
- package/src/app/api/system/cli-backends/route.js +91 -0
- package/src/app/api/system/cron/route.js +110 -0
- package/src/app/api/system/knowledge/route.js +104 -0
- package/src/app/api/system/plugins/route.js +40 -0
- package/src/app/api/system/skills/route.js +46 -0
- package/src/app/api/system/status/route.js +46 -0
- package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
- package/src/app/api/talent-market/[profileId]/route.js +17 -0
- package/src/app/api/talent-market/route.js +26 -0
- package/src/app/api/teams/route.js +773 -0
- package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
- package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
- package/src/app/globals.css +130 -0
- package/src/app/layout.jsx +40 -0
- package/src/app/page.jsx +97 -0
- package/src/components/AgentChatModal.jsx +164 -0
- package/src/components/AgentDetailModal.jsx +425 -0
- package/src/components/AgentSpyModal.jsx +481 -0
- package/src/components/AvatarGrid.jsx +29 -0
- package/src/components/BossProfileModal.jsx +162 -0
- package/src/components/CachedAvatar.jsx +77 -0
- package/src/components/ChatPanel.jsx +219 -0
- package/src/components/ChatShared.jsx +255 -0
- package/src/components/DepartmentDetail.jsx +842 -0
- package/src/components/DepartmentView.jsx +367 -0
- package/src/components/FileReference.jsx +260 -0
- package/src/components/FilesView.jsx +465 -0
- package/src/components/GroupChatView.jsx +799 -0
- package/src/components/Mailbox.jsx +926 -0
- package/src/components/MessagesView.jsx +112 -0
- package/src/components/OnboardingGuide.jsx +209 -0
- package/src/components/OrgTree.jsx +151 -0
- package/src/components/Overview.jsx +391 -0
- package/src/components/PixelOffice.jsx +2281 -0
- package/src/components/ProviderGrid.jsx +551 -0
- package/src/components/ProvidersBoard.jsx +16 -0
- package/src/components/RequirementDetail.jsx +1279 -0
- package/src/components/RequirementsBoard.jsx +187 -0
- package/src/components/SecretarySettings.jsx +295 -0
- package/src/components/SetupWizard.jsx +388 -0
- package/src/components/Sidebar.jsx +169 -0
- package/src/components/SystemMonitor.jsx +808 -0
- package/src/components/TalentMarket.jsx +183 -0
- package/src/components/TeamDetail.jsx +697 -0
- package/src/core/agent/base-agent.js +104 -0
- package/src/core/agent/chat-store.js +602 -0
- package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
- package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
- package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
- package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
- package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
- package/src/core/agent/cli-agent/backends/index.js +27 -0
- package/src/core/agent/cli-agent/backends/registry.js +580 -0
- package/src/core/agent/cli-agent/index.js +154 -0
- package/src/core/agent/index.js +60 -0
- package/src/core/agent/llm-agent/client.js +320 -0
- package/src/core/agent/llm-agent/index.js +97 -0
- package/src/core/agent/message-bus.js +211 -0
- package/src/core/agent/session.js +608 -0
- package/src/core/agent/tools.js +596 -0
- package/src/core/agent/web-agent/backends/base-backend.js +180 -0
- package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
- package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
- package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
- package/src/core/agent/web-agent/backends/index.js +91 -0
- package/src/core/agent/web-agent/index.js +278 -0
- package/src/core/agent/web-agent/web-client.js +407 -0
- package/src/core/employee/base-employee.js +1088 -0
- package/src/core/employee/index.js +35 -0
- package/src/core/employee/knowledge.js +327 -0
- package/src/core/employee/lifecycle.js +990 -0
- package/src/core/employee/memory/index.js +642 -0
- package/src/core/employee/memory/store.js +143 -0
- package/src/core/employee/performance.js +224 -0
- package/src/core/employee/secretary.js +625 -0
- package/src/core/employee/skills.js +398 -0
- package/src/core/index.js +38 -0
- package/src/core/organization/company.js +2600 -0
- package/src/core/organization/department.js +737 -0
- package/src/core/organization/group-chat-loop.js +264 -0
- package/src/core/organization/index.js +8 -0
- package/src/core/organization/persistence.js +111 -0
- package/src/core/organization/team.js +267 -0
- package/src/core/organization/workforce/hr.js +377 -0
- package/src/core/organization/workforce/providers.js +468 -0
- package/src/core/organization/workforce/role-archetypes.js +805 -0
- package/src/core/organization/workforce/talent-market.js +205 -0
- package/src/core/prompts.js +532 -0
- package/src/core/requirement.js +1789 -0
- package/src/core/system/audit.js +483 -0
- package/src/core/system/cron.js +449 -0
- package/src/core/system/index.js +7 -0
- package/src/core/system/plugin.js +2183 -0
- package/src/core/utils/json-parse.js +188 -0
- package/src/core/workspace.js +239 -0
- package/src/lib/api-i18n.js +211 -0
- package/src/lib/avatar.js +268 -0
- package/src/lib/client-store.js +1025 -0
- package/src/lib/config-validator.js +483 -0
- package/src/lib/format-time.js +22 -0
- package/src/lib/hooks.js +414 -0
- package/src/lib/i18n.js +134 -0
- package/src/lib/paths.js +23 -0
- package/src/lib/store.js +72 -0
- package/src/locales/de.js +393 -0
- package/src/locales/en.js +1054 -0
- package/src/locales/es.js +393 -0
- package/src/locales/fr.js +393 -0
- package/src/locales/ja.js +501 -0
- package/src/locales/ko.js +513 -0
- package/src/locales/zh.js +828 -0
- 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
|