jettypod 4.4.116 → 4.4.120
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/.env +7 -0
- package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +124 -48
- package/apps/dashboard/app/api/claude/[workItemId]/route.ts +171 -58
- package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +161 -10
- package/apps/dashboard/app/api/tests/run/stream/route.ts +13 -1
- package/apps/dashboard/app/api/usage/route.ts +17 -0
- package/apps/dashboard/app/api/work/[id]/route.ts +35 -0
- package/apps/dashboard/app/api/work/[id]/status/route.ts +43 -1
- package/apps/dashboard/app/connect-claude/page.tsx +24 -0
- package/apps/dashboard/app/decision/[id]/page.tsx +14 -14
- package/apps/dashboard/app/demo/gates/page.tsx +42 -42
- package/apps/dashboard/app/design-system/page.tsx +868 -0
- package/apps/dashboard/app/globals.css +6 -2
- package/apps/dashboard/app/install-claude/page.tsx +9 -7
- package/apps/dashboard/app/layout.tsx +17 -5
- package/apps/dashboard/app/login/page.tsx +250 -0
- package/apps/dashboard/app/page.tsx +11 -9
- package/apps/dashboard/app/settings/page.tsx +4 -2
- package/apps/dashboard/app/signup/page.tsx +245 -0
- package/apps/dashboard/app/subscribe/page.tsx +11 -0
- package/apps/dashboard/app/welcome/page.tsx +24 -1
- package/apps/dashboard/app/work/[id]/page.tsx +34 -50
- package/apps/dashboard/components/AppShell.tsx +95 -55
- package/apps/dashboard/components/CardMenu.tsx +56 -13
- package/apps/dashboard/components/ClaudePanel.tsx +301 -582
- package/apps/dashboard/components/ClaudePanelInput.tsx +23 -14
- package/apps/dashboard/components/ConnectClaudeScreen.tsx +210 -0
- package/apps/dashboard/components/CopyableId.tsx +3 -3
- package/apps/dashboard/components/DetailReviewActions.tsx +109 -0
- package/apps/dashboard/components/DragContext.tsx +75 -65
- package/apps/dashboard/components/DraggableCard.tsx +6 -46
- package/apps/dashboard/components/DropZone.tsx +2 -2
- package/apps/dashboard/components/EditableDetailDescription.tsx +1 -1
- package/apps/dashboard/components/EditableTitle.tsx +26 -6
- package/apps/dashboard/components/ElapsedTimer.tsx +54 -0
- package/apps/dashboard/components/EpicGroup.tsx +329 -0
- package/apps/dashboard/components/GateCard.tsx +100 -16
- package/apps/dashboard/components/GateChoiceCard.tsx +15 -17
- package/apps/dashboard/components/InstallClaudeScreen.tsx +140 -51
- package/apps/dashboard/components/JettyLoader.tsx +38 -0
- package/apps/dashboard/components/KanbanBoard.tsx +147 -766
- package/apps/dashboard/components/KanbanCard.tsx +506 -0
- package/apps/dashboard/components/LazyMarkdown.tsx +12 -0
- package/apps/dashboard/components/MainNav.tsx +20 -54
- package/apps/dashboard/components/MessageBlock.tsx +391 -0
- package/apps/dashboard/components/ModeStartCard.tsx +15 -15
- package/apps/dashboard/components/OnboardingWelcome.tsx +214 -0
- package/apps/dashboard/components/PlaceholderCard.tsx +11 -21
- package/apps/dashboard/components/ProjectSwitcher.tsx +36 -8
- package/apps/dashboard/components/PrototypeTimeline.tsx +25 -25
- package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +265 -301
- package/apps/dashboard/components/RealTimeTestsWrapper.tsx +97 -74
- package/apps/dashboard/components/ReviewFooter.tsx +141 -0
- package/apps/dashboard/components/SessionList.tsx +19 -18
- package/apps/dashboard/components/SubscribeContent.tsx +206 -0
- package/apps/dashboard/components/TestTree.tsx +15 -14
- package/apps/dashboard/components/TipCard.tsx +177 -0
- package/apps/dashboard/components/Toast.tsx +5 -5
- package/apps/dashboard/components/TypeIcon.tsx +56 -0
- package/apps/dashboard/components/UpgradeBanner.tsx +30 -0
- package/apps/dashboard/components/WaveCompletionAnimation.tsx +61 -62
- package/apps/dashboard/components/WelcomeScreen.tsx +25 -27
- package/apps/dashboard/components/WorkItemHeader.tsx +4 -4
- package/apps/dashboard/components/WorkItemTree.tsx +9 -28
- package/apps/dashboard/components/settings/AccountSection.tsx +169 -0
- package/apps/dashboard/components/settings/EnvVarsSection.tsx +54 -79
- package/apps/dashboard/components/settings/GeneralSection.tsx +26 -31
- package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -4
- package/apps/dashboard/components/ui/Button.tsx +104 -0
- package/apps/dashboard/components/ui/Input.tsx +78 -0
- package/apps/dashboard/contexts/ClaudeSessionContext.tsx +408 -105
- package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -4
- package/apps/dashboard/contexts/UsageContext.tsx +155 -0
- package/apps/dashboard/contexts/usageHelpers.js +9 -0
- package/apps/dashboard/electron/ipc-handlers.js +281 -88
- package/apps/dashboard/electron/main.js +691 -131
- package/apps/dashboard/electron/preload.js +25 -4
- package/apps/dashboard/electron/session-manager.js +163 -0
- package/apps/dashboard/electron-builder.config.js +3 -5
- package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
- package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
- package/apps/dashboard/lib/backlog-parser.ts +50 -0
- package/apps/dashboard/lib/claude-process-manager.ts +50 -11
- package/apps/dashboard/lib/constants.ts +43 -0
- package/apps/dashboard/lib/db-bridge.ts +33 -0
- package/apps/dashboard/lib/db.ts +136 -20
- package/apps/dashboard/lib/kanban-utils.ts +70 -0
- package/apps/dashboard/lib/run-migrations.js +27 -2
- package/apps/dashboard/lib/session-state-machine.ts +3 -0
- package/apps/dashboard/lib/session-stream-manager.ts +144 -38
- package/apps/dashboard/lib/shadows.ts +7 -0
- package/apps/dashboard/lib/tests.ts +3 -1
- package/apps/dashboard/lib/utils.ts +6 -0
- package/apps/dashboard/next.config.js +35 -14
- package/apps/dashboard/package.json +6 -3
- package/apps/dashboard/public/bug-icon.svg +9 -0
- package/apps/dashboard/public/buoy-icon.svg +9 -0
- package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
- package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
- package/apps/dashboard/public/in-flight-seagull.svg +9 -0
- package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
- package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
- package/apps/dashboard/public/jettypod_logo.png +0 -0
- package/apps/dashboard/public/pier-icon.svg +14 -0
- package/apps/dashboard/public/star-icon.svg +9 -0
- package/apps/dashboard/public/wrench-icon.svg +9 -0
- package/apps/dashboard/scripts/upload-to-r2.js +89 -0
- package/apps/dashboard/scripts/ws-server.js +191 -0
- package/apps/dashboard/tsconfig.tsbuildinfo +1 -0
- package/apps/update-server/package.json +16 -0
- package/apps/update-server/schema.sql +31 -0
- package/apps/update-server/src/index.ts +1085 -0
- package/apps/update-server/tsconfig.json +16 -0
- package/apps/update-server/wrangler.toml +35 -0
- package/cucumber.js +9 -3
- package/docs/COMMAND_REFERENCE.md +34 -0
- package/hooks/post-checkout +32 -75
- package/hooks/post-merge +111 -10
- package/jest.setup.js +1 -0
- package/jettypod.js +54 -116
- package/lib/chore-taxonomy.js +33 -10
- package/lib/database.js +36 -16
- package/lib/db-watcher.js +1 -1
- package/lib/git-hooks/pre-commit +1 -1
- package/lib/jettypod-backup.js +27 -4
- package/lib/migrations/027-plan-at-creation-column.js +33 -0
- package/lib/migrations/028-ready-for-review-column.js +27 -0
- package/lib/migrations/029-remove-autoincrement.js +307 -0
- package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
- package/lib/migrations/index.js +47 -4
- package/lib/schema.js +13 -6
- package/lib/seed-onboarding.js +101 -69
- package/lib/update-command/index.js +9 -175
- package/lib/work-commands/index.js +129 -16
- package/lib/work-tracking/index.js +86 -46
- package/lib/worktree-diagnostics.js +16 -16
- package/lib/worktree-facade.js +1 -1
- package/lib/worktree-manager.js +8 -8
- package/lib/worktree-reconciler.js +5 -5
- package/package.json +9 -2
- package/scripts/ndjson-to-cucumber-json.js +152 -0
- package/scripts/postinstall.js +25 -0
- package/skills-templates/bug-mode/SKILL.md +39 -28
- package/skills-templates/bug-planning/SKILL.md +25 -29
- package/skills-templates/chore-mode/SKILL.md +131 -68
- package/skills-templates/chore-mode/verification.js +51 -10
- package/skills-templates/chore-planning/SKILL.md +47 -18
- package/skills-templates/epic-planning/SKILL.md +68 -48
- package/skills-templates/external-transition/SKILL.md +47 -47
- package/skills-templates/feature-planning/SKILL.md +83 -73
- package/skills-templates/production-mode/SKILL.md +49 -49
- package/skills-templates/request-routing/SKILL.md +27 -14
- package/skills-templates/simple-improvement/SKILL.md +68 -44
- package/skills-templates/speed-mode/SKILL.md +209 -128
- package/skills-templates/stable-mode/SKILL.md +105 -94
- package/templates/bdd-guidance.md +139 -0
- package/templates/bdd-scaffolding/wait.js +18 -0
- package/templates/bdd-scaffolding/world.js +19 -0
- package/.jettypod-backup/work.db +0 -0
- package/apps/dashboard/app/access-code/page.tsx +0 -110
- package/lib/discovery-checkpoint.js +0 -123
- package/skills-templates/project-discovery/SKILL.md +0 -372
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upload release artifacts to Cloudflare R2.
|
|
5
|
+
* Run after electron-builder: npm run upload:r2
|
|
6
|
+
*
|
|
7
|
+
* Requires CLOUDFLARE_API_TOKEN env var (or wrangler login).
|
|
8
|
+
* Uploads: latest-mac.yml, DMG, ZIP, and blockmap files.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const BUCKET_NAME = 'jettypod-releases';
|
|
16
|
+
const DIST_DIR = path.join(__dirname, '..', 'dist');
|
|
17
|
+
|
|
18
|
+
// File patterns to upload
|
|
19
|
+
const UPLOAD_PATTERNS = [
|
|
20
|
+
/^latest-mac\.yml$/,
|
|
21
|
+
/\.dmg$/,
|
|
22
|
+
/\.zip$/,
|
|
23
|
+
/\.blockmap$/,
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function findArtifacts() {
|
|
27
|
+
if (!fs.existsSync(DIST_DIR)) {
|
|
28
|
+
console.error(`❌ dist/ directory not found at ${DIST_DIR}`);
|
|
29
|
+
console.error('Run electron:build first.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const files = fs.readdirSync(DIST_DIR);
|
|
34
|
+
return files.filter((file) =>
|
|
35
|
+
UPLOAD_PATTERNS.some((pattern) => pattern.test(file))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function uploadFile(filename) {
|
|
40
|
+
const filePath = path.join(DIST_DIR, filename);
|
|
41
|
+
const stats = fs.statSync(filePath);
|
|
42
|
+
const sizeMB = (stats.size / (1024 * 1024)).toFixed(1);
|
|
43
|
+
|
|
44
|
+
console.log(` Uploading ${filename} (${sizeMB} MB)...`);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
execSync(
|
|
48
|
+
`npx wrangler r2 object put "${BUCKET_NAME}/${filename}" --file="${filePath}" --remote`,
|
|
49
|
+
{ stdio: 'pipe' }
|
|
50
|
+
);
|
|
51
|
+
console.log(` ✅ ${filename}`);
|
|
52
|
+
return true;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(` ❌ Failed to upload ${filename}: ${error.message}`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function main() {
|
|
60
|
+
console.log('🚀 Uploading release artifacts to R2...\n');
|
|
61
|
+
|
|
62
|
+
const artifacts = findArtifacts();
|
|
63
|
+
if (artifacts.length === 0) {
|
|
64
|
+
console.error('❌ No release artifacts found in dist/');
|
|
65
|
+
console.error('Expected: latest-mac.yml, .dmg, .zip, or .blockmap files');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`Found ${artifacts.length} artifact(s):\n`);
|
|
70
|
+
|
|
71
|
+
let success = 0;
|
|
72
|
+
let failed = 0;
|
|
73
|
+
|
|
74
|
+
for (const artifact of artifacts) {
|
|
75
|
+
if (uploadFile(artifact)) {
|
|
76
|
+
success++;
|
|
77
|
+
} else {
|
|
78
|
+
failed++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(`\n📦 Upload complete: ${success} uploaded, ${failed} failed`);
|
|
83
|
+
|
|
84
|
+
if (failed > 0) {
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone WebSocket server for browser dev mode.
|
|
5
|
+
*
|
|
6
|
+
* Polls the JettyPod SQLite database and cucumber-results.json for changes
|
|
7
|
+
* and broadcasts notifications to connected clients. This is the same
|
|
8
|
+
* behaviour embedded in electron/main.js, extracted so `npm run dev` can
|
|
9
|
+
* provide live updates without Electron running.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/ws-server.js [project-path]
|
|
13
|
+
*
|
|
14
|
+
* project-path defaults to the repository root (two levels up from this script).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { WebSocketServer } = require('ws');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
const WS_PORT = 47808;
|
|
22
|
+
const DB_POLL_MS = 500;
|
|
23
|
+
|
|
24
|
+
const projectRoot = process.argv[2] || path.resolve(__dirname, '..', '..', '..');
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// State
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const clients = new Set();
|
|
30
|
+
let dbPollInterval = null;
|
|
31
|
+
let testResultsPollInterval = null;
|
|
32
|
+
let lastDbMtimes = { db: null, wal: null };
|
|
33
|
+
let lastTestResultsMtime = null;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
function log(msg) {
|
|
39
|
+
console.log(`[ws-server] ${msg}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function broadcast(message) {
|
|
43
|
+
const payload = JSON.stringify(message);
|
|
44
|
+
for (const client of clients) {
|
|
45
|
+
if (client.readyState === 1) { // WebSocket.OPEN
|
|
46
|
+
client.send(payload);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Database polling (WAL-aware)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
function startDatabasePolling() {
|
|
55
|
+
const dbPath = path.join(projectRoot, '.jettypod', 'work.db');
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(dbPath)) {
|
|
58
|
+
log(`Database not found at ${dbPath}, retrying in 5s...`);
|
|
59
|
+
setTimeout(startDatabasePolling, 5000);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const dbStats = fs.statSync(dbPath);
|
|
65
|
+
lastDbMtimes.db = dbStats.mtimeMs;
|
|
66
|
+
|
|
67
|
+
const walPath = dbPath + '-wal';
|
|
68
|
+
if (fs.existsSync(walPath)) {
|
|
69
|
+
lastDbMtimes.wal = fs.statSync(walPath).mtimeMs;
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
log(`Failed to get initial db stats: ${err.message}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
log(`Polling database for changes (WAL-aware)...`);
|
|
77
|
+
|
|
78
|
+
dbPollInterval = setInterval(() => {
|
|
79
|
+
try {
|
|
80
|
+
let changed = false;
|
|
81
|
+
|
|
82
|
+
const dbStats = fs.statSync(dbPath);
|
|
83
|
+
if (dbStats.mtimeMs !== lastDbMtimes.db) {
|
|
84
|
+
lastDbMtimes.db = dbStats.mtimeMs;
|
|
85
|
+
changed = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const walPath = dbPath + '-wal';
|
|
89
|
+
if (fs.existsSync(walPath)) {
|
|
90
|
+
const walStats = fs.statSync(walPath);
|
|
91
|
+
if (walStats.mtimeMs !== lastDbMtimes.wal) {
|
|
92
|
+
lastDbMtimes.wal = walStats.mtimeMs;
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (changed) {
|
|
98
|
+
broadcast({ type: 'db_change', timestamp: Date.now() });
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// File might be temporarily locked during writes — ignore
|
|
102
|
+
}
|
|
103
|
+
}, DB_POLL_MS);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Test results polling
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
function startTestResultsPolling() {
|
|
110
|
+
const testResultsPath = path.join(projectRoot, 'cucumber-results.json');
|
|
111
|
+
|
|
112
|
+
if (fs.existsSync(testResultsPath)) {
|
|
113
|
+
try {
|
|
114
|
+
lastTestResultsMtime = fs.statSync(testResultsPath).mtimeMs;
|
|
115
|
+
log(`Watching test results at ${testResultsPath}`);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log(`Failed to get initial test results stats: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
log(`Test results file not found at ${testResultsPath}, will watch for creation`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
testResultsPollInterval = setInterval(() => {
|
|
124
|
+
try {
|
|
125
|
+
if (!fs.existsSync(testResultsPath)) {
|
|
126
|
+
if (lastTestResultsMtime !== null) {
|
|
127
|
+
lastTestResultsMtime = null;
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const stats = fs.statSync(testResultsPath);
|
|
133
|
+
if (stats.mtimeMs !== lastTestResultsMtime) {
|
|
134
|
+
lastTestResultsMtime = stats.mtimeMs;
|
|
135
|
+
broadcast({ type: 'test_change', timestamp: Date.now() });
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// ignore
|
|
139
|
+
}
|
|
140
|
+
}, DB_POLL_MS);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Server
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
const wss = new WebSocketServer({ port: WS_PORT });
|
|
147
|
+
|
|
148
|
+
wss.on('connection', (ws) => {
|
|
149
|
+
clients.add(ws);
|
|
150
|
+
log(`Client connected. Total: ${clients.size}`);
|
|
151
|
+
ws.send(JSON.stringify({ type: 'connected', timestamp: Date.now() }));
|
|
152
|
+
|
|
153
|
+
ws.on('close', () => {
|
|
154
|
+
clients.delete(ws);
|
|
155
|
+
log(`Client disconnected. Total: ${clients.size}`);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
ws.on('error', (err) => {
|
|
159
|
+
log(`Client error: ${err.message}`);
|
|
160
|
+
ws.close();
|
|
161
|
+
clients.delete(ws);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
wss.on('error', (err) => {
|
|
166
|
+
if (err.code === 'EADDRINUSE') {
|
|
167
|
+
log(`Port ${WS_PORT} already in use (Electron running?). Exiting.`);
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
log(`Server error: ${err.message}`);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
wss.on('listening', () => {
|
|
174
|
+
log(`WebSocket server running on ws://localhost:${WS_PORT}`);
|
|
175
|
+
log(`Watching project: ${projectRoot}`);
|
|
176
|
+
startDatabasePolling();
|
|
177
|
+
startTestResultsPolling();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Graceful shutdown
|
|
181
|
+
function shutdown() {
|
|
182
|
+
log('Shutting down...');
|
|
183
|
+
if (dbPollInterval) clearInterval(dbPollInterval);
|
|
184
|
+
if (testResultsPollInterval) clearInterval(testResultsPollInterval);
|
|
185
|
+
for (const client of clients) client.close();
|
|
186
|
+
clients.clear();
|
|
187
|
+
wss.close(() => process.exit(0));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
process.on('SIGINT', shutdown);
|
|
191
|
+
process.on('SIGTERM', shutdown);
|