@geraldmaron/construct 1.0.23 → 1.0.24
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 +0 -2
- package/bin/construct +15 -215
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/reconcile/index.mjs +0 -2
- package/lib/service-manager.mjs +38 -256
- package/lib/setup.mjs +26 -426
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- package/lib/storage/hybrid-query.mjs +15 -196
- package/lib/storage/sync.mjs +36 -177
- package/lib/storage/vector-client.mjs +256 -235
- package/lib/strategy-store.mjs +35 -286
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/platforms/claude/settings.template.json +0 -7
- package/scripts/sync-specialists.mjs +46 -12
- package/specialists/prompts/cx-qa.md +1 -1
- package/specialists/registry.json +0 -8
- package/templates/docs/construct_guide.md +1 -1
- package/db/schema/001_init.sql +0 -40
- package/db/schema/002_pgvector.sql +0 -182
- package/db/schema/003_intake.sql +0 -47
- package/db/schema/003_observation_reconciliation.sql +0 -14
- package/db/schema/004_recommendations.sql +0 -46
- package/db/schema/005_strategy.sql +0 -21
- package/db/schema/006_graph.sql +0 -24
- package/db/schema/007_tags.sql +0 -30
- package/db/schema/008_skill_usage.sql +0 -24
- package/db/schema/009_scheduler.sql +0 -14
- package/db/schema/010_cx_scores.sql +0 -51
- package/lib/intake/postgres-queue.mjs +0 -240
- package/lib/reconcile/postgres-namespace.mjs +0 -102
- package/lib/services/local-postgres.mjs +0 -15
- package/lib/storage/backup.mjs +0 -347
- package/lib/storage/migrations.mjs +0 -187
- package/lib/storage/postgres-backup.mjs +0 -124
- package/lib/storage/sql-store.mjs +0 -45
- package/lib/storage/store-version.mjs +0 -115
- package/lib/storage/unified-storage.mjs +0 -550
- package/lib/storage/vector-store.mjs +0 -100
package/lib/service-manager.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/service-manager.mjs — start, stop, and describe Construct runtime services.
|
|
3
3
|
*
|
|
4
|
-
* Manages the dashboard process,
|
|
5
|
-
*
|
|
4
|
+
* Manages the dashboard process, memory server (cm), and OpenCode.
|
|
5
|
+
* startServices() is the single entry point for `construct up` —
|
|
6
6
|
* spawns whatever is available and returns a result per service.
|
|
7
7
|
*/
|
|
8
8
|
import { spawn, spawnSync } from 'node:child_process';
|
|
@@ -14,37 +14,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
14
14
|
|
|
15
15
|
import { findAvailablePort } from './host-capabilities.mjs';
|
|
16
16
|
import { getUserEnvPath, loadConstructEnv, parseEnvFile, writeEnvValues } from './env-config.mjs';
|
|
17
|
-
import { detectDockerCompose } from './setup.mjs';
|
|
18
|
-
import { stashConstructDb, restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
19
17
|
import { runPressureRelease } from './runtime-pressure.mjs';
|
|
20
|
-
import {
|
|
21
|
-
pruneStashDir,
|
|
22
|
-
verifyTelemetryKeys,
|
|
23
|
-
isRemoteTelemetry,
|
|
24
|
-
} from './services/local-postgres.mjs';
|
|
25
18
|
import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
const CONSTRUCT_PG_COMPOSE_DIR = 'services/postgres';
|
|
29
|
-
const CONSTRUCT_PG_CONTAINER = postgresContainerName();
|
|
30
|
-
const CONSTRUCT_PG_PORT = postgresPort();
|
|
31
|
-
const CONSTRUCT_PG_HEALTH_RETRIES = 12;
|
|
32
|
-
const CONSTRUCT_PG_HEALTH_INTERVAL_MS = 2000;
|
|
19
|
+
import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
|
|
33
20
|
|
|
34
21
|
const DASHBOARD_STATE_FILE = 'dashboard.json';
|
|
35
22
|
|
|
36
|
-
// The caller's `rootDir` is the project (for compose/context/traces), but the
|
|
37
|
-
// dashboard and doctor entrypoints ship with the install, not the project. They
|
|
38
|
-
// must resolve against the install root — derived from this module's own
|
|
39
|
-
// location — or `construct init` spawns `<project>/lib/server/index.mjs` and the
|
|
40
|
-
// process dies with MODULE_NOT_FOUND while still advertising the URL.
|
|
41
|
-
|
|
42
23
|
const INSTALL_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
24
|
|
|
44
|
-
// pruneStashDir and verifyTelemetryKeys now live in lib/services/telemetry-backend.mjs.
|
|
45
|
-
// Re-exported below at module bottom for tests that already import from here.
|
|
46
|
-
|
|
47
|
-
|
|
48
25
|
function runtimeStateDir(homeDir = os.homedir()) {
|
|
49
26
|
return path.join(homeDir, '.construct', 'runtime');
|
|
50
27
|
}
|
|
@@ -118,17 +95,8 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
|
|
|
118
95
|
}
|
|
119
96
|
|
|
120
97
|
export function isManagedConstructPostgresUrl(databaseUrl = '') {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const parsed = new URL(databaseUrl);
|
|
124
|
-
const host = parsed.hostname;
|
|
125
|
-
return (
|
|
126
|
-
parsed.port === String(CONSTRUCT_PG_PORT) &&
|
|
127
|
-
['127.0.0.1', 'localhost', '::1'].includes(host)
|
|
128
|
-
) || host === CONSTRUCT_PG_CONTAINER;
|
|
129
|
-
} catch {
|
|
130
|
-
return databaseUrl.includes(`:${CONSTRUCT_PG_PORT}/`);
|
|
131
|
-
}
|
|
98
|
+
// Construct no longer manages local Postgres containers.
|
|
99
|
+
return false;
|
|
132
100
|
}
|
|
133
101
|
|
|
134
102
|
function processExists(pid) {
|
|
@@ -296,11 +264,6 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
|
|
|
296
264
|
});
|
|
297
265
|
child.unref();
|
|
298
266
|
|
|
299
|
-
// Confirm the process actually bound the port before persisting state or
|
|
300
|
-
// advertising the URL. Without this the caller reports a dead PID/URL when the
|
|
301
|
-
// server crashes on boot — the readiness gate turns a silent failure into an
|
|
302
|
-
// honest, actionable one (the log tail names the cause).
|
|
303
|
-
|
|
304
267
|
let ready = false;
|
|
305
268
|
for (let i = 0; i < 30; i += 1) {
|
|
306
269
|
if (await probeRuntimePort(port)) { ready = true; break; }
|
|
@@ -324,10 +287,6 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
|
|
|
324
287
|
return { started: true, reused: false, ...state };
|
|
325
288
|
}
|
|
326
289
|
|
|
327
|
-
/**
|
|
328
|
-
* Resolve runtime ports. Re-uses ports from config.env when the service is
|
|
329
|
-
* already listening there (avoids port drift on repeated `construct up`).
|
|
330
|
-
*/
|
|
331
290
|
export async function getRuntimePorts(homeDir = os.homedir(), {
|
|
332
291
|
dashboardProbeFn = isDashboardRunning,
|
|
333
292
|
memoryProbeFn = isMemoryRunning,
|
|
@@ -363,118 +322,12 @@ export async function describeRuntimeSupport() {
|
|
|
363
322
|
}
|
|
364
323
|
return {
|
|
365
324
|
tmux: commandExists('tmux'),
|
|
366
|
-
docker: commandExists('docker'),
|
|
367
325
|
cm: commandExists('cm'),
|
|
368
326
|
opencode: commandExists('opencode'),
|
|
369
327
|
};
|
|
370
328
|
}
|
|
371
329
|
|
|
372
|
-
// ── Construct Postgres management ──────────────────────────────────────────
|
|
373
|
-
|
|
374
|
-
function constructPgComposePath(rootDir) {
|
|
375
|
-
return path.join(rootDir, CONSTRUCT_PG_COMPOSE_DIR, 'docker-compose.yml');
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function isConstructPostgresRunning(spawnSyncFn = spawnSync) {
|
|
379
|
-
const result = spawnSyncFn('docker', ['inspect', '--format', '{{.State.Running}}', CONSTRUCT_PG_CONTAINER], {
|
|
380
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
381
|
-
});
|
|
382
|
-
return result.stdout?.trim() === 'true';
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function isConstructPostgresHealthy(spawnSyncFn = spawnSync) {
|
|
386
|
-
const result = spawnSyncFn('docker', ['exec', CONSTRUCT_PG_CONTAINER, 'pg_isready', '-U', 'construct'], {
|
|
387
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
388
|
-
});
|
|
389
|
-
return result.status === 0;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async function waitForConstructPostgresHealthy({
|
|
393
|
-
spawnSyncFn = spawnSync,
|
|
394
|
-
maxRetries = CONSTRUCT_PG_HEALTH_RETRIES,
|
|
395
|
-
intervalMs = CONSTRUCT_PG_HEALTH_INTERVAL_MS,
|
|
396
|
-
} = {}) {
|
|
397
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
398
|
-
if (isConstructPostgresHealthy(spawnSyncFn)) return true;
|
|
399
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
400
|
-
}
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function startConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
|
|
405
|
-
const composeRunner = detectDockerComposeFn({ autoStart: true });
|
|
406
|
-
if (!composeRunner) return { status: 'unavailable', note: 'Docker not available' };
|
|
407
|
-
|
|
408
|
-
const composeFile = constructPgComposePath(rootDir);
|
|
409
|
-
if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct init first' };
|
|
410
|
-
|
|
411
|
-
const args = [...composeRunner.argsPrefix, '-p', CONSTRUCT_PG_CONTAINER, '-f', composeFile, 'up', '-d'];
|
|
412
|
-
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
413
|
-
if (r.status === 0) return { status: 'started' };
|
|
414
|
-
return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose up failed' };
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function stopConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
|
|
418
|
-
const composeRunner = detectDockerComposeFn();
|
|
419
|
-
if (!composeRunner) return { status: 'skipped', note: 'Docker not available' };
|
|
420
|
-
|
|
421
|
-
const composeFile = constructPgComposePath(rootDir);
|
|
422
|
-
if (!fs.existsSync(composeFile)) return { status: 'skipped', note: 'no compose file' };
|
|
423
|
-
|
|
424
|
-
const args = [...composeRunner.argsPrefix, '-p', CONSTRUCT_PG_CONTAINER, '-f', composeFile, 'down'];
|
|
425
|
-
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
426
|
-
if (r.status === 0) return { status: 'stopped' };
|
|
427
|
-
return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose down failed' };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function checkPgvectorEnabled(spawnSyncFn = spawnSync) {
|
|
431
|
-
const result = spawnSyncFn('docker', [
|
|
432
|
-
'exec', CONSTRUCT_PG_CONTAINER, 'psql', '-U', 'construct', '-d', 'construct',
|
|
433
|
-
'-tAc', "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector')",
|
|
434
|
-
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
435
|
-
return result.stdout?.trim() === 't';
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* On macOS, when the Docker CLI is installed but the daemon is down,
|
|
440
|
-
* launch Docker Desktop and poll `docker info` up to `timeoutMs` for
|
|
441
|
-
* the daemon to come up. Opt-out via `CONSTRUCT_DISABLE_DOCKER_AUTOSTART=1`
|
|
442
|
-
* so users who keep Docker off aren't forced into it. No-ops on Linux
|
|
443
|
-
* (systemd needs sudo) and Windows (different launcher).
|
|
444
|
-
*/
|
|
445
|
-
async function tryStartDockerDaemon({
|
|
446
|
-
env = process.env,
|
|
447
|
-
spawnSyncFn = spawnSync,
|
|
448
|
-
timeoutMs = 60_000,
|
|
449
|
-
pollMs = 1500,
|
|
450
|
-
} = {}) {
|
|
451
|
-
if (env.CONSTRUCT_DISABLE_DOCKER_AUTOSTART === '1') return false;
|
|
452
|
-
if (process.platform !== 'darwin') return false;
|
|
453
|
-
|
|
454
|
-
// CLI must be present; without it, nothing to start.
|
|
455
|
-
if (spawnSyncFn('command -v docker', { stdio: 'ignore', shell: true }).status !== 0) return false;
|
|
456
|
-
|
|
457
|
-
// If the daemon is already up, no work needed.
|
|
458
|
-
if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
|
|
459
|
-
|
|
460
|
-
// Best-effort launch. `open -a Docker` returns immediately; the daemon
|
|
461
|
-
// takes 5-30s to come fully up so we poll.
|
|
462
|
-
spawnSyncFn('open', ['-a', 'Docker'], { stdio: 'ignore' });
|
|
463
|
-
|
|
464
|
-
const deadline = Date.now() + timeoutMs;
|
|
465
|
-
while (Date.now() < deadline) {
|
|
466
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
467
|
-
if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
|
|
468
|
-
}
|
|
469
|
-
return false;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// The optional services a user can pick from with `construct dev --select` /
|
|
473
|
-
// `--only=`. Pressure Guard (cleanup) and Doctor (the L0 supervisor daemon)
|
|
474
|
-
// are infrastructure, always on — they are intentionally not selectable.
|
|
475
|
-
|
|
476
330
|
export const SELECTABLE_SERVICES = Object.freeze([
|
|
477
|
-
{ key: 'postgres', label: 'Postgres', description: 'Managed pgvector container (or external DATABASE_URL).' },
|
|
478
331
|
{ key: 'dashboard', label: 'Dashboard', description: 'Local operations dashboard on http://127.0.0.1:4242.' },
|
|
479
332
|
{ key: 'telemetry', label: 'Telemetry', description: 'Trace export / local JSONL traces.' },
|
|
480
333
|
{ key: 'memory', label: 'Memory (cm)', description: 'Persistent memory service (cm).' },
|
|
@@ -488,36 +341,24 @@ export async function startServices({
|
|
|
488
341
|
describeRuntimeSupportFn = describeRuntimeSupport,
|
|
489
342
|
getRuntimePortsFn = getRuntimePorts,
|
|
490
343
|
startDashboardFn = startDashboard,
|
|
491
|
-
detectDockerComposeFn = detectDockerCompose,
|
|
492
344
|
loadConstructEnvFn = loadConstructEnv,
|
|
493
345
|
spawnDetachedFn = spawnDetached,
|
|
494
|
-
verifyTelemetryKeysFn = verifyTelemetryKeys,
|
|
495
346
|
memoryProbeFn = isMemoryRunning,
|
|
496
347
|
openCodeProbeFn = isOpenCodeRunning,
|
|
497
348
|
runPressureReleaseFn = runPressureRelease,
|
|
498
|
-
tryStartDockerDaemonFn = tryStartDockerDaemon,
|
|
499
349
|
} = {}) {
|
|
500
350
|
const support = await describeRuntimeSupportFn();
|
|
501
351
|
const ports = await getRuntimePortsFn(homeDir);
|
|
502
352
|
const envPath = getUserEnvPath(homeDir);
|
|
503
353
|
|
|
504
|
-
// A null selection means "start everything" (the default). A Set restricts
|
|
505
|
-
// startup to the chosen optional services; always-on infrastructure ignores it.
|
|
506
|
-
|
|
507
354
|
const wants = (key) => selected === null || selected.has(key);
|
|
508
355
|
|
|
509
|
-
// On macOS, auto-launch Docker Desktop when the CLI is present but the
|
|
510
|
-
// daemon is down. Polls up to 60s so downstream Postgres startup sees a working
|
|
511
|
-
// compose runner instead of "Docker not available".
|
|
512
|
-
await tryStartDockerDaemonFn({ env: { ...process.env, HOME: homeDir }, timeoutMs: 5000 });
|
|
513
|
-
|
|
514
356
|
writeEnvValues(envPath, {
|
|
515
357
|
DASHBOARD_PORT: String(ports.dashboard),
|
|
516
358
|
MEMORY_PORT: String(ports.memory),
|
|
517
359
|
BRIDGE_PORT: String(ports.bridge),
|
|
518
360
|
});
|
|
519
361
|
|
|
520
|
-
// Construct Postgres — start if DATABASE_URL points to managed container
|
|
521
362
|
const liveEnv = loadConstructEnvFn({ rootDir, homeDir });
|
|
522
363
|
const results = [];
|
|
523
364
|
const pressureReport = runPressureReleaseFn({ env: { ...liveEnv, HOME: homeDir } });
|
|
@@ -529,40 +370,6 @@ export async function startServices({
|
|
|
529
370
|
note: `terminated ${pressureReport.killed.length} stale process(es)`,
|
|
530
371
|
});
|
|
531
372
|
}
|
|
532
|
-
const databaseUrl = liveEnv.DATABASE_URL || '';
|
|
533
|
-
const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
|
|
534
|
-
|
|
535
|
-
if (wants('postgres') && usesManagedPostgres) {
|
|
536
|
-
if (!isConstructPostgresRunning()) {
|
|
537
|
-
const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
|
|
538
|
-
if (pgStart.status === 'started') {
|
|
539
|
-
const healthy = await waitForConstructPostgresHealthy();
|
|
540
|
-
if (healthy) {
|
|
541
|
-
const pgvector = checkPgvectorEnabled();
|
|
542
|
-
results.push({
|
|
543
|
-
name: 'Postgres',
|
|
544
|
-
url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
|
|
545
|
-
status: pgvector ? 'started' : 'degraded',
|
|
546
|
-
note: pgvector ? 'pgvector enabled' : 'pgvector not installed — semantic search unavailable',
|
|
547
|
-
});
|
|
548
|
-
} else {
|
|
549
|
-
results.push({ name: 'Postgres', status: 'degraded', note: 'container started but health check timed out' });
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
results.push({ name: 'Postgres', status: 'error', note: pgStart.note });
|
|
553
|
-
}
|
|
554
|
-
} else {
|
|
555
|
-
const pgvector = checkPgvectorEnabled();
|
|
556
|
-
results.push({
|
|
557
|
-
name: 'Postgres',
|
|
558
|
-
url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
|
|
559
|
-
status: 'reused',
|
|
560
|
-
note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
} else if (wants('postgres') && databaseUrl) {
|
|
564
|
-
results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
|
|
565
|
-
}
|
|
566
373
|
|
|
567
374
|
// Dashboard
|
|
568
375
|
if (wants('dashboard')) {
|
|
@@ -575,38 +382,38 @@ export async function startServices({
|
|
|
575
382
|
const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
|
|
576
383
|
const traceBackend = resolveTraceBackend(liveEnv);
|
|
577
384
|
if (wants('telemetry')) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
385
|
+
if (traceBackend === 'local') {
|
|
386
|
+
results.push({
|
|
387
|
+
name: 'Telemetry',
|
|
388
|
+
url: path.join(rootDir, '.cx', 'traces'),
|
|
389
|
+
status: 'configured',
|
|
390
|
+
note: 'local JSONL traces; remote export not configured',
|
|
391
|
+
});
|
|
392
|
+
} else if (traceBackend === 'none') {
|
|
393
|
+
results.push({
|
|
394
|
+
name: 'Telemetry',
|
|
395
|
+
url: path.join(rootDir, '.cx', 'traces'),
|
|
396
|
+
status: 'configured',
|
|
397
|
+
note: 'remote export disabled; local JSONL traces preserved',
|
|
398
|
+
});
|
|
399
|
+
} else if (traceBackend === 'otel') {
|
|
400
|
+
const endpoint = liveEnv.CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT || '';
|
|
401
|
+
results.push({
|
|
402
|
+
name: 'Telemetry',
|
|
403
|
+
url: endpoint || undefined,
|
|
404
|
+
status: endpoint ? 'configured' : 'unavailable',
|
|
405
|
+
note: endpoint ? `OTLP export (${telemetryProviderLabel(liveEnv)})` : 'CONSTRUCT_OTEL_EXPORTER_OTLP_ENDPOINT not set',
|
|
406
|
+
});
|
|
407
|
+
} else if (telemetryUrl && (traceBackend === 'langfuse' || traceBackend === 'http')) {
|
|
408
|
+
results.push({
|
|
409
|
+
name: 'Telemetry',
|
|
410
|
+
url: telemetryUrl,
|
|
411
|
+
status: 'configured',
|
|
412
|
+
note: `${telemetryProviderLabel(liveEnv)} export — ${telemetryUrl}`,
|
|
413
|
+
});
|
|
414
|
+
} else {
|
|
415
|
+
results.push({ name: 'Telemetry', status: 'unavailable', note: 'remote export requested but endpoint is not configured' });
|
|
416
|
+
}
|
|
610
417
|
}
|
|
611
418
|
|
|
612
419
|
// Memory (cm)
|
|
@@ -650,9 +457,6 @@ export async function startServices({
|
|
|
650
457
|
};
|
|
651
458
|
}
|
|
652
459
|
|
|
653
|
-
// Surface service probe failures into the role-framework event bus so cx-sre
|
|
654
|
-
// can be invoked when a critical service is down. Non-blocking, best effort.
|
|
655
|
-
|
|
656
460
|
async function emitServiceFailures(results) {
|
|
657
461
|
if (process.env.CONSTRUCT_ROLES === 'off') return;
|
|
658
462
|
const failed = results.filter((r) => r.status === 'error');
|
|
@@ -670,17 +474,9 @@ async function emitServiceFailures(results) {
|
|
|
670
474
|
} catch { /* best effort */ }
|
|
671
475
|
}
|
|
672
476
|
|
|
673
|
-
// Exported for testing only
|
|
674
|
-
export { verifyTelemetryKeys as _verifyTelemetryKeys, pruneStashDir as _pruneStashDir };
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Kill every process listening on a given TCP port (best-effort, non-fatal).
|
|
678
|
-
* Returns true if at least one PID was killed.
|
|
679
|
-
*/
|
|
680
477
|
function killPortOwners(port, spawnSyncFn = spawnSync) {
|
|
681
478
|
if (!port || !Number.isInteger(port) || port <= 0) return false;
|
|
682
479
|
try {
|
|
683
|
-
// lsof works on macOS and Linux; -t returns just PIDs
|
|
684
480
|
const result = spawnSyncFn('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
|
|
685
481
|
const pids = (result.stdout || '').trim().split(/\s+/).filter(Boolean).map(Number).filter((n) => n > 0);
|
|
686
482
|
if (pids.length === 0) return false;
|
|
@@ -695,21 +491,10 @@ function killPortOwners(port, spawnSyncFn = spawnSync) {
|
|
|
695
491
|
|
|
696
492
|
export async function stopServices({
|
|
697
493
|
homeDir = os.homedir(),
|
|
698
|
-
rootDir,
|
|
699
494
|
spawnSyncFn = spawnSync,
|
|
700
|
-
detectDockerComposeFn = detectDockerCompose,
|
|
701
495
|
} = {}) {
|
|
702
496
|
const results = [];
|
|
703
497
|
|
|
704
|
-
// Stash construct Postgres data before any container is stopped so data
|
|
705
|
-
// survives Docker restarts and machine reboots.
|
|
706
|
-
const constructDbStash = stashConstructDb({ homeDir, spawnSyncFn });
|
|
707
|
-
|
|
708
|
-
// Construct Postgres
|
|
709
|
-
const pgStop = stopConstructPostgres({ rootDir, homeDir, spawnSyncFn, detectDockerComposeFn });
|
|
710
|
-
results.push({ name: 'Postgres', status: pgStop.status });
|
|
711
|
-
|
|
712
|
-
// Doctor (L0 daemon)
|
|
713
498
|
const doctor = stopDoctor(homeDir);
|
|
714
499
|
if (doctor.stopped) {
|
|
715
500
|
results.push({ name: 'Doctor', status: 'stopped', note: `pid ${doctor.pid}` });
|
|
@@ -717,7 +502,6 @@ export async function stopServices({
|
|
|
717
502
|
results.push({ name: 'Doctor', status: 'cleaned', note: 'stale state file removed' });
|
|
718
503
|
}
|
|
719
504
|
|
|
720
|
-
// Dashboard
|
|
721
505
|
const dashboard = stopDashboard(homeDir);
|
|
722
506
|
if (dashboard.stopped) {
|
|
723
507
|
results.push({ name: 'Dashboard', status: 'stopped', note: `pid ${dashboard.pid}, port ${dashboard.port}` });
|
|
@@ -727,18 +511,16 @@ export async function stopServices({
|
|
|
727
511
|
results.push({ name: 'Dashboard', status: 'not-running' });
|
|
728
512
|
}
|
|
729
513
|
|
|
730
|
-
// Memory (cm) — find port from config.env or fall back to default
|
|
731
514
|
const envPath = getUserEnvPath(homeDir);
|
|
732
515
|
const envValues = parseEnvFile(envPath);
|
|
733
516
|
const memoryPort = Number(envValues.MEMORY_PORT) || derivedMemoryPort();
|
|
734
517
|
const cmKilled = killPortOwners(memoryPort, spawnSyncFn);
|
|
735
518
|
results.push({ name: 'Memory (cm)', status: cmKilled ? 'stopped' : 'not-running' });
|
|
736
519
|
|
|
737
|
-
// OpenCode — find port from config.env or fall back to default
|
|
738
520
|
const bridgePort = Number(envValues.BRIDGE_PORT) || 5173;
|
|
739
521
|
const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn);
|
|
740
522
|
results.push({ name: 'OpenCode', status: openCodeKilled ? 'stopped' : 'not-running' });
|
|
741
523
|
|
|
742
524
|
const stopped = results.filter((r) => r.status === 'stopped' || r.status === 'cleaned').map((r) => r.name);
|
|
743
|
-
return { stopped, results
|
|
525
|
+
return { stopped, results };
|
|
744
526
|
}
|