thepopebot 1.2.74 → 1.2.75-beta.10
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/README.md +11 -0
- package/api/CLAUDE.md +2 -0
- package/api/index.js +95 -3
- package/bin/cli.js +35 -6
- package/bin/docker-build.js +5 -0
- package/bin/managed-paths.js +1 -1
- package/bin/sync.js +91 -2
- package/config/instrumentation.js +4 -0
- package/lib/ai/async-channel.js +51 -0
- package/lib/ai/index.js +154 -153
- package/lib/ai/tools.js +46 -30
- package/lib/chat/actions.js +32 -5
- package/lib/chat/components/chat-header.js +4 -0
- package/lib/chat/components/chat-header.jsx +4 -0
- package/lib/chat/components/chat-input.js +2 -2
- package/lib/chat/components/chat-input.jsx +2 -2
- package/lib/chat/components/settings-chat-page.js +42 -37
- package/lib/chat/components/settings-chat-page.jsx +42 -37
- package/lib/chat/components/settings-coding-agents-page.js +139 -1
- package/lib/chat/components/settings-coding-agents-page.jsx +160 -0
- package/lib/chat/components/settings-jobs-page.js +81 -12
- package/lib/chat/components/settings-jobs-page.jsx +114 -35
- package/lib/chat/components/settings-secrets-layout.js +1 -1
- package/lib/chat/components/settings-secrets-layout.jsx +1 -1
- package/lib/code/actions.js +26 -10
- package/lib/code/code-page.js +2 -1
- package/lib/code/code-page.jsx +2 -1
- package/lib/code/port-forwards.js +17 -3
- package/lib/code/terminal-view.js +6 -3
- package/lib/code/terminal-view.jsx +6 -3
- package/lib/config.js +4 -0
- package/lib/db/api-keys.js +39 -45
- package/lib/db/config.js +63 -4
- package/lib/maintenance.js +58 -0
- package/lib/oauth/helper.js +34 -0
- package/lib/tools/create-agent-job.js +0 -1
- package/lib/tools/docker.js +28 -7
- package/package.json +3 -2
- package/setup/setup-ssl.mjs +414 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
- package/templates/.gitignore.template +7 -0
- package/templates/CLAUDE.md.template +3 -4
- package/templates/README.md +1 -1
- package/templates/docker-compose.custom.yml +40 -58
- package/templates/docker-compose.yml +14 -17
- package/templates/docs/CLI.md +3 -3
- package/templates/docs/CONFIGURATION.md +31 -65
- package/templates/docs/GETTING_STARTED.md +1 -1
- package/templates/docs/SECURITY.md +3 -3
- package/templates/docs/SKILLS.md +2 -1
- package/templates/skills/agent-job-secrets/SKILL.md +25 -0
- package/templates/skills/agent-job-secrets/agent-job-secrets.js +75 -0
- package/templates/skills/playwright-cli/SKILL.md +294 -0
- package/templates/docker-compose.litellm.yml +0 -81
- package/templates/skills/LICENSE +0 -21
- package/templates/skills/brave-search/SKILL.md +0 -79
- package/templates/skills/brave-search/content.js +0 -86
- package/templates/skills/brave-search/package-lock.json +0 -621
- package/templates/skills/brave-search/package.json +0 -14
- package/templates/skills/brave-search/search.js +0 -199
- package/templates/skills/browser-tools/SKILL.md +0 -196
- package/templates/skills/browser-tools/browser-content.js +0 -103
- package/templates/skills/browser-tools/browser-cookies.js +0 -35
- package/templates/skills/browser-tools/browser-eval.js +0 -53
- package/templates/skills/browser-tools/browser-hn-scraper.js +0 -108
- package/templates/skills/browser-tools/browser-nav.js +0 -44
- package/templates/skills/browser-tools/browser-pick.js +0 -162
- package/templates/skills/browser-tools/browser-screenshot.js +0 -34
- package/templates/skills/browser-tools/browser-start.js +0 -87
- package/templates/skills/browser-tools/package-lock.json +0 -2556
- package/templates/skills/browser-tools/package.json +0 -19
- package/templates/skills/get-secret/SKILL.md +0 -34
- package/templates/skills/get-secret/get-secret.js +0 -33
- package/templates/skills/google-docs/SKILL.md +0 -23
- package/templates/skills/google-docs/create.sh +0 -69
- package/templates/skills/google-drive/SKILL.md +0 -47
- package/templates/skills/google-drive/delete.sh +0 -47
- package/templates/skills/google-drive/download.sh +0 -50
- package/templates/skills/google-drive/list.sh +0 -41
- package/templates/skills/google-drive/upload.sh +0 -76
- package/templates/skills/kie-ai/SKILL.md +0 -38
- package/templates/skills/kie-ai/generate-image.sh +0 -77
- package/templates/skills/kie-ai/generate-video.sh +0 -69
- package/templates/skills/youtube-transcript/SKILL.md +0 -41
- package/templates/skills/youtube-transcript/package-lock.json +0 -24
- package/templates/skills/youtube-transcript/package.json +0 -8
- package/templates/skills/youtube-transcript/transcript.js +0 -84
- package/templates/traefik-dynamic.yml.example +0 -7
package/README.md
CHANGED
|
@@ -192,6 +192,17 @@ See [Different Models](docs/RUNNING_DIFFERENT_MODELS.md) for the full provider r
|
|
|
192
192
|
|
|
193
193
|
---
|
|
194
194
|
|
|
195
|
+
## Known Issues
|
|
196
|
+
|
|
197
|
+
### Windows: `SQLITE_IOERR_SHMOPEN`
|
|
198
|
+
|
|
199
|
+
SQLite can't create or open its shared-memory (`.shm`) file. Common causes:
|
|
200
|
+
|
|
201
|
+
- **Antivirus** (Windows Defender, etc.) locking the database files — add your project folder to the exclusion list
|
|
202
|
+
- **Cloud-synced folders** (OneDrive, Dropbox, Google Drive) — move your project to a non-synced directory like `C:\Projects\`
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
195
206
|
## Docs
|
|
196
207
|
|
|
197
208
|
| Document | Description |
|
package/api/CLAUDE.md
CHANGED
|
@@ -25,6 +25,8 @@ Browser-facing data fetching uses **fetch route handlers** colocated with pages
|
|
|
25
25
|
|--------|------|------|---------|
|
|
26
26
|
| GET | `/api/ping` | None | Health check |
|
|
27
27
|
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job |
|
|
28
|
+
| GET | `/api/get-agent-job-secret` | `x-api-key` | Get an agent job secret; oauth2 credentials return only the access_token (auto-refreshed) |
|
|
29
|
+
| POST | `/api/set-agent-job-secret` | `x-api-key` | Set/update an agent job secret (for agents to persist rotated credentials) |
|
|
28
30
|
| GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
|
|
29
31
|
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler |
|
|
30
32
|
| POST | `/api/telegram/register` | `x-api-key` | Register bot token + webhook URL |
|
package/api/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import { getConfig } from '../lib/config.js';
|
|
|
11
11
|
import { parseOAuthState, exchangeCodeForToken } from '../lib/oauth/helper.js';
|
|
12
12
|
import { setAgentJobSecret } from '../lib/db/config.js';
|
|
13
13
|
|
|
14
|
+
// ── Per-key lock for OAuth token refresh ────────────────────────────
|
|
15
|
+
const _refreshLocks = new Map();
|
|
16
|
+
|
|
14
17
|
// Bot token — resolved from DB/env, can be overridden by /telegram/register
|
|
15
18
|
let telegramBotToken = null;
|
|
16
19
|
|
|
@@ -104,6 +107,81 @@ async function handleCreateAgentJob(request) {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
async function handleGetAgentSecret(request) {
|
|
111
|
+
const record = verifyApiKey(request.headers.get('x-api-key'));
|
|
112
|
+
if (record.type !== 'agent_job_api_key') {
|
|
113
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const key = new URL(request.url).searchParams.get('key');
|
|
117
|
+
if (!key) return Response.json({ error: 'Missing key' }, { status: 400 });
|
|
118
|
+
|
|
119
|
+
const { getAgentJobSecretRaw, setAgentJobSecret: saveSecret } = await import('../lib/db/config.js');
|
|
120
|
+
const raw = getAgentJobSecretRaw(key);
|
|
121
|
+
if (!raw) return Response.json({ error: 'Not found' }, { status: 404 });
|
|
122
|
+
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(raw);
|
|
126
|
+
} catch {
|
|
127
|
+
// Plain string
|
|
128
|
+
return Response.json({ value: raw });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (parsed.type === 'oauth2') {
|
|
132
|
+
// Serialize refresh per key — prevents concurrent requests from racing on token rotation
|
|
133
|
+
if (!_refreshLocks.has(key)) _refreshLocks.set(key, Promise.resolve());
|
|
134
|
+
let release;
|
|
135
|
+
const gate = new Promise((r) => { release = r; });
|
|
136
|
+
const prev = _refreshLocks.get(key);
|
|
137
|
+
_refreshLocks.set(key, gate);
|
|
138
|
+
await prev;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Re-read after acquiring lock — previous request may have already refreshed
|
|
142
|
+
const freshRaw = getAgentJobSecretRaw(key);
|
|
143
|
+
const freshParsed = freshRaw ? JSON.parse(freshRaw) : parsed;
|
|
144
|
+
|
|
145
|
+
const { refreshOAuthToken } = await import('../lib/oauth/helper.js');
|
|
146
|
+
const newToken = await refreshOAuthToken({
|
|
147
|
+
refreshToken: freshParsed.token.refresh_token,
|
|
148
|
+
clientId: freshParsed.clientId,
|
|
149
|
+
clientSecret: freshParsed.clientSecret,
|
|
150
|
+
tokenUrl: freshParsed.tokenUrl,
|
|
151
|
+
});
|
|
152
|
+
// Persist updated token (refresh token may have rotated)
|
|
153
|
+
saveSecret(key, JSON.stringify({ ...freshParsed, token: { ...freshParsed.token, ...newToken } }), 'refresh');
|
|
154
|
+
return Response.json({ value: newToken.access_token });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(`[secrets] OAuth refresh failed for "${key}":`, err.message);
|
|
157
|
+
return Response.json({ error: `OAuth refresh failed: ${err.message}` }, { status: 502 });
|
|
158
|
+
} finally {
|
|
159
|
+
release();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (parsed.type === 'oauth_token') {
|
|
163
|
+
return Response.json({ value: JSON.stringify(parsed.token) });
|
|
164
|
+
}
|
|
165
|
+
// Unknown structured value — return raw
|
|
166
|
+
return Response.json({ value: raw });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handleSetAgentSecret(request) {
|
|
170
|
+
const record = verifyApiKey(request.headers.get('x-api-key'));
|
|
171
|
+
if (record.type !== 'agent_job_api_key') {
|
|
172
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const body = await request.json();
|
|
176
|
+
const { key, value } = body;
|
|
177
|
+
if (!key || typeof value !== 'string') {
|
|
178
|
+
return Response.json({ error: 'Missing key or value' }, { status: 400 });
|
|
179
|
+
}
|
|
180
|
+
const { setAgentJobSecret } = await import('../lib/db/config.js');
|
|
181
|
+
setAgentJobSecret(key, value, 'agent');
|
|
182
|
+
return Response.json({ success: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
107
185
|
async function handleTelegramRegister(request) {
|
|
108
186
|
const body = await request.json();
|
|
109
187
|
const { bot_token, webhook_url } = body;
|
|
@@ -248,9 +326,21 @@ async function handleOAuthCallback(request) {
|
|
|
248
326
|
redirectUri,
|
|
249
327
|
});
|
|
250
328
|
|
|
251
|
-
// Save
|
|
252
|
-
const
|
|
253
|
-
|
|
329
|
+
// Save token with typed wrapper so the API can auto-refresh on fetch
|
|
330
|
+
const secretType = state.secretType || 'oauth2';
|
|
331
|
+
let stored;
|
|
332
|
+
if (secretType === 'oauth_token') {
|
|
333
|
+
stored = JSON.stringify({ type: 'oauth_token', token: tokenData });
|
|
334
|
+
} else {
|
|
335
|
+
stored = JSON.stringify({
|
|
336
|
+
type: 'oauth2',
|
|
337
|
+
token: tokenData,
|
|
338
|
+
clientId: state.clientId,
|
|
339
|
+
clientSecret: state.clientSecret,
|
|
340
|
+
tokenUrl: state.tokenUrl,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
setAgentJobSecret(state.secretName, stored, 'oauth');
|
|
254
344
|
|
|
255
345
|
return oauthResultPage(true, state.secretName);
|
|
256
346
|
} catch (err) {
|
|
@@ -315,6 +405,7 @@ async function POST(request) {
|
|
|
315
405
|
// Route to handler
|
|
316
406
|
switch (routePath) {
|
|
317
407
|
case '/create-agent-job': return handleCreateAgentJob(request);
|
|
408
|
+
case '/set-agent-job-secret': return handleSetAgentSecret(request);
|
|
318
409
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
319
410
|
case '/telegram/register': return handleTelegramRegister(request);
|
|
320
411
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
@@ -333,6 +424,7 @@ async function GET(request) {
|
|
|
333
424
|
switch (routePath) {
|
|
334
425
|
case '/ping': return Response.json({ message: 'Pong!' });
|
|
335
426
|
case '/agent-jobs/status': return handleAgentJobStatus(request);
|
|
427
|
+
case '/get-agent-job-secret': return handleGetAgentSecret(request);
|
|
336
428
|
case '/oauth/callback': return handleOAuthCallback(request);
|
|
337
429
|
default: return Response.json({ error: 'Not found' }, { status: 404 });
|
|
338
430
|
}
|
package/bin/cli.js
CHANGED
|
@@ -52,11 +52,13 @@ Commands:
|
|
|
52
52
|
init Scaffold a new thepopebot project
|
|
53
53
|
upgrade|update [@beta|version] Upgrade thepopebot (install, init, build, commit, push)
|
|
54
54
|
setup Run interactive setup wizard
|
|
55
|
+
setup-ssl Configure SSL with Let's Encrypt wildcard cert
|
|
55
56
|
setup-telegram Reconfigure Telegram webhook
|
|
56
57
|
reset-auth Regenerate AUTH_SECRET (invalidates all sessions)
|
|
57
58
|
reset [file] Restore a template file (or list available templates)
|
|
58
59
|
diff [file] Show differences between project files and package templates
|
|
59
60
|
sync <path> Sync local package to a test install (build, pack, Docker)
|
|
61
|
+
sync --fast <path> Fast sync — copy source into running container, rebuild .next
|
|
60
62
|
set-var <KEY> [VALUE] Set a GitHub repository variable
|
|
61
63
|
user:password <email> Change a user's password
|
|
62
64
|
`);
|
|
@@ -246,6 +248,7 @@ async function init() {
|
|
|
246
248
|
const pkg = {
|
|
247
249
|
name: dirName,
|
|
248
250
|
private: true,
|
|
251
|
+
type: 'module',
|
|
249
252
|
scripts: {
|
|
250
253
|
setup: 'thepopebot setup',
|
|
251
254
|
'setup-telegram': 'thepopebot setup-telegram',
|
|
@@ -258,11 +261,19 @@ async function init() {
|
|
|
258
261
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
259
262
|
console.log(' Created package.json');
|
|
260
263
|
} else {
|
|
261
|
-
|
|
264
|
+
// Ensure "type": "module" is set for ESM support
|
|
265
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
266
|
+
if (!pkg.type) {
|
|
267
|
+
pkg.type = 'module';
|
|
268
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
269
|
+
console.log(' Added "type": "module" to package.json');
|
|
270
|
+
} else {
|
|
271
|
+
console.log(' Skipped package.json (already exists)');
|
|
272
|
+
}
|
|
262
273
|
}
|
|
263
274
|
|
|
264
275
|
// Create default skill activation symlinks
|
|
265
|
-
const defaultSkills = [
|
|
276
|
+
const defaultSkills = [];
|
|
266
277
|
const activeDir = path.join(cwd, 'skills', 'active');
|
|
267
278
|
fs.mkdirSync(activeDir, { recursive: true });
|
|
268
279
|
for (const skill of defaultSkills) {
|
|
@@ -337,8 +348,7 @@ AUTH_TRUST_HOST=true
|
|
|
337
348
|
DATABASE_PATH=data/db/thepopebot.sqlite
|
|
338
349
|
THEPOPEBOT_VERSION=${version}
|
|
339
350
|
|
|
340
|
-
#
|
|
341
|
-
# Edit docker-compose.custom.yml with your changes, then uncomment:
|
|
351
|
+
# To enable SSL with Let's Encrypt, run: npx thepopebot setup-ssl
|
|
342
352
|
# COMPOSE_FILE=docker-compose.custom.yml
|
|
343
353
|
`;
|
|
344
354
|
fs.writeFileSync(envPath, seedEnv);
|
|
@@ -490,6 +500,15 @@ function setup() {
|
|
|
490
500
|
}
|
|
491
501
|
}
|
|
492
502
|
|
|
503
|
+
function setupSsl() {
|
|
504
|
+
const setupScript = path.join(__dirname, '..', 'setup', 'setup-ssl.mjs');
|
|
505
|
+
try {
|
|
506
|
+
execFileSync(process.execPath, [setupScript], { stdio: 'inherit', cwd: process.cwd() });
|
|
507
|
+
} catch {
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
493
512
|
function setupTelegram() {
|
|
494
513
|
const setupScript = path.join(__dirname, '..', 'setup', 'setup-telegram.mjs');
|
|
495
514
|
try {
|
|
@@ -770,6 +789,9 @@ switch (command) {
|
|
|
770
789
|
case 'setup':
|
|
771
790
|
setup();
|
|
772
791
|
break;
|
|
792
|
+
case 'setup-ssl':
|
|
793
|
+
setupSsl();
|
|
794
|
+
break;
|
|
773
795
|
case 'setup-telegram':
|
|
774
796
|
setupTelegram();
|
|
775
797
|
break;
|
|
@@ -787,8 +809,15 @@ switch (command) {
|
|
|
787
809
|
await upgrade();
|
|
788
810
|
break;
|
|
789
811
|
case 'sync': {
|
|
790
|
-
const
|
|
791
|
-
|
|
812
|
+
const fast = args.includes('--fast');
|
|
813
|
+
const syncArgs = args.filter(a => a !== '--fast');
|
|
814
|
+
if (fast) {
|
|
815
|
+
const { syncFast } = await import('./sync.js');
|
|
816
|
+
await syncFast(syncArgs[0]);
|
|
817
|
+
} else {
|
|
818
|
+
const { sync } = await import('./sync.js');
|
|
819
|
+
await sync(syncArgs[0]);
|
|
820
|
+
}
|
|
792
821
|
break;
|
|
793
822
|
}
|
|
794
823
|
case 'set-var':
|
package/bin/docker-build.js
CHANGED
|
@@ -62,6 +62,11 @@ const CODING_AGENTS = [
|
|
|
62
62
|
context: 'docker/coding-agent',
|
|
63
63
|
dockerfile: 'docker/coding-agent/Dockerfile.opencode',
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: 'coding-agent-kimi-cli',
|
|
67
|
+
context: 'docker/coding-agent',
|
|
68
|
+
dockerfile: 'docker/coding-agent/Dockerfile.kimi-cli',
|
|
69
|
+
},
|
|
65
70
|
];
|
|
66
71
|
|
|
67
72
|
// Non-coding-agent images (independent, built in parallel)
|
package/bin/managed-paths.js
CHANGED
package/bin/sync.js
CHANGED
|
@@ -277,8 +277,7 @@ function buildDockerImage(projectPath) {
|
|
|
277
277
|
fs.cpSync(webSrc, webDest, { recursive: true });
|
|
278
278
|
|
|
279
279
|
try {
|
|
280
|
-
|
|
281
|
-
execSync(`docker build --no-cache -f - -t ${imageTag} .`, {
|
|
280
|
+
execSync(`docker build -f - -t ${imageTag} .`, {
|
|
282
281
|
input: dockerfile,
|
|
283
282
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
284
283
|
cwd: projectPath,
|
|
@@ -287,6 +286,12 @@ function buildDockerImage(projectPath) {
|
|
|
287
286
|
fs.rmSync(webDest, { recursive: true, force: true });
|
|
288
287
|
}
|
|
289
288
|
|
|
289
|
+
// Clean up dangling images from previous builds
|
|
290
|
+
try {
|
|
291
|
+
execSync('docker image prune -f', { stdio: 'ignore' });
|
|
292
|
+
} catch {}
|
|
293
|
+
|
|
294
|
+
|
|
290
295
|
// Update THEPOPEBOT_VERSION in .env
|
|
291
296
|
const envPath = path.join(projectPath, '.env');
|
|
292
297
|
if (fs.existsSync(envPath)) {
|
|
@@ -301,6 +306,90 @@ function buildDockerImage(projectPath) {
|
|
|
301
306
|
}
|
|
302
307
|
}
|
|
303
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Fast sync — skip Docker image rebuild entirely.
|
|
311
|
+
*
|
|
312
|
+
* 1. Build package JSX (npm run build)
|
|
313
|
+
* 2. mirrorTemplates() — scaffold using init's managed-path logic
|
|
314
|
+
* 3. docker cp package source (lib/, api/, config/, package.json) into
|
|
315
|
+
* the running container's /app/node_modules/thepopebot/
|
|
316
|
+
* 4. docker cp web/app/ + web/postcss.config.mjs into container
|
|
317
|
+
* 5. docker exec next build inside the container (tailwindcss already there)
|
|
318
|
+
* 6. Clean up copied source from container
|
|
319
|
+
* 7. docker exec pm2 restart all
|
|
320
|
+
*/
|
|
321
|
+
export async function syncFast(projectPath) {
|
|
322
|
+
if (!projectPath) {
|
|
323
|
+
console.error('\n Usage: thepopebot sync --fast <path-to-project>\n');
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
projectPath = path.resolve(projectPath);
|
|
328
|
+
|
|
329
|
+
if (!fs.existsSync(path.join(projectPath, 'package.json'))) {
|
|
330
|
+
console.error(`\n Not a project directory (no package.json): ${projectPath}\n`);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 1. Build JSX
|
|
335
|
+
console.log('\n Building package...');
|
|
336
|
+
execSync('npm run build', { stdio: 'inherit', cwd: PACKAGE_DIR });
|
|
337
|
+
|
|
338
|
+
// 2. Mirror templates
|
|
339
|
+
console.log('\n Mirroring templates...');
|
|
340
|
+
mirrorTemplates(projectPath);
|
|
341
|
+
|
|
342
|
+
// 3. Get running container ID
|
|
343
|
+
const container = execSync('docker compose ps -q event-handler', {
|
|
344
|
+
encoding: 'utf8',
|
|
345
|
+
cwd: projectPath,
|
|
346
|
+
}).trim();
|
|
347
|
+
|
|
348
|
+
if (!container) {
|
|
349
|
+
console.error('\n event-handler container is not running. Use full sync instead.\n');
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 4. Copy package source into container's node_modules/thepopebot/
|
|
354
|
+
const PKG_DEST = '/app/node_modules/thepopebot';
|
|
355
|
+
const PACKAGE_DIRS = ['lib', 'api', 'config'];
|
|
356
|
+
|
|
357
|
+
console.log('\n Copying package source into container...');
|
|
358
|
+
for (const dir of PACKAGE_DIRS) {
|
|
359
|
+
execSync(`docker exec ${container} rm -rf ${PKG_DEST}/${dir}`, { stdio: 'inherit' });
|
|
360
|
+
execSync(`docker cp ${path.join(PACKAGE_DIR, dir)} ${container}:${PKG_DEST}/${dir}`, { stdio: 'inherit' });
|
|
361
|
+
}
|
|
362
|
+
// Also copy package.json for exports resolution
|
|
363
|
+
execSync(`docker cp ${path.join(PACKAGE_DIR, 'package.json')} ${container}:${PKG_DEST}/package.json`, { stdio: 'inherit' });
|
|
364
|
+
|
|
365
|
+
// 5. Copy web/app/ source into container for next build
|
|
366
|
+
const webDir = path.join(PACKAGE_DIR, 'web');
|
|
367
|
+
console.log('\n Copying web source into container...');
|
|
368
|
+
execSync(`docker cp ${path.join(webDir, 'app')} ${container}:/app/app`, { stdio: 'inherit' });
|
|
369
|
+
execSync(`docker cp ${path.join(webDir, 'postcss.config.mjs')} ${container}:/app/postcss.config.mjs`, { stdio: 'inherit' });
|
|
370
|
+
execSync(`docker cp ${path.join(webDir, 'next.config.mjs')} ${container}:/app/next.config.mjs`, { stdio: 'inherit' });
|
|
371
|
+
|
|
372
|
+
// 6. Run next build inside the container
|
|
373
|
+
// Hide data/logs dirs so webpack's FileSystemInfo doesn't crawl them (causes OOM/RangeError
|
|
374
|
+
// when workspaces contain thousands of files). Restored immediately after build.
|
|
375
|
+
console.log('\n Building Next.js inside container...');
|
|
376
|
+
execSync(`docker exec ${container} sh -c 'mv /app/data /app/.data-build-tmp 2>/dev/null; mv /app/logs /app/.logs-build-tmp 2>/dev/null; true'`, { stdio: 'inherit' });
|
|
377
|
+
try {
|
|
378
|
+
execSync(`docker exec ${container} ./node_modules/.bin/next build`, { stdio: 'inherit' });
|
|
379
|
+
} finally {
|
|
380
|
+
execSync(`docker exec ${container} sh -c 'mv /app/.data-build-tmp /app/data 2>/dev/null; mv /app/.logs-build-tmp /app/logs 2>/dev/null; true'`, { stdio: 'inherit' });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 7. Clean up web source from container (not needed at runtime)
|
|
384
|
+
execSync(`docker exec ${container} rm -rf /app/app`, { stdio: 'inherit' });
|
|
385
|
+
|
|
386
|
+
// 8. Restart PM2
|
|
387
|
+
console.log('\n Restarting server...');
|
|
388
|
+
execSync(`docker exec ${container} pm2 restart all`, { stdio: 'inherit' });
|
|
389
|
+
|
|
390
|
+
console.log('\n Fast synced!\n');
|
|
391
|
+
}
|
|
392
|
+
|
|
304
393
|
export async function sync(projectPath) {
|
|
305
394
|
if (!projectPath) {
|
|
306
395
|
console.error('\n Usage: thepopebot sync <path-to-project>\n');
|
|
@@ -70,5 +70,9 @@ export async function register() {
|
|
|
70
70
|
const { startClusterRuntime } = await import('../lib/cluster/runtime.js');
|
|
71
71
|
startClusterRuntime();
|
|
72
72
|
|
|
73
|
+
// Start internal maintenance cron (cleanup orphaned agent job keys, etc.)
|
|
74
|
+
const { startMaintenanceCron } = await import('../lib/maintenance.js');
|
|
75
|
+
startMaintenanceCron();
|
|
76
|
+
|
|
73
77
|
console.log('thepopebot initialized');
|
|
74
78
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async push/pull queue. Producer calls push()/done(), consumer uses for-await.
|
|
3
|
+
*/
|
|
4
|
+
export function createChannel() {
|
|
5
|
+
const queue = [];
|
|
6
|
+
const waiters = [];
|
|
7
|
+
let isDone = false;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
push(value) {
|
|
11
|
+
if (waiters.length > 0) waiters.shift()(value);
|
|
12
|
+
else queue.push(value);
|
|
13
|
+
},
|
|
14
|
+
done() {
|
|
15
|
+
isDone = true;
|
|
16
|
+
while (waiters.length > 0) waiters.shift()(Symbol.for('done'));
|
|
17
|
+
},
|
|
18
|
+
async *[Symbol.asyncIterator]() {
|
|
19
|
+
while (true) {
|
|
20
|
+
if (queue.length > 0) {
|
|
21
|
+
yield queue.shift();
|
|
22
|
+
} else if (isDone) {
|
|
23
|
+
return;
|
|
24
|
+
} else {
|
|
25
|
+
const value = await new Promise(resolve => waiters.push(resolve));
|
|
26
|
+
if (value === Symbol.for('done')) return;
|
|
27
|
+
yield value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merge two async iterables — yields from whichever has data first.
|
|
36
|
+
* Completes when BOTH are exhausted.
|
|
37
|
+
*/
|
|
38
|
+
export async function* mergeAsyncIterables(iter1, iter2) {
|
|
39
|
+
const channel = createChannel();
|
|
40
|
+
let active = 2;
|
|
41
|
+
|
|
42
|
+
const consume = async (iter) => {
|
|
43
|
+
for await (const item of iter) channel.push(item);
|
|
44
|
+
if (--active === 0) channel.done();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
consume(iter1);
|
|
48
|
+
consume(iter2);
|
|
49
|
+
|
|
50
|
+
yield* channel;
|
|
51
|
+
}
|