@geraldmaron/construct 1.0.23 → 1.1.0
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 +189 -207
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- package/lib/beads-client.mjs +9 -0
- package/lib/beads-optimistic.mjs +23 -71
- package/lib/bridges/copilot-proxy.mjs +116 -0
- package/lib/cli-commands.mjs +5 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.mjs +1 -1
- package/lib/document-extract/docling-client.mjs +16 -6
- package/lib/document-extract/docling-sidecar.py +32 -2
- package/lib/document-extract.mjs +37 -10
- package/lib/document-ingest.mjs +90 -5
- package/lib/embed/inbox.mjs +6 -3
- package/lib/embed/recommendation-store.mjs +7 -289
- package/lib/embed/reconcile.mjs +2 -2
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- package/lib/hooks/config-protection.mjs +4 -4
- package/lib/hooks/session-reflect.mjs +5 -1
- package/lib/host-capabilities.mjs +30 -0
- package/lib/ingest/docling-remote.mjs +90 -0
- package/lib/ingest/strategy.mjs +1 -1
- package/lib/init-unified.mjs +9 -13
- package/lib/intake/git-queue.mjs +195 -0
- package/lib/intake/queue.mjs +9 -16
- package/lib/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp/tools/storage.mjs +2 -3
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +59 -3
- package/lib/observation-store.mjs +38 -166
- package/lib/ollama/capability-store.mjs +78 -0
- package/lib/ollama/provision-context.mjs +344 -0
- package/lib/opencode-config.mjs +148 -0
- package/lib/opencode-telemetry.mjs +7 -0
- package/lib/orchestration/runtime.mjs +3 -2
- package/lib/orchestration-policy.mjs +41 -6
- package/lib/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +12 -8
- package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
- package/lib/reconcile/index.mjs +0 -2
- package/lib/reflect/extractor.mjs +14 -1
- package/lib/reflect/salience.mjs +65 -0
- package/lib/rules-delivery.mjs +122 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -17
- package/lib/service-manager.mjs +79 -259
- package/lib/setup.mjs +44 -425
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/status.mjs +3 -6
- package/lib/storage/admin.mjs +48 -325
- package/lib/storage/backend.mjs +10 -57
- package/lib/storage/embeddings-engine.mjs +19 -5
- 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/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/lib/worker/entrypoint.mjs +6 -14
- package/package.json +6 -5
- package/personas/construct.md +1 -1
- package/platforms/capabilities.json +76 -0
- package/platforms/claude/settings.template.json +0 -7
- package/platforms/opencode/sync-config.mjs +121 -25
- package/rules/common/neurodivergent-output.md +1 -1
- package/rules/web/coding-style.md +8 -0
- package/rules/web/design-quality.md +8 -0
- package/rules/web/hooks.md +8 -0
- package/rules/web/patterns.md +8 -0
- package/rules/web/performance.md +8 -0
- package/rules/web/security.md +8 -0
- package/rules/web/testing.md +8 -0
- package/scripts/sync-specialists.mjs +174 -40
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-qa.md +1 -1
- package/specialists/prompts/cx-test-automation.md +12 -0
- 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/setup.mjs
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lib/setup.mjs — Machine-scoped first-run setup wizard for Construct.
|
|
4
4
|
*
|
|
5
|
-
* Installs cm/cass, configures managed defaults,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Installs cm/cass, configures managed defaults, pre-warms the embedding model,
|
|
6
|
+
* wires MCP integrations, and writes ~/.construct/config.env.
|
|
7
|
+
* Invoked by `construct install` and by lib/install/first-invocation.mjs
|
|
8
|
+
* when a TTY user is missing resources.
|
|
9
9
|
*
|
|
10
10
|
* Project-scoped scaffolding (`.cx/`, AGENTS.md, plan.md, adapters) lives
|
|
11
11
|
* in lib/init-unified.mjs — invoked by `construct init`. These two flows
|
|
12
12
|
* are intentionally separate: install runs once per machine, init runs
|
|
13
13
|
* once per repo.
|
|
14
|
+
*
|
|
15
|
+
* Construct now uses embedded LanceDB and Git-backed state, removing the
|
|
16
|
+
* dependency on local Docker-managed Postgres.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import fs from 'node:fs';
|
|
@@ -26,51 +29,37 @@ import {
|
|
|
26
29
|
getDeploymentMode,
|
|
27
30
|
} from './deployment-mode.mjs';
|
|
28
31
|
import { getCanonicalOpenCodeConfigPath, readOpenCodeConfig, writeOpenCodeConfig } from './opencode-config.mjs';
|
|
29
|
-
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
30
|
-
import { runMigrations } from './storage/migrations.mjs';
|
|
31
|
-
import { createSqlClient, closeSqlClient } from './storage/backend.mjs';
|
|
32
32
|
import { getEmbeddingModelInfo, warmupEmbeddingModel } from './storage/embeddings-engine.mjs';
|
|
33
|
-
import { restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
34
33
|
import { buildPressureGuardValues, installPressureGuardLaunchAgent, loadPressureGuardLaunchAgent } from './runtime-pressure.mjs';
|
|
35
34
|
import { consentToInstall } from './setup-prompts.mjs';
|
|
36
|
-
import { postgresPort, postgresContainerName } from './home-namespace.mjs';
|
|
37
35
|
|
|
38
36
|
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
|
|
39
37
|
const HOME = os.homedir();
|
|
40
|
-
const LOCAL_POSTGRES_USER = 'construct';
|
|
41
|
-
const LOCAL_POSTGRES_PASSWORD = 'construct';
|
|
42
|
-
const LOCAL_POSTGRES_DB = 'construct';
|
|
43
|
-
|
|
44
|
-
// The local DB URL's port is derived per home (construct-lb7b), so build it at
|
|
45
|
-
// the call site from the active homeDir rather than a single module-level const.
|
|
46
|
-
|
|
47
|
-
function localDatabaseUrl(env = process.env, home = HOME) {
|
|
48
|
-
return `postgresql://${LOCAL_POSTGRES_USER}:${LOCAL_POSTGRES_PASSWORD}@127.0.0.1:${postgresPort(env, home)}/${LOCAL_POSTGRES_DB}`;
|
|
49
|
-
}
|
|
50
38
|
|
|
51
39
|
function printHelp() {
|
|
52
40
|
console.log(`Construct install — machine setup (once per machine)
|
|
53
41
|
|
|
54
42
|
Usage:
|
|
55
|
-
construct install [--scope=project|user|both] [--yes] [--no-
|
|
43
|
+
construct install [--scope=project|user|both] [--yes] [--no-launch-agent] [--reconfigure] [--with-docling]
|
|
56
44
|
|
|
57
45
|
Flags:
|
|
58
46
|
--scope=<s> project | user | both (default: project — see ADR-0029)
|
|
59
47
|
project: no-op + scope guidance; Construct's project artifacts
|
|
60
48
|
are written by \`construct init\` inside a repo.
|
|
61
49
|
user: write machine-scope state (~/.construct/, MCP configs,
|
|
62
|
-
|
|
50
|
+
~/.claude/* via consent-gated sync).
|
|
63
51
|
both: print project guidance, then run user-scope install.
|
|
64
52
|
--yes accept detected defaults without prompting
|
|
65
|
-
--no-docker skip local Postgres / Docker service setup
|
|
66
53
|
--no-launch-agent skip macOS LaunchAgent background service registration
|
|
67
54
|
--reconfigure re-prompt for service consent, ignoring cached answers
|
|
55
|
+
--with-docling eagerly provision the docling document-extraction venv now
|
|
56
|
+
(heavy, ~10 min via uv; otherwise provisioned lazily on
|
|
57
|
+
first document ingest)
|
|
68
58
|
|
|
69
59
|
What --scope=user does:
|
|
70
60
|
- creates ~/.construct/config.env
|
|
71
61
|
- ensures OpenCode config exists
|
|
72
|
-
- configures managed defaults for local vector retrieval
|
|
73
|
-
- starts local Postgres with Docker when available
|
|
62
|
+
- configures managed defaults for local vector retrieval (LanceDB)
|
|
74
63
|
- checks required runtime tools and installs cm and cass when available
|
|
75
64
|
- wires Memory, GitHub, and telemetry configuration
|
|
76
65
|
- runs construct sync --global (which writes ~/.claude/* per consent)
|
|
@@ -100,23 +89,20 @@ function findCommand(command) {
|
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
export function defaultVectorIndexPath(homeDir = HOME) {
|
|
103
|
-
|
|
92
|
+
// LanceDB storage path
|
|
93
|
+
return path.join(homeDir, '.construct', 'vector', 'lancedb');
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
export async function buildManagedSetupValues({ homeDir = HOME, env = process.env
|
|
96
|
+
export async function buildManagedSetupValues({ homeDir = HOME, env = process.env } = {}) {
|
|
107
97
|
const modelInfo = await getEmbeddingModelInfo({ env });
|
|
108
98
|
|
|
109
99
|
const values = {
|
|
110
100
|
CONSTRUCT_TRACE_BACKEND: env.CONSTRUCT_TRACE_BACKEND || 'local',
|
|
111
|
-
|
|
101
|
+
CONSTRUCT_LANCEDB_PATH: env.CONSTRUCT_LANCEDB_PATH || defaultVectorIndexPath(homeDir),
|
|
112
102
|
CONSTRUCT_VECTOR_MODEL: env.CONSTRUCT_VECTOR_MODEL || modelInfo.model,
|
|
113
103
|
...buildPressureGuardValues({ env }),
|
|
114
104
|
};
|
|
115
|
-
|
|
116
|
-
// CONSTRUCT_DEPLOYMENT_MODE to ~/.construct/config.env. The env var is
|
|
117
|
-
// reserved for ephemeral runtime overrides (CI/scripts via `export …`).
|
|
118
|
-
// Forwarding it here pinned the env value and silently masked any JSON
|
|
119
|
-
// setter that ran later.
|
|
105
|
+
|
|
120
106
|
if (env[DEPLOYMENT_MODE_ENV_KEY] && getDeploymentMode(env) !== DEFAULT_DEPLOYMENT_MODE) {
|
|
121
107
|
values[DEPLOYMENT_MODE_ENV_KEY] = getDeploymentMode(env);
|
|
122
108
|
}
|
|
@@ -125,53 +111,10 @@ export async function buildManagedSetupValues({ homeDir = HOME, env = process.en
|
|
|
125
111
|
if (env.CONSTRUCT_TELEMETRY_PUBLIC_KEY) values.CONSTRUCT_TELEMETRY_PUBLIC_KEY = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
|
|
126
112
|
if (env.CONSTRUCT_TELEMETRY_SECRET_KEY) values.CONSTRUCT_TELEMETRY_SECRET_KEY = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
127
113
|
|
|
128
|
-
|
|
129
|
-
if (resolvedDatabaseUrl) values.DATABASE_URL = resolvedDatabaseUrl;
|
|
130
|
-
if (env.CONSTRUCT_VECTOR_URL) values.CONSTRUCT_VECTOR_URL = env.CONSTRUCT_VECTOR_URL;
|
|
114
|
+
if (env.DATABASE_URL) values.DATABASE_URL = env.DATABASE_URL;
|
|
131
115
|
return values;
|
|
132
116
|
}
|
|
133
117
|
|
|
134
|
-
export function localPostgresComposePath(homeDir = HOME) {
|
|
135
|
-
return path.join(homeDir, '.construct', 'services', 'postgres', 'docker-compose.yml');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function writeLocalPostgresCompose(homeDir = HOME) {
|
|
139
|
-
const composePath = localPostgresComposePath(homeDir);
|
|
140
|
-
fs.mkdirSync(path.dirname(composePath), { recursive: true });
|
|
141
|
-
|
|
142
|
-
// Container, port, and volume are derived per home so isolated HOMEs do not
|
|
143
|
-
// collide (construct-lb7b). The runtime (service-manager) derives the same
|
|
144
|
-
// values from the same home, so they match.
|
|
145
|
-
|
|
146
|
-
const container = postgresContainerName(process.env, homeDir);
|
|
147
|
-
const port = postgresPort(process.env, homeDir);
|
|
148
|
-
const volume = `${container}-data`;
|
|
149
|
-
const content = `services:
|
|
150
|
-
postgres:
|
|
151
|
-
image: pgvector/pgvector:pg16
|
|
152
|
-
container_name: ${container}
|
|
153
|
-
restart: unless-stopped
|
|
154
|
-
environment:
|
|
155
|
-
POSTGRES_USER: ${LOCAL_POSTGRES_USER}
|
|
156
|
-
POSTGRES_PASSWORD: ${LOCAL_POSTGRES_PASSWORD}
|
|
157
|
-
POSTGRES_DB: ${LOCAL_POSTGRES_DB}
|
|
158
|
-
ports:
|
|
159
|
-
- "127.0.0.1:${port}:5432"
|
|
160
|
-
volumes:
|
|
161
|
-
- ${volume}:/var/lib/postgresql/data
|
|
162
|
-
healthcheck:
|
|
163
|
-
test: ["CMD-SHELL", "pg_isready -U ${LOCAL_POSTGRES_USER} -d ${LOCAL_POSTGRES_DB}"]
|
|
164
|
-
interval: 5s
|
|
165
|
-
timeout: 5s
|
|
166
|
-
retries: 20
|
|
167
|
-
|
|
168
|
-
volumes:
|
|
169
|
-
${volume}:
|
|
170
|
-
`;
|
|
171
|
-
fs.writeFileSync(composePath, content, 'utf8');
|
|
172
|
-
return composePath;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
118
|
function runQuiet(command, args, { env = process.env, spawn = spawnSync } = {}) {
|
|
176
119
|
const result = spawn(command, args, {
|
|
177
120
|
env,
|
|
@@ -180,26 +123,6 @@ function runQuiet(command, args, { env = process.env, spawn = spawnSync } = {})
|
|
|
180
123
|
return result;
|
|
181
124
|
}
|
|
182
125
|
|
|
183
|
-
async function runAsyncQuiet(command, args, { env = process.env, spawn = spawn } = {}) {
|
|
184
|
-
return new Promise((resolve) => {
|
|
185
|
-
const timeout = setTimeout(() => {
|
|
186
|
-
resolve({ status: -1, signal: 'SIGTERM' }); // treat timeout as error
|
|
187
|
-
}, 5000);
|
|
188
|
-
const child = spawn(command, args, {
|
|
189
|
-
env,
|
|
190
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
191
|
-
});
|
|
192
|
-
let stdout = '';
|
|
193
|
-
let stderr = '';
|
|
194
|
-
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
195
|
-
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
196
|
-
child.on('close', (code, signal) => {
|
|
197
|
-
clearTimeout(timeout);
|
|
198
|
-
resolve({ status: code || 0, signal, stdout, stderr });
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
126
|
export function commandExists(command, { env = process.env, spawn = spawnSync } = {}) {
|
|
204
127
|
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
205
128
|
return runQuiet(checker, [command], { env, spawn }).status === 0;
|
|
@@ -233,8 +156,6 @@ export function ensureCmInstalled({ env = process.env, spawn = spawnSync } = {})
|
|
|
233
156
|
};
|
|
234
157
|
}
|
|
235
158
|
|
|
236
|
-
// Version 0.3.0 has a known issue where the FTS-rebuild loop can OOM on large corpora and fail to converge.
|
|
237
|
-
// Version 0.4.0 resolves this. Below this version, we fail closed to avoid the problem.
|
|
238
159
|
const CASS_MIN_VERSION = '0.4.0';
|
|
239
160
|
|
|
240
161
|
function parseSemver(s) {
|
|
@@ -268,7 +189,7 @@ export function ensureCassInstalled({ env = process.env, spawn = spawnSync } = {
|
|
|
268
189
|
}
|
|
269
190
|
return {
|
|
270
191
|
status: 'outdated',
|
|
271
|
-
message: `cass ${version || 'unknown'} is older than required ${CASS_MIN_VERSION}
|
|
192
|
+
message: `cass ${version || 'unknown'} is older than required ${CASS_MIN_VERSION}.`,
|
|
272
193
|
installCommand: 'brew upgrade dicklesworthstone/tap/cass || brew install dicklesworthstone/tap/cass',
|
|
273
194
|
};
|
|
274
195
|
}
|
|
@@ -306,121 +227,11 @@ export function ensureCassInstalled({ env = process.env, spawn = spawnSync } = {
|
|
|
306
227
|
};
|
|
307
228
|
}
|
|
308
229
|
|
|
309
|
-
/**
|
|
310
|
-
* Platform-aware install hint shown when Docker isn't on PATH. Mirrors
|
|
311
|
-
* what `supabase start` / `prisma init` print: don't auto-install, but
|
|
312
|
-
* point at the canonical option for the user's OS so they have a one-line
|
|
313
|
-
* command to copy.
|
|
314
|
-
*/
|
|
315
|
-
export function dockerInstallHint(platform = process.platform) {
|
|
316
|
-
if (platform === 'darwin') {
|
|
317
|
-
return 'Install Docker Desktop (https://www.docker.com/products/docker-desktop) or a lighter alternative like OrbStack (`brew install orbstack`) or Colima (`brew install colima && colima start`).';
|
|
318
|
-
}
|
|
319
|
-
if (platform === 'win32') {
|
|
320
|
-
return 'Install Docker Desktop for Windows (https://www.docker.com/products/docker-desktop).';
|
|
321
|
-
}
|
|
322
|
-
return 'Install Docker Engine (https://docs.docker.com/engine/install/) — your distribution likely has a `docker` package (apt/dnf/pacman).';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export function detectDockerCompose({ env = process.env, spawn = spawnSync } = {}) {
|
|
326
|
-
// Use a timeout wrapper so we don't block forever if Docker daemon is hung/missing
|
|
327
|
-
const timeoutWrap = (fn) => {
|
|
328
|
-
let timedOut = false;
|
|
329
|
-
const timer = setTimeout(() => { timedOut = true; }, 5000);
|
|
330
|
-
try {
|
|
331
|
-
const result = fn();
|
|
332
|
-
clearTimeout(timer);
|
|
333
|
-
return timedOut ? null : result;
|
|
334
|
-
} catch {
|
|
335
|
-
clearTimeout(timer);
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
const docker = timeoutWrap(() => runQuiet('docker', ['info'], { env, spawn }));
|
|
341
|
-
if (!docker || docker.status !== 0) return null;
|
|
342
|
-
const compose = timeoutWrap(() => runQuiet('docker', ['compose', 'version'], { env, spawn }));
|
|
343
|
-
if (compose && compose.status === 0) return { command: 'docker', argsPrefix: ['compose'] };
|
|
344
|
-
const dockerCompose = timeoutWrap(() => runQuiet('docker-compose', ['version'], { env, spawn }));
|
|
345
|
-
if (dockerCompose && dockerCompose.status === 0) return { command: 'docker-compose', argsPrefix: [] };
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Duplicated from service-manager.spawnDetached to avoid the import cycle
|
|
350
|
-
// setup ↔ service-manager (service-manager already imports detectDockerCompose
|
|
351
|
-
// from setup, so the reverse path can't exist).
|
|
352
|
-
|
|
353
|
-
function spawnDetachedForSetup(command, args, homeDir, logFile, options = {}) {
|
|
354
|
-
const logDir = path.join(homeDir, '.construct', 'runtime');
|
|
355
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
356
|
-
const logPath = path.join(logDir, logFile);
|
|
357
|
-
const fd = fs.openSync(logPath, 'a');
|
|
358
|
-
const child = spawn(command, args, {
|
|
359
|
-
detached: true,
|
|
360
|
-
stdio: ['ignore', fd, fd],
|
|
361
|
-
cwd: options.cwd,
|
|
362
|
-
env: options.env,
|
|
363
|
-
});
|
|
364
|
-
child.unref();
|
|
365
|
-
return { child, logPath };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
export function startManagedPostgres({ homeDir = HOME, env = process.env, spawn = spawnSync } = {}) {
|
|
369
|
-
const composeRunner = detectDockerCompose({ env, spawn });
|
|
370
|
-
if (!composeRunner) {
|
|
371
|
-
return {
|
|
372
|
-
status: 'skipped',
|
|
373
|
-
databaseUrl: env.DATABASE_URL || '',
|
|
374
|
-
message: 'Docker is not available; using existing DATABASE_URL if configured.',
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const composePath = writeLocalPostgresCompose(homeDir);
|
|
379
|
-
const result = runQuiet(
|
|
380
|
-
composeRunner.command,
|
|
381
|
-
[...composeRunner.argsPrefix, '-f', composePath, 'up', '-d', 'postgres'],
|
|
382
|
-
{ env, spawn },
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
if (result.status !== 0) {
|
|
386
|
-
return {
|
|
387
|
-
status: 'degraded',
|
|
388
|
-
databaseUrl: env.DATABASE_URL || '',
|
|
389
|
-
composePath,
|
|
390
|
-
message: (result.stderr || result.stdout || 'Docker compose failed').trim(),
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
status: 'ok',
|
|
396
|
-
databaseUrl: localDatabaseUrl(env, homeDir),
|
|
397
|
-
composePath,
|
|
398
|
-
message: 'Managed local Postgres is running.',
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async function waitForSqlReady(env, { attempts = 20, delayMs = 500 } = {}) {
|
|
403
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
404
|
-
const client = createSqlClient(env);
|
|
405
|
-
try {
|
|
406
|
-
await client`select 1 as ok`;
|
|
407
|
-
await closeSqlClient(client);
|
|
408
|
-
return true;
|
|
409
|
-
} catch {
|
|
410
|
-
await closeSqlClient(client).catch(() => {});
|
|
411
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
230
|
function warnIfGlobalCommandIsUnavailable() {
|
|
418
231
|
const globalConstruct = findCommand('construct');
|
|
419
232
|
if (!globalConstruct) {
|
|
420
233
|
console.log('\nInstall warning: `construct` is not on PATH yet.');
|
|
421
234
|
console.log(' From this checkout, run: npm install -g .');
|
|
422
|
-
console.log(' Without cloning, run: npm install -g github:geraldmaron/construct');
|
|
423
|
-
console.log(' Do not use `npm install -g construct`; that npm name belongs to another project.');
|
|
424
235
|
return;
|
|
425
236
|
}
|
|
426
237
|
|
|
@@ -431,7 +242,6 @@ function warnIfGlobalCommandIsUnavailable() {
|
|
|
431
242
|
if (version.status !== 0 || !/^construct v\d+\./.test(version.stdout.trim())) {
|
|
432
243
|
console.log(`\nInstall warning: PATH resolves \`construct\` to ${globalConstruct}, but it does not look like this CLI.`);
|
|
433
244
|
console.log(' Reinstall from this checkout with: npm install -g .');
|
|
434
|
-
console.log(' Or install from GitHub with: npm install -g github:geraldmaron/construct');
|
|
435
245
|
}
|
|
436
246
|
}
|
|
437
247
|
|
|
@@ -446,25 +256,8 @@ function ensureOpenCodeConfig() {
|
|
|
446
256
|
return getCanonicalOpenCodeConfigPath();
|
|
447
257
|
}
|
|
448
258
|
|
|
449
|
-
// Wire core.hooksPath to .beads/hooks so the tracked hook scripts (ECC
|
|
450
|
-
// secret-scan, Construct policy gates, BEADS dispatcher) actually fire on
|
|
451
|
-
// git commit/push. Without this, .beads/hooks/* are inert — present in the
|
|
452
|
-
// repo but never executed by git. Idempotent: leaves a user-set value alone
|
|
453
|
-
// rather than clobbering it.
|
|
454
|
-
|
|
455
|
-
// Project-scoped git-hooks wiring lives in its own module and is owned by
|
|
456
|
-
// `construct init`. Re-exported here for importers that reference it via
|
|
457
|
-
// setup.mjs (e.g. tests/setup-git-hooks-path.test.mjs). `construct install`
|
|
458
|
-
// must never mutate the cwd repo (ADR-0027 §3), so the install flow leaves
|
|
459
|
-
// hooks wiring to init.
|
|
460
|
-
|
|
461
259
|
export { ensureGitHooksPath } from './git-hooks-path.mjs';
|
|
462
260
|
|
|
463
|
-
// $HOME/.construct/lib/hooks/* is the resolution path for Claude Code
|
|
464
|
-
// Stop-hook commands. Absent symlink → every Stop hook fails silently,
|
|
465
|
-
// including stop-notify (dashboard cost feed). Idempotent: matching
|
|
466
|
-
// symlink left alone, stale symlink replaced, real dir refused.
|
|
467
|
-
|
|
468
261
|
export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
|
|
469
262
|
const target = path.join(homeDir, '.construct', 'lib');
|
|
470
263
|
const source = path.join(rootDir, 'lib');
|
|
@@ -485,15 +278,10 @@ export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
|
|
|
485
278
|
return {
|
|
486
279
|
status: 'conflict',
|
|
487
280
|
target,
|
|
488
|
-
message: `${target} exists and is not a symlink — leaving alone
|
|
281
|
+
message: `${target} exists and is not a symlink — leaving alone.`,
|
|
489
282
|
};
|
|
490
283
|
}
|
|
491
284
|
|
|
492
|
-
// ADR-0029: install is explicitly scoped. Default is `project`, which writes
|
|
493
|
-
// nothing and prints scope guidance — silent global writes are the failure mode
|
|
494
|
-
// that ADR retires. `user` runs the machine-state body below; `both` prints the
|
|
495
|
-
// project guidance and then runs user-scope.
|
|
496
|
-
|
|
497
285
|
const VALID_SCOPES = new Set(['project', 'user', 'both']);
|
|
498
286
|
const DEFAULT_SCOPE = 'project';
|
|
499
287
|
|
|
@@ -516,13 +304,11 @@ function printProjectScopeGuidance() {
|
|
|
516
304
|
console.log(' construct install --scope=user');
|
|
517
305
|
console.log('To do both project (guidance) and user-scope install:');
|
|
518
306
|
console.log(' construct install --scope=both');
|
|
519
|
-
console.log('\nSee ADR-0029 (docs/adr/0029-install-scopes-and-hook-budgets.md) for the contract.');
|
|
520
307
|
}
|
|
521
308
|
|
|
522
309
|
export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME } = {}) {
|
|
523
310
|
const argSet = new Set(args);
|
|
524
311
|
const isYes = argSet.has('--yes');
|
|
525
|
-
const skipDocker = argSet.has('--no-docker');
|
|
526
312
|
const reconfigure = argSet.has('--reconfigure');
|
|
527
313
|
|
|
528
314
|
if (argSet.has('--help') || argSet.has('-h')) {
|
|
@@ -530,10 +316,8 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
530
316
|
return;
|
|
531
317
|
}
|
|
532
318
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const KNOWN_FLAGS = new Set(['--yes', '--no-docker', '--no-launch-agent', '--reconfigure', '--help', '-h']);
|
|
319
|
+
const KNOWN_FLAGS = new Set(['--yes', '--no-launch-agent', '--reconfigure', '--with-docling', '--help', '-h']);
|
|
320
|
+
const withDocling = argSet.has('--with-docling');
|
|
537
321
|
const unknownFlags = args.filter((a) => {
|
|
538
322
|
if (!a.startsWith('-')) return false;
|
|
539
323
|
if (a.startsWith('--scope=') || a === '--scope') return false;
|
|
@@ -575,68 +359,25 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
575
359
|
console.log(`OpenCode config: ${opencodePath}`);
|
|
576
360
|
if (libLink.status === 'created' || libLink.status === 'replaced') {
|
|
577
361
|
console.log(`Hook lib link: ${libLink.target} → ${libLink.source} (${libLink.status})`);
|
|
578
|
-
} else if (libLink.status === 'kept') {
|
|
579
|
-
console.log(`Hook lib link: ${libLink.target} already in place`);
|
|
580
|
-
} else if (libLink.status === 'conflict') {
|
|
581
|
-
console.log(`Hook lib link: ${libLink.message}`);
|
|
582
362
|
}
|
|
583
363
|
warnIfGlobalCommandIsUnavailable();
|
|
584
364
|
|
|
585
365
|
const cmInstall = ensureCmInstalled({ env: process.env });
|
|
586
|
-
if (cmInstall.status === 'installed') {
|
|
587
|
-
console.log(
|
|
588
|
-
} else if (cmInstall.status === 'available') {
|
|
589
|
-
console.log('Memory CLI: cm available');
|
|
366
|
+
if (cmInstall.status === 'installed' || cmInstall.status === 'available') {
|
|
367
|
+
console.log(`Memory CLI: ${cmInstall.message}`);
|
|
590
368
|
} else {
|
|
591
369
|
console.log(`Memory CLI: ${cmInstall.message}`);
|
|
592
370
|
if (cmInstall.installCommand) console.log(` Install with: ${cmInstall.installCommand}`);
|
|
593
371
|
}
|
|
594
372
|
|
|
595
373
|
const cassInstall = ensureCassInstalled({ env: process.env });
|
|
596
|
-
if (cassInstall.status === 'installed') {
|
|
374
|
+
if (cassInstall.status === 'installed' || cassInstall.status === 'available') {
|
|
597
375
|
console.log(`Session search: ${cassInstall.message}`);
|
|
598
|
-
} else if (cassInstall.status === 'available') {
|
|
599
|
-
console.log('Session search: cass available');
|
|
600
376
|
} else {
|
|
601
377
|
console.log(`Session search: ${cassInstall.message}`);
|
|
602
378
|
if (cassInstall.installCommand) console.log(` Install with: ${cassInstall.installCommand}`);
|
|
603
379
|
}
|
|
604
380
|
|
|
605
|
-
// Local Postgres — consent-driven. Interactive default-yes; --yes accepts
|
|
606
|
-
// without prompting. Skipped when --no-docker, when DATABASE_URL is already
|
|
607
|
-
// set (caller has an external DB), or when user declines.
|
|
608
|
-
|
|
609
|
-
const dockerRunner = detectDockerCompose();
|
|
610
|
-
const dockerAvailable = !skipDocker && Boolean(dockerRunner);
|
|
611
|
-
let serviceResult = {
|
|
612
|
-
status: 'skipped',
|
|
613
|
-
databaseUrl: process.env.DATABASE_URL || '',
|
|
614
|
-
message: skipDocker ? 'Docker service setup skipped by flag.' : 'Docker not detected — skipping local Postgres.',
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
if (dockerAvailable) {
|
|
618
|
-
const pgConsent = await consentToInstall({
|
|
619
|
-
name: 'postgres',
|
|
620
|
-
isYes,
|
|
621
|
-
force: reconfigure,
|
|
622
|
-
alreadyConfigured: Boolean(process.env.DATABASE_URL),
|
|
623
|
-
alreadyConfiguredNote: 'DATABASE_URL already set — using external database.',
|
|
624
|
-
envPath,
|
|
625
|
-
});
|
|
626
|
-
if (pgConsent.decision) {
|
|
627
|
-
writeLocalPostgresCompose(homeDir);
|
|
628
|
-
serviceResult = startManagedPostgres({ homeDir, env: process.env });
|
|
629
|
-
console.log(`Postgres: ${serviceResult.status === 'ok' ? 'started locally' : serviceResult.message}`);
|
|
630
|
-
} else {
|
|
631
|
-
console.log(`Postgres: skipped (${pgConsent.note})`);
|
|
632
|
-
}
|
|
633
|
-
} else if (skipDocker) {
|
|
634
|
-
console.log('Postgres: skipped (--no-docker)');
|
|
635
|
-
} else {
|
|
636
|
-
console.log('Postgres: skipped (Docker not detected — local Postgres unavailable)');
|
|
637
|
-
console.log(` ${dockerInstallHint()}`);
|
|
638
|
-
}
|
|
639
|
-
|
|
640
381
|
const telemetryResult = process.env.CONSTRUCT_TELEMETRY_URL
|
|
641
382
|
? { status: 'configured', note: `remote export configured (${process.env.CONSTRUCT_TELEMETRY_URL})` }
|
|
642
383
|
: { status: 'local', note: 'local JSONL traces in .cx/traces; remote export optional' };
|
|
@@ -644,13 +385,6 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
644
385
|
|
|
645
386
|
fs.mkdirSync(path.dirname(defaultVectorIndexPath(homeDir)), { recursive: true });
|
|
646
387
|
|
|
647
|
-
// Pre-warm the embedding model so the first agent query doesn't stall on
|
|
648
|
-
// a one-time ONNX download. Same pattern as `supabase start` pre-pulling
|
|
649
|
-
// images and `playwright install` pre-fetching browsers — make the wait
|
|
650
|
-
// visible during setup, not during user-facing work. Best-effort: a
|
|
651
|
-
// download failure here degrades gracefully via the engine's hash fallback
|
|
652
|
-
// and is non-fatal to setup.
|
|
653
|
-
|
|
654
388
|
try {
|
|
655
389
|
const warm = await warmupEmbeddingModel({ env: process.env });
|
|
656
390
|
if (warm.degraded) {
|
|
@@ -662,12 +396,22 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
662
396
|
console.log(`Embeddings: warmup skipped (${err?.message || 'unknown error'}) — model will load on first use`);
|
|
663
397
|
}
|
|
664
398
|
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
// and
|
|
668
|
-
//
|
|
399
|
+
// Docling (document/PDF extraction) provisions a pinned Python venv via uv,
|
|
400
|
+
// which is heavy (~10 min) and only needed for document ingest — so it stays
|
|
401
|
+
// lazy by default and is provisioned eagerly only when the user opts in with
|
|
402
|
+
// --with-docling. First document ingest still auto-provisions if skipped.
|
|
403
|
+
if (withDocling) {
|
|
404
|
+
try {
|
|
405
|
+
const { ensureDoclingVenv } = await import('./runtime/uv-bootstrap.mjs');
|
|
406
|
+
console.log('Docling: provisioning Python venv (uv) — this can take several minutes…');
|
|
407
|
+
const docling = await ensureDoclingVenv();
|
|
408
|
+
console.log(`Docling: ready (${docling.fresh ? 'provisioned' : 'already present'} at ${docling.venvDir})`);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.log(`Docling: provisioning skipped (${err?.message || 'unknown error'}) — will provision on first document ingest`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
669
413
|
|
|
670
|
-
const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference
|
|
414
|
+
const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference } =
|
|
671
415
|
await import('./model-cheapest-provider.mjs');
|
|
672
416
|
const cheapestAlreadyEnabled = isCheapestProviderEnabled(envPath, { env: process.env });
|
|
673
417
|
if (!cheapestAlreadyEnabled) {
|
|
@@ -694,29 +438,22 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
694
438
|
const label = selections[tier]?.providerLabel || '';
|
|
695
439
|
console.log(` ${tier.padEnd(11)} ${model} (${label})`);
|
|
696
440
|
}
|
|
697
|
-
} else {
|
|
698
|
-
console.log('\nCheapest provider: no configured providers found — nothing to apply.');
|
|
699
441
|
}
|
|
700
442
|
setCheapestProviderPreference(envPath, true);
|
|
701
443
|
} catch (err) {
|
|
702
444
|
console.log(`Cheapest provider: skipped (${err?.message || 'unknown error'})`);
|
|
703
445
|
}
|
|
704
|
-
} else {
|
|
705
|
-
console.log(`Cheapest provider: skipped (${cheapestConsent.note})`);
|
|
706
446
|
}
|
|
707
|
-
} else {
|
|
708
|
-
console.log('Cheapest provider: already enabled — skipping prompt.');
|
|
709
447
|
}
|
|
710
448
|
|
|
711
|
-
// Ensure workspace directory with docs lanes exists
|
|
712
449
|
ensureWorkspace(homeDir);
|
|
713
450
|
|
|
714
451
|
const managedValues = await buildManagedSetupValues({
|
|
715
452
|
homeDir,
|
|
716
453
|
env: process.env,
|
|
717
|
-
databaseUrl: serviceResult.databaseUrl,
|
|
718
454
|
});
|
|
719
455
|
writeEnvValues(envPath, managedValues);
|
|
456
|
+
|
|
720
457
|
let pressureGuardAgent = null;
|
|
721
458
|
let pressureGuardLoad = null;
|
|
722
459
|
if (process.platform === 'darwin' && !argSet.has('--no-launch-agent')) {
|
|
@@ -737,110 +474,18 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
737
474
|
nodePath: process.execPath,
|
|
738
475
|
});
|
|
739
476
|
pressureGuardLoad = loadPressureGuardLaunchAgent({ plistPath: pressureGuardAgent.plistPath });
|
|
740
|
-
} else {
|
|
741
|
-
console.log(`LaunchAgent: skipped (${laConsent.note})`);
|
|
742
|
-
}
|
|
743
|
-
} else if (argSet.has('--no-launch-agent')) {
|
|
744
|
-
console.log('LaunchAgent: skipped (--no-launch-agent)');
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (isYes) {
|
|
748
|
-
console.log('\nManaged setup:');
|
|
749
|
-
console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
750
|
-
console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
|
|
751
|
-
console.log(` Vector model: ${managedValues.CONSTRUCT_VECTOR_MODEL}`);
|
|
752
|
-
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}`);
|
|
753
|
-
if (managedValues.CONSTRUCT_TELEMETRY_URL) console.log(` Trace URL: ${managedValues.CONSTRUCT_TELEMETRY_URL}`);
|
|
754
|
-
console.log(` Pressure guard: every ${managedValues.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS}s, swap threshold ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB`);
|
|
755
|
-
if (serviceResult.status === 'ok') {
|
|
756
|
-
console.log(` Postgres: ${serviceResult.databaseUrl}`);
|
|
757
|
-
console.log(` Compose file: ${serviceResult.composePath}`);
|
|
758
|
-
} else {
|
|
759
|
-
console.log(` Postgres: ${serviceResult.message}`);
|
|
760
|
-
}
|
|
761
|
-
if (pressureGuardAgent?.plistPath) {
|
|
762
|
-
console.log(` LaunchAgent: ${pressureGuardAgent.plistPath}`);
|
|
763
|
-
console.log(` LaunchAgent load: ${pressureGuardLoad?.loaded ? 'active' : pressureGuardLoad?.reason || 'pending manual load'}`);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Schema init runs whenever postgres is reachable — pgvector tables must exist before any
|
|
768
|
-
// retrieval call. Idempotent via construct_schema_migrations hash tracking.
|
|
769
|
-
|
|
770
|
-
if (serviceResult.status === 'ok') {
|
|
771
|
-
try {
|
|
772
|
-
await waitForSqlReady({ ...process.env, ...managedValues });
|
|
773
|
-
const readyClient = createSqlClient({ ...process.env, ...managedValues });
|
|
774
|
-
if (readyClient) {
|
|
775
|
-
try {
|
|
776
|
-
const { applied } = await runMigrations(readyClient);
|
|
777
|
-
if (applied.length) console.log(`Postgres schema: applied ${applied.length} migration(s) — ${applied.join(', ')}`);
|
|
778
|
-
else console.log('Postgres schema: up to date');
|
|
779
|
-
} finally {
|
|
780
|
-
await closeSqlClient(readyClient).catch(() => {});
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
} catch (error) {
|
|
784
|
-
console.log(`Postgres schema init failed: ${error?.message || 'unknown error'}`);
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (isYes && serviceResult.status === 'ok') {
|
|
789
|
-
const sqlClient = createSqlClient({ ...process.env, ...managedValues });
|
|
790
|
-
if (sqlClient) {
|
|
791
|
-
try {
|
|
792
|
-
const restore = restoreConstructDb({ homeDir });
|
|
793
|
-
if (restore.status === 'restored') {
|
|
794
|
-
console.log(`Construct DB restored from stash: ${restore.stashPath}`);
|
|
795
|
-
} else if (restore.status === 'no-stash') {
|
|
796
|
-
console.log('No stash found — starting with empty construct DB.');
|
|
797
|
-
} else {
|
|
798
|
-
console.log(`Construct DB restore: ${restore.status}`);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// Index Construct's own package docs (rootDir, machine-scoped) into the
|
|
802
|
-
// shared hybrid store. Seeding a downstream project's corpus is project
|
|
803
|
-
// state and belongs to `construct init` / `construct ingest`, never to
|
|
804
|
-
// machine install (ADR-0027 §3): install reads no cwd.
|
|
805
|
-
|
|
806
|
-
const syncResult = await syncFileStateToSql(rootDir, { env: { ...process.env, ...managedValues }, project: 'construct' });
|
|
807
|
-
console.log(`Hybrid storage sync: ${syncResult.status}`);
|
|
808
|
-
if (syncResult.embeddingModel) console.log(`Embedding model: ${syncResult.embeddingModel}`);
|
|
809
|
-
} catch (error) {
|
|
810
|
-
console.log(`Hybrid storage sync failed: ${error?.message || 'unknown error'}`);
|
|
811
|
-
} finally {
|
|
812
|
-
await closeSqlClient(sqlClient).catch(() => {});
|
|
813
|
-
}
|
|
814
477
|
}
|
|
815
478
|
}
|
|
816
479
|
|
|
817
480
|
if (isYes) {
|
|
818
481
|
runConstruct(['mcp', 'add', 'memory', '--auto'], { optional: true });
|
|
819
482
|
runConstruct(['mcp', 'add', 'github', '--auto'], { optional: true });
|
|
820
|
-
} else {
|
|
821
|
-
console.log('\nManaged defaults written:');
|
|
822
|
-
console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
823
|
-
console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
|
|
824
|
-
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
|
|
825
|
-
console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
|
|
826
|
-
console.log('\nFor unattended setup, including local Postgres when Docker is running:');
|
|
827
|
-
console.log(' construct install --yes');
|
|
828
483
|
}
|
|
829
484
|
|
|
830
|
-
// Install is machine setup: the global front-door agent must land on every
|
|
831
|
-
// user-scope surface (opencode/claude/codex/copilot) or a fresh machine fails
|
|
832
|
-
// cross-surface parity. Both `sync` and `sync --global` are kept to preserve
|
|
833
|
-
// every write across the project-detected and forced-global paths; in a
|
|
834
|
-
// non-project install cwd both resolve to the same global branch and would
|
|
835
|
-
// print the "Synced … to global scope" summary twice. The first call runs
|
|
836
|
-
// --quiet so the canonical summary prints exactly once from `sync --global`,
|
|
837
|
-
// with no change to what either writes.
|
|
838
|
-
|
|
839
485
|
runConstruct(['sync', '--quiet']);
|
|
840
486
|
runConstruct(['sync', '--global']);
|
|
841
487
|
runConstruct(['doctor']);
|
|
842
488
|
|
|
843
|
-
// ── Summary panel ────────────────────────────────────────────────────────
|
|
844
489
|
const setupTs = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
|
|
845
490
|
const setupLogPath = path.join(HOME, '.cx', `setup-${setupTs}.log`);
|
|
846
491
|
try {
|
|
@@ -850,42 +495,21 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
850
495
|
`Config: ${getUserEnvPath(HOME)}`,
|
|
851
496
|
`OpenCode: ${getCanonicalOpenCodeConfigPath(HOME)}`,
|
|
852
497
|
];
|
|
853
|
-
if (managedValues?.DATABASE_URL) {
|
|
854
|
-
logLines.push(`Database: ${managedValues.DATABASE_URL.replace(/:\/\/[^@]+@/, '://<credentials>@')}`);
|
|
855
|
-
}
|
|
856
498
|
fs.writeFileSync(setupLogPath, logLines.join('\n') + '\n');
|
|
857
499
|
} catch { /* best effort */ }
|
|
858
500
|
|
|
859
501
|
console.log('\n────────────────────────────────────');
|
|
860
502
|
console.log('Setup complete.');
|
|
861
503
|
|
|
862
|
-
// Local-service summary — surfaces running local services. Same pattern
|
|
863
|
-
// as `supabase start`, which prints all local URLs + keys after spin-up.
|
|
864
|
-
|
|
865
|
-
const hasLocalPostgres = serviceResult?.status === 'ok' && serviceResult?.databaseUrl?.includes('127.0.0.1');
|
|
866
|
-
const vectorBackend = hasLocalPostgres
|
|
867
|
-
? { label: 'Postgres + pgvector', detail: `${serviceResult.databaseUrl} (384d embeddings, ${managedValues.CONSTRUCT_VECTOR_MODEL})` }
|
|
868
|
-
: { label: 'JSON fallback', detail: `${managedValues.CONSTRUCT_VECTOR_INDEX_PATH} (${managedValues.CONSTRUCT_VECTOR_MODEL})` };
|
|
869
|
-
|
|
870
504
|
console.log('\nLocal services:');
|
|
871
505
|
console.log(' Traces: local JSONL (.cx/traces)');
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (hasLocalPostgres) {
|
|
875
|
-
console.log(` Postgres: ${serviceResult.databaseUrl}`);
|
|
876
|
-
} else if (process.env.DATABASE_URL) {
|
|
877
|
-
console.log(` Postgres: external (${process.env.DATABASE_URL.replace(/:\/\/[^@]+@/, '://<credentials>@')})`);
|
|
878
|
-
} else {
|
|
879
|
-
console.log(` Postgres: not running — vector retrieval will use JSON fallback`);
|
|
880
|
-
}
|
|
881
|
-
console.log(` Vector: ${vectorBackend.label}`);
|
|
882
|
-
console.log(` ${vectorBackend.detail}`);
|
|
506
|
+
console.log(` Vector: LanceDB (embedded)`);
|
|
507
|
+
console.log(` ${managedValues.CONSTRUCT_LANCEDB_PATH}`);
|
|
883
508
|
console.log(' Credentials are saved to ~/.construct/config.env for later reference.');
|
|
884
509
|
|
|
885
510
|
console.log('\nNext steps:');
|
|
886
511
|
console.log(' construct provider add github # Connect GitHub repository data');
|
|
887
512
|
console.log(' construct doctor # Verify all systems');
|
|
888
|
-
console.log(' construct evals retrieval # Baseline retrieval quality');
|
|
889
513
|
console.log(`\nSetup log: ${setupLogPath}`);
|
|
890
514
|
}
|
|
891
515
|
|
|
@@ -896,17 +520,12 @@ function ensureUserConfig(homeDir = HOME) {
|
|
|
896
520
|
return envPath;
|
|
897
521
|
}
|
|
898
522
|
|
|
899
|
-
/**
|
|
900
|
-
* Create ~/.construct/workspace/ with the standard docs lane structure.
|
|
901
|
-
* Fallback target for embed output that isn't repo-specific.
|
|
902
|
-
*/
|
|
903
523
|
export function ensureWorkspace(homeDir = HOME) {
|
|
904
524
|
const wsPath = path.join(homeDir, '.construct', 'workspace');
|
|
905
525
|
const docsPath = path.join(wsPath, 'docs');
|
|
906
526
|
for (const lane of WORKSPACE_DOCS_LANES) {
|
|
907
527
|
fs.mkdirSync(path.join(docsPath, lane), { recursive: true });
|
|
908
528
|
}
|
|
909
|
-
// Ensure top-level workspace files exist
|
|
910
529
|
const snapshotPath = path.join(wsPath, 'snapshot.md');
|
|
911
530
|
if (!fs.existsSync(snapshotPath)) fs.writeFileSync(snapshotPath, '# Snapshot\n\nNo snapshot yet.\n');
|
|
912
531
|
const roadmapPath = path.join(wsPath, 'roadmap.md');
|