@geraldmaron/construct 1.0.21 → 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 +23 -7
- package/bin/construct +206 -228
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +31 -10
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +3 -3
- package/lib/mcp-manager.mjs +59 -3
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +39 -163
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/runtime.mjs +72 -10
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +169 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +59 -252
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +104 -480
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -43
- 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/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +7 -6
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -11
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +427 -107
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +3 -12
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +28 -36
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +15 -15
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- 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/server/static/index.html +0 -1
- 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/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';
|
|
@@ -10,30 +10,17 @@ import fs from 'node:fs';
|
|
|
10
10
|
import net from 'node:net';
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
13
14
|
|
|
14
15
|
import { findAvailablePort } from './host-capabilities.mjs';
|
|
15
16
|
import { getUserEnvPath, loadConstructEnv, parseEnvFile, writeEnvValues } from './env-config.mjs';
|
|
16
|
-
import { detectDockerCompose } from './setup.mjs';
|
|
17
|
-
import { stashConstructDb, restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
18
17
|
import { runPressureRelease } from './runtime-pressure.mjs';
|
|
19
|
-
import {
|
|
20
|
-
pruneStashDir,
|
|
21
|
-
verifyTelemetryKeys,
|
|
22
|
-
isRemoteTelemetry,
|
|
23
|
-
} from './services/local-postgres.mjs';
|
|
24
18
|
import { resolveTraceBackend, telemetryProviderLabel } from './telemetry/client.mjs';
|
|
25
|
-
|
|
26
|
-
const CONSTRUCT_PG_COMPOSE_DIR = 'services/postgres';
|
|
27
|
-
const CONSTRUCT_PG_CONTAINER = 'construct-postgres';
|
|
28
|
-
const CONSTRUCT_PG_PORT = 54329;
|
|
29
|
-
const CONSTRUCT_PG_HEALTH_RETRIES = 12;
|
|
30
|
-
const CONSTRUCT_PG_HEALTH_INTERVAL_MS = 2000;
|
|
19
|
+
import { memoryPort as derivedMemoryPort } from './home-namespace.mjs';
|
|
31
20
|
|
|
32
21
|
const DASHBOARD_STATE_FILE = 'dashboard.json';
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
// Re-exported below at module bottom for tests that already import from here.
|
|
36
|
-
|
|
23
|
+
const INSTALL_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
37
24
|
|
|
38
25
|
function runtimeStateDir(homeDir = os.homedir()) {
|
|
39
26
|
return path.join(homeDir, '.construct', 'runtime');
|
|
@@ -108,17 +95,8 @@ export function buildRuntimeRecoverySummary({ rootDir, homeDir = os.homedir(), r
|
|
|
108
95
|
}
|
|
109
96
|
|
|
110
97
|
export function isManagedConstructPostgresUrl(databaseUrl = '') {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const parsed = new URL(databaseUrl);
|
|
114
|
-
const host = parsed.hostname;
|
|
115
|
-
return (
|
|
116
|
-
parsed.port === String(CONSTRUCT_PG_PORT) &&
|
|
117
|
-
['127.0.0.1', 'localhost', '::1'].includes(host)
|
|
118
|
-
) || host === CONSTRUCT_PG_CONTAINER;
|
|
119
|
-
} catch {
|
|
120
|
-
return databaseUrl.includes(`:${CONSTRUCT_PG_PORT}/`);
|
|
121
|
-
}
|
|
98
|
+
// Construct no longer manages local Postgres containers.
|
|
99
|
+
return false;
|
|
122
100
|
}
|
|
123
101
|
|
|
124
102
|
function processExists(pid) {
|
|
@@ -260,7 +238,7 @@ export function startDoctor({ rootDir, homeDir = os.homedir() } = {}) {
|
|
|
260
238
|
}
|
|
261
239
|
const existing = readDoctorState(homeDir);
|
|
262
240
|
if (existing) return { started: false, reused: true, pid: existing.pid };
|
|
263
|
-
const indexPath = path.join(
|
|
241
|
+
const indexPath = path.join(INSTALL_ROOT, 'lib', 'doctor', 'index.mjs');
|
|
264
242
|
if (!fs.existsSync(indexPath)) return { started: false, reason: 'missing-binary' };
|
|
265
243
|
const { logPath } = spawnDetached('node', [indexPath], homeDir, 'doctor.log', { cwd: rootDir, env: { ...process.env } });
|
|
266
244
|
return { started: true, logPath };
|
|
@@ -279,13 +257,25 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
|
|
|
279
257
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
280
258
|
const out = fs.openSync(outPath, 'a');
|
|
281
259
|
const err = fs.openSync(outPath, 'a');
|
|
282
|
-
const child = spawn(process.execPath, [path.join(
|
|
260
|
+
const child = spawn(process.execPath, [path.join(INSTALL_ROOT, 'lib', 'server', 'index.mjs')], {
|
|
283
261
|
detached: true,
|
|
284
262
|
stdio: ['ignore', out, err],
|
|
285
263
|
env: { ...process.env, PORT: String(port) },
|
|
286
264
|
});
|
|
287
265
|
child.unref();
|
|
288
266
|
|
|
267
|
+
let ready = false;
|
|
268
|
+
for (let i = 0; i < 30; i += 1) {
|
|
269
|
+
if (await probeRuntimePort(port)) { ready = true; break; }
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!ready) {
|
|
274
|
+
let logTail = '';
|
|
275
|
+
try { logTail = fs.readFileSync(outPath, 'utf8').split('\n').slice(-12).join('\n'); } catch { /* log unreadable */ }
|
|
276
|
+
return { started: false, failed: true, port, logPath: outPath, error: `dashboard did not bind 127.0.0.1:${port} within 6s`, logTail };
|
|
277
|
+
}
|
|
278
|
+
|
|
289
279
|
const state = {
|
|
290
280
|
pid: child.pid,
|
|
291
281
|
port,
|
|
@@ -297,10 +287,6 @@ export async function startDashboard({ rootDir, homeDir = os.homedir(), preferre
|
|
|
297
287
|
return { started: true, reused: false, ...state };
|
|
298
288
|
}
|
|
299
289
|
|
|
300
|
-
/**
|
|
301
|
-
* Resolve runtime ports. Re-uses ports from config.env when the service is
|
|
302
|
-
* already listening there (avoids port drift on repeated `construct up`).
|
|
303
|
-
*/
|
|
304
290
|
export async function getRuntimePorts(homeDir = os.homedir(), {
|
|
305
291
|
dashboardProbeFn = isDashboardRunning,
|
|
306
292
|
memoryProbeFn = isMemoryRunning,
|
|
@@ -320,7 +306,7 @@ export async function getRuntimePorts(homeDir = os.homedir(), {
|
|
|
320
306
|
|
|
321
307
|
return {
|
|
322
308
|
dashboard: dashboard?.port ?? await resolvePort('DASHBOARD_PORT', 4242, dashboardProbeFn),
|
|
323
|
-
memory: await resolvePort('MEMORY_PORT',
|
|
309
|
+
memory: await resolvePort('MEMORY_PORT', derivedMemoryPort(), memoryProbeFn),
|
|
324
310
|
bridge: await resolvePort('BRIDGE_PORT', 5173, openCodeProbeFn),
|
|
325
311
|
};
|
|
326
312
|
}
|
|
@@ -336,118 +322,12 @@ export async function describeRuntimeSupport() {
|
|
|
336
322
|
}
|
|
337
323
|
return {
|
|
338
324
|
tmux: commandExists('tmux'),
|
|
339
|
-
docker: commandExists('docker'),
|
|
340
325
|
cm: commandExists('cm'),
|
|
341
326
|
opencode: commandExists('opencode'),
|
|
342
327
|
};
|
|
343
328
|
}
|
|
344
329
|
|
|
345
|
-
// ── Construct Postgres management ──────────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
function constructPgComposePath(rootDir) {
|
|
348
|
-
return path.join(rootDir, CONSTRUCT_PG_COMPOSE_DIR, 'docker-compose.yml');
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function isConstructPostgresRunning(spawnSyncFn = spawnSync) {
|
|
352
|
-
const result = spawnSyncFn('docker', ['inspect', '--format', '{{.State.Running}}', CONSTRUCT_PG_CONTAINER], {
|
|
353
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
354
|
-
});
|
|
355
|
-
return result.stdout?.trim() === 'true';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function isConstructPostgresHealthy(spawnSyncFn = spawnSync) {
|
|
359
|
-
const result = spawnSyncFn('docker', ['exec', CONSTRUCT_PG_CONTAINER, 'pg_isready', '-U', 'construct'], {
|
|
360
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
361
|
-
});
|
|
362
|
-
return result.status === 0;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
async function waitForConstructPostgresHealthy({
|
|
366
|
-
spawnSyncFn = spawnSync,
|
|
367
|
-
maxRetries = CONSTRUCT_PG_HEALTH_RETRIES,
|
|
368
|
-
intervalMs = CONSTRUCT_PG_HEALTH_INTERVAL_MS,
|
|
369
|
-
} = {}) {
|
|
370
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
371
|
-
if (isConstructPostgresHealthy(spawnSyncFn)) return true;
|
|
372
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
373
|
-
}
|
|
374
|
-
return false;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function startConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
|
|
378
|
-
const composeRunner = detectDockerComposeFn();
|
|
379
|
-
if (!composeRunner) return { status: 'unavailable', note: 'Docker not available' };
|
|
380
|
-
|
|
381
|
-
const composeFile = constructPgComposePath(rootDir);
|
|
382
|
-
if (!fs.existsSync(composeFile)) return { status: 'unavailable', note: 'Postgres compose file not found — run construct init first' };
|
|
383
|
-
|
|
384
|
-
const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'up', '-d'];
|
|
385
|
-
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
386
|
-
if (r.status === 0) return { status: 'started' };
|
|
387
|
-
return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose up failed' };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function stopConstructPostgres({ rootDir, homeDir = os.homedir(), spawnSyncFn = spawnSync, detectDockerComposeFn = detectDockerCompose } = {}) {
|
|
391
|
-
const composeRunner = detectDockerComposeFn();
|
|
392
|
-
if (!composeRunner) return { status: 'skipped', note: 'Docker not available' };
|
|
393
|
-
|
|
394
|
-
const composeFile = constructPgComposePath(rootDir);
|
|
395
|
-
if (!fs.existsSync(composeFile)) return { status: 'skipped', note: 'no compose file' };
|
|
396
|
-
|
|
397
|
-
const args = [...composeRunner.argsPrefix, '-p', 'construct-postgres', '-f', composeFile, 'down'];
|
|
398
|
-
const r = spawnSyncFn(composeRunner.command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
399
|
-
if (r.status === 0) return { status: 'stopped' };
|
|
400
|
-
return { status: 'error', note: (r.stderr || '').trim().split('\n')[0] || 'compose down failed' };
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function checkPgvectorEnabled(spawnSyncFn = spawnSync) {
|
|
404
|
-
const result = spawnSyncFn('docker', [
|
|
405
|
-
'exec', CONSTRUCT_PG_CONTAINER, 'psql', '-U', 'construct', '-d', 'construct',
|
|
406
|
-
'-tAc', "SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'vector')",
|
|
407
|
-
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
408
|
-
return result.stdout?.trim() === 't';
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* On macOS, when the Docker CLI is installed but the daemon is down,
|
|
413
|
-
* launch Docker Desktop and poll `docker info` up to `timeoutMs` for
|
|
414
|
-
* the daemon to come up. Opt-out via `CONSTRUCT_DISABLE_DOCKER_AUTOSTART=1`
|
|
415
|
-
* so users who keep Docker off aren't forced into it. No-ops on Linux
|
|
416
|
-
* (systemd needs sudo) and Windows (different launcher).
|
|
417
|
-
*/
|
|
418
|
-
async function tryStartDockerDaemon({
|
|
419
|
-
env = process.env,
|
|
420
|
-
spawnSyncFn = spawnSync,
|
|
421
|
-
timeoutMs = 60_000,
|
|
422
|
-
pollMs = 1500,
|
|
423
|
-
} = {}) {
|
|
424
|
-
if (env.CONSTRUCT_DISABLE_DOCKER_AUTOSTART === '1') return false;
|
|
425
|
-
if (process.platform !== 'darwin') return false;
|
|
426
|
-
|
|
427
|
-
// CLI must be present; without it, nothing to start.
|
|
428
|
-
if (spawnSyncFn('command -v docker', { stdio: 'ignore', shell: true }).status !== 0) return false;
|
|
429
|
-
|
|
430
|
-
// If the daemon is already up, no work needed.
|
|
431
|
-
if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
|
|
432
|
-
|
|
433
|
-
// Best-effort launch. `open -a Docker` returns immediately; the daemon
|
|
434
|
-
// takes 5-30s to come fully up so we poll.
|
|
435
|
-
spawnSyncFn('open', ['-a', 'Docker'], { stdio: 'ignore' });
|
|
436
|
-
|
|
437
|
-
const deadline = Date.now() + timeoutMs;
|
|
438
|
-
while (Date.now() < deadline) {
|
|
439
|
-
await new Promise((r) => setTimeout(r, pollMs));
|
|
440
|
-
if (spawnSyncFn('docker', ['info'], { stdio: 'ignore' }).status === 0) return true;
|
|
441
|
-
}
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// The optional services a user can pick from with `construct dev --select` /
|
|
446
|
-
// `--only=`. Pressure Guard (cleanup) and Doctor (the L0 supervisor daemon)
|
|
447
|
-
// are infrastructure, always on — they are intentionally not selectable.
|
|
448
|
-
|
|
449
330
|
export const SELECTABLE_SERVICES = Object.freeze([
|
|
450
|
-
{ key: 'postgres', label: 'Postgres', description: 'Managed pgvector container (or external DATABASE_URL).' },
|
|
451
331
|
{ key: 'dashboard', label: 'Dashboard', description: 'Local operations dashboard on http://127.0.0.1:4242.' },
|
|
452
332
|
{ key: 'telemetry', label: 'Telemetry', description: 'Trace export / local JSONL traces.' },
|
|
453
333
|
{ key: 'memory', label: 'Memory (cm)', description: 'Persistent memory service (cm).' },
|
|
@@ -461,36 +341,24 @@ export async function startServices({
|
|
|
461
341
|
describeRuntimeSupportFn = describeRuntimeSupport,
|
|
462
342
|
getRuntimePortsFn = getRuntimePorts,
|
|
463
343
|
startDashboardFn = startDashboard,
|
|
464
|
-
detectDockerComposeFn = detectDockerCompose,
|
|
465
344
|
loadConstructEnvFn = loadConstructEnv,
|
|
466
345
|
spawnDetachedFn = spawnDetached,
|
|
467
|
-
verifyTelemetryKeysFn = verifyTelemetryKeys,
|
|
468
346
|
memoryProbeFn = isMemoryRunning,
|
|
469
347
|
openCodeProbeFn = isOpenCodeRunning,
|
|
470
348
|
runPressureReleaseFn = runPressureRelease,
|
|
471
|
-
tryStartDockerDaemonFn = tryStartDockerDaemon,
|
|
472
349
|
} = {}) {
|
|
473
350
|
const support = await describeRuntimeSupportFn();
|
|
474
351
|
const ports = await getRuntimePortsFn(homeDir);
|
|
475
352
|
const envPath = getUserEnvPath(homeDir);
|
|
476
353
|
|
|
477
|
-
// A null selection means "start everything" (the default). A Set restricts
|
|
478
|
-
// startup to the chosen optional services; always-on infrastructure ignores it.
|
|
479
|
-
|
|
480
354
|
const wants = (key) => selected === null || selected.has(key);
|
|
481
355
|
|
|
482
|
-
// On macOS, auto-launch Docker Desktop when the CLI is present but the
|
|
483
|
-
// daemon is down. Polls up to 60s so downstream Postgres startup sees a working
|
|
484
|
-
// compose runner instead of "Docker not available".
|
|
485
|
-
await tryStartDockerDaemonFn({ env: { ...process.env, HOME: homeDir }, timeoutMs: 5000 });
|
|
486
|
-
|
|
487
356
|
writeEnvValues(envPath, {
|
|
488
357
|
DASHBOARD_PORT: String(ports.dashboard),
|
|
489
358
|
MEMORY_PORT: String(ports.memory),
|
|
490
359
|
BRIDGE_PORT: String(ports.bridge),
|
|
491
360
|
});
|
|
492
361
|
|
|
493
|
-
// Construct Postgres — start if DATABASE_URL points to managed container
|
|
494
362
|
const liveEnv = loadConstructEnvFn({ rootDir, homeDir });
|
|
495
363
|
const results = [];
|
|
496
364
|
const pressureReport = runPressureReleaseFn({ env: { ...liveEnv, HOME: homeDir } });
|
|
@@ -502,86 +370,50 @@ export async function startServices({
|
|
|
502
370
|
note: `terminated ${pressureReport.killed.length} stale process(es)`,
|
|
503
371
|
});
|
|
504
372
|
}
|
|
505
|
-
const databaseUrl = liveEnv.DATABASE_URL || '';
|
|
506
|
-
const usesManagedPostgres = isManagedConstructPostgresUrl(databaseUrl);
|
|
507
|
-
|
|
508
|
-
if (wants('postgres') && usesManagedPostgres) {
|
|
509
|
-
if (!isConstructPostgresRunning()) {
|
|
510
|
-
const pgStart = startConstructPostgres({ rootDir, homeDir, spawnSyncFn: spawnSync, detectDockerComposeFn: detectDockerComposeFn });
|
|
511
|
-
if (pgStart.status === 'started') {
|
|
512
|
-
const healthy = await waitForConstructPostgresHealthy();
|
|
513
|
-
if (healthy) {
|
|
514
|
-
const pgvector = checkPgvectorEnabled();
|
|
515
|
-
results.push({
|
|
516
|
-
name: 'Postgres',
|
|
517
|
-
url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
|
|
518
|
-
status: pgvector ? 'started' : 'degraded',
|
|
519
|
-
note: pgvector ? 'pgvector enabled' : 'pgvector not installed — semantic search unavailable',
|
|
520
|
-
});
|
|
521
|
-
} else {
|
|
522
|
-
results.push({ name: 'Postgres', status: 'degraded', note: 'container started but health check timed out' });
|
|
523
|
-
}
|
|
524
|
-
} else {
|
|
525
|
-
results.push({ name: 'Postgres', status: 'error', note: pgStart.note });
|
|
526
|
-
}
|
|
527
|
-
} else {
|
|
528
|
-
const pgvector = checkPgvectorEnabled();
|
|
529
|
-
results.push({
|
|
530
|
-
name: 'Postgres',
|
|
531
|
-
url: `postgresql://127.0.0.1:${CONSTRUCT_PG_PORT}/construct`,
|
|
532
|
-
status: 'reused',
|
|
533
|
-
note: pgvector ? 'pgvector enabled' : 'pgvector not installed',
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
} else if (wants('postgres') && databaseUrl) {
|
|
537
|
-
results.push({ name: 'Postgres', url: databaseUrl, status: 'configured', note: 'external database' });
|
|
538
|
-
}
|
|
539
373
|
|
|
540
374
|
// Dashboard
|
|
541
375
|
if (wants('dashboard')) {
|
|
542
376
|
const dashboard = await startDashboardFn({ rootDir, homeDir, preferredPort: ports.dashboard });
|
|
543
|
-
results.push(
|
|
544
|
-
name: 'Dashboard',
|
|
545
|
-
url: dashboard.url,
|
|
546
|
-
status: dashboard.reused ? 'reused' : 'started',
|
|
547
|
-
});
|
|
377
|
+
results.push(dashboard.failed
|
|
378
|
+
? { name: 'Dashboard', status: 'failed', note: `${dashboard.error} — see ${dashboard.logPath}` }
|
|
379
|
+
: { name: 'Dashboard', url: dashboard.url, status: dashboard.reused ? 'reused' : 'started' });
|
|
548
380
|
}
|
|
549
381
|
|
|
550
382
|
const telemetryUrl = liveEnv.CONSTRUCT_TELEMETRY_URL ?? '';
|
|
551
383
|
const traceBackend = resolveTraceBackend(liveEnv);
|
|
552
384
|
if (wants('telemetry')) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
}
|
|
585
417
|
}
|
|
586
418
|
|
|
587
419
|
// Memory (cm)
|
|
@@ -625,9 +457,6 @@ export async function startServices({
|
|
|
625
457
|
};
|
|
626
458
|
}
|
|
627
459
|
|
|
628
|
-
// Surface service probe failures into the role-framework event bus so cx-sre
|
|
629
|
-
// can be invoked when a critical service is down. Non-blocking, best effort.
|
|
630
|
-
|
|
631
460
|
async function emitServiceFailures(results) {
|
|
632
461
|
if (process.env.CONSTRUCT_ROLES === 'off') return;
|
|
633
462
|
const failed = results.filter((r) => r.status === 'error');
|
|
@@ -645,17 +474,9 @@ async function emitServiceFailures(results) {
|
|
|
645
474
|
} catch { /* best effort */ }
|
|
646
475
|
}
|
|
647
476
|
|
|
648
|
-
// Exported for testing only
|
|
649
|
-
export { verifyTelemetryKeys as _verifyTelemetryKeys, pruneStashDir as _pruneStashDir };
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Kill every process listening on a given TCP port (best-effort, non-fatal).
|
|
653
|
-
* Returns true if at least one PID was killed.
|
|
654
|
-
*/
|
|
655
477
|
function killPortOwners(port, spawnSyncFn = spawnSync) {
|
|
656
478
|
if (!port || !Number.isInteger(port) || port <= 0) return false;
|
|
657
479
|
try {
|
|
658
|
-
// lsof works on macOS and Linux; -t returns just PIDs
|
|
659
480
|
const result = spawnSyncFn('lsof', ['-t', `-i:${port}`], { encoding: 'utf8' });
|
|
660
481
|
const pids = (result.stdout || '').trim().split(/\s+/).filter(Boolean).map(Number).filter((n) => n > 0);
|
|
661
482
|
if (pids.length === 0) return false;
|
|
@@ -670,21 +491,10 @@ function killPortOwners(port, spawnSyncFn = spawnSync) {
|
|
|
670
491
|
|
|
671
492
|
export async function stopServices({
|
|
672
493
|
homeDir = os.homedir(),
|
|
673
|
-
rootDir,
|
|
674
494
|
spawnSyncFn = spawnSync,
|
|
675
|
-
detectDockerComposeFn = detectDockerCompose,
|
|
676
495
|
} = {}) {
|
|
677
496
|
const results = [];
|
|
678
497
|
|
|
679
|
-
// Stash construct Postgres data before any container is stopped so data
|
|
680
|
-
// survives Docker restarts and machine reboots.
|
|
681
|
-
const constructDbStash = stashConstructDb({ homeDir, spawnSyncFn });
|
|
682
|
-
|
|
683
|
-
// Construct Postgres
|
|
684
|
-
const pgStop = stopConstructPostgres({ rootDir, homeDir, spawnSyncFn, detectDockerComposeFn });
|
|
685
|
-
results.push({ name: 'Postgres', status: pgStop.status });
|
|
686
|
-
|
|
687
|
-
// Doctor (L0 daemon)
|
|
688
498
|
const doctor = stopDoctor(homeDir);
|
|
689
499
|
if (doctor.stopped) {
|
|
690
500
|
results.push({ name: 'Doctor', status: 'stopped', note: `pid ${doctor.pid}` });
|
|
@@ -692,7 +502,6 @@ export async function stopServices({
|
|
|
692
502
|
results.push({ name: 'Doctor', status: 'cleaned', note: 'stale state file removed' });
|
|
693
503
|
}
|
|
694
504
|
|
|
695
|
-
// Dashboard
|
|
696
505
|
const dashboard = stopDashboard(homeDir);
|
|
697
506
|
if (dashboard.stopped) {
|
|
698
507
|
results.push({ name: 'Dashboard', status: 'stopped', note: `pid ${dashboard.pid}, port ${dashboard.port}` });
|
|
@@ -702,18 +511,16 @@ export async function stopServices({
|
|
|
702
511
|
results.push({ name: 'Dashboard', status: 'not-running' });
|
|
703
512
|
}
|
|
704
513
|
|
|
705
|
-
// Memory (cm) — find port from config.env or fall back to default
|
|
706
514
|
const envPath = getUserEnvPath(homeDir);
|
|
707
515
|
const envValues = parseEnvFile(envPath);
|
|
708
|
-
const memoryPort = Number(envValues.MEMORY_PORT) ||
|
|
516
|
+
const memoryPort = Number(envValues.MEMORY_PORT) || derivedMemoryPort();
|
|
709
517
|
const cmKilled = killPortOwners(memoryPort, spawnSyncFn);
|
|
710
518
|
results.push({ name: 'Memory (cm)', status: cmKilled ? 'stopped' : 'not-running' });
|
|
711
519
|
|
|
712
|
-
// OpenCode — find port from config.env or fall back to default
|
|
713
520
|
const bridgePort = Number(envValues.BRIDGE_PORT) || 5173;
|
|
714
521
|
const openCodeKilled = killPortOwners(bridgePort, spawnSyncFn);
|
|
715
522
|
results.push({ name: 'OpenCode', status: openCodeKilled ? 'stopped' : 'not-running' });
|
|
716
523
|
|
|
717
524
|
const stopped = results.filter((r) => r.status === 'stopped' || r.status === 'cleaned').map((r) => r.name);
|
|
718
|
-
return { stopped, results
|
|
525
|
+
return { stopped, results };
|
|
719
526
|
}
|
package/lib/setup-prompts.mjs
CHANGED
|
@@ -29,6 +29,7 @@ export async function consentToInstall({
|
|
|
29
29
|
alreadyConfigured = false,
|
|
30
30
|
alreadyConfiguredNote = '',
|
|
31
31
|
envPath,
|
|
32
|
+
question = '',
|
|
32
33
|
defaultYes = true,
|
|
33
34
|
readlineModule = readline,
|
|
34
35
|
stdin = process.stdin,
|
|
@@ -64,7 +65,7 @@ export async function consentToInstall({
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const answer = await promptYesNo({
|
|
67
|
-
question: `Install local ${name} via Docker?`,
|
|
68
|
+
question: question || `Install local ${name} via Docker?`,
|
|
68
69
|
defaultYes,
|
|
69
70
|
readlineModule,
|
|
70
71
|
stdin,
|