@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/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,42 +29,38 @@ 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 { createSqlClient, closeSqlClient } from './storage/backend.mjs';
|
|
31
32
|
import { getEmbeddingModelInfo, warmupEmbeddingModel } from './storage/embeddings-engine.mjs';
|
|
32
|
-
import { restoreConstructDb } from './storage/postgres-backup.mjs';
|
|
33
33
|
import { buildPressureGuardValues, installPressureGuardLaunchAgent, loadPressureGuardLaunchAgent } from './runtime-pressure.mjs';
|
|
34
34
|
import { consentToInstall } from './setup-prompts.mjs';
|
|
35
35
|
|
|
36
36
|
const ROOT_DIR = path.resolve(import.meta.dirname, '..');
|
|
37
37
|
const HOME = os.homedir();
|
|
38
|
-
const LOCAL_POSTGRES_PORT = '54329';
|
|
39
|
-
const LOCAL_POSTGRES_USER = 'construct';
|
|
40
|
-
const LOCAL_POSTGRES_PASSWORD = 'construct';
|
|
41
|
-
const LOCAL_POSTGRES_DB = 'construct';
|
|
42
|
-
const LOCAL_DATABASE_URL = `postgresql://${LOCAL_POSTGRES_USER}:${LOCAL_POSTGRES_PASSWORD}@127.0.0.1:${LOCAL_POSTGRES_PORT}/${LOCAL_POSTGRES_DB}`;
|
|
43
38
|
|
|
44
39
|
function printHelp() {
|
|
45
40
|
console.log(`Construct install — machine setup (once per machine)
|
|
46
41
|
|
|
47
42
|
Usage:
|
|
48
|
-
construct install [--yes] [--no-
|
|
43
|
+
construct install [--scope=project|user|both] [--yes] [--no-launch-agent] [--reconfigure]
|
|
49
44
|
|
|
50
45
|
Flags:
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
--scope=<s> project | user | both (default: project — see ADR-0029)
|
|
47
|
+
project: no-op + scope guidance; Construct's project artifacts
|
|
48
|
+
are written by \`construct init\` inside a repo.
|
|
49
|
+
user: write machine-scope state (~/.construct/, MCP configs,
|
|
50
|
+
~/.claude/* via consent-gated sync).
|
|
51
|
+
both: print project guidance, then run user-scope install.
|
|
52
|
+
--yes accept detected defaults without prompting
|
|
53
|
+
--no-launch-agent skip macOS LaunchAgent background service registration
|
|
54
|
+
--reconfigure re-prompt for service consent, ignoring cached answers
|
|
55
|
+
|
|
56
|
+
What --scope=user does:
|
|
56
57
|
- creates ~/.construct/config.env
|
|
57
58
|
- ensures OpenCode config exists
|
|
58
|
-
- configures managed defaults for local vector retrieval
|
|
59
|
-
- starts local Postgres with Docker when available
|
|
59
|
+
- configures managed defaults for local vector retrieval (LanceDB)
|
|
60
60
|
- checks required runtime tools and installs cm and cass when available
|
|
61
61
|
- wires Memory, GitHub, and telemetry configuration
|
|
62
|
-
- runs construct sync (which
|
|
62
|
+
- runs construct sync --global (which writes ~/.claude/* per consent)
|
|
63
63
|
- runs construct doctor
|
|
64
|
-
- detects the project tech stack and writes .cx/project-profile.json
|
|
65
64
|
|
|
66
65
|
For project setup (once per repo): construct init`);
|
|
67
66
|
}
|
|
@@ -87,23 +86,20 @@ function findCommand(command) {
|
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
export function defaultVectorIndexPath(homeDir = HOME) {
|
|
90
|
-
|
|
89
|
+
// LanceDB storage path
|
|
90
|
+
return path.join(homeDir, '.construct', 'vector', 'lancedb');
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export async function buildManagedSetupValues({ homeDir = HOME, env = process.env
|
|
93
|
+
export async function buildManagedSetupValues({ homeDir = HOME, env = process.env } = {}) {
|
|
94
94
|
const modelInfo = await getEmbeddingModelInfo({ env });
|
|
95
95
|
|
|
96
96
|
const values = {
|
|
97
97
|
CONSTRUCT_TRACE_BACKEND: env.CONSTRUCT_TRACE_BACKEND || 'local',
|
|
98
|
-
|
|
98
|
+
CONSTRUCT_LANCEDB_PATH: env.CONSTRUCT_LANCEDB_PATH || defaultVectorIndexPath(homeDir),
|
|
99
99
|
CONSTRUCT_VECTOR_MODEL: env.CONSTRUCT_VECTOR_MODEL || modelInfo.model,
|
|
100
100
|
...buildPressureGuardValues({ env }),
|
|
101
101
|
};
|
|
102
|
-
|
|
103
|
-
// CONSTRUCT_DEPLOYMENT_MODE to ~/.construct/config.env. The env var is
|
|
104
|
-
// reserved for ephemeral runtime overrides (CI/scripts via `export …`).
|
|
105
|
-
// Forwarding it here pinned the env value and silently masked any JSON
|
|
106
|
-
// setter that ran later.
|
|
102
|
+
|
|
107
103
|
if (env[DEPLOYMENT_MODE_ENV_KEY] && getDeploymentMode(env) !== DEFAULT_DEPLOYMENT_MODE) {
|
|
108
104
|
values[DEPLOYMENT_MODE_ENV_KEY] = getDeploymentMode(env);
|
|
109
105
|
}
|
|
@@ -112,45 +108,10 @@ export async function buildManagedSetupValues({ homeDir = HOME, env = process.en
|
|
|
112
108
|
if (env.CONSTRUCT_TELEMETRY_PUBLIC_KEY) values.CONSTRUCT_TELEMETRY_PUBLIC_KEY = env.CONSTRUCT_TELEMETRY_PUBLIC_KEY;
|
|
113
109
|
if (env.CONSTRUCT_TELEMETRY_SECRET_KEY) values.CONSTRUCT_TELEMETRY_SECRET_KEY = env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
if (resolvedDatabaseUrl) values.DATABASE_URL = resolvedDatabaseUrl;
|
|
117
|
-
if (env.CONSTRUCT_VECTOR_URL) values.CONSTRUCT_VECTOR_URL = env.CONSTRUCT_VECTOR_URL;
|
|
111
|
+
if (env.DATABASE_URL) values.DATABASE_URL = env.DATABASE_URL;
|
|
118
112
|
return values;
|
|
119
113
|
}
|
|
120
114
|
|
|
121
|
-
export function localPostgresComposePath(homeDir = HOME) {
|
|
122
|
-
return path.join(homeDir, '.construct', 'services', 'postgres', 'docker-compose.yml');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function writeLocalPostgresCompose(homeDir = HOME) {
|
|
126
|
-
const composePath = localPostgresComposePath(homeDir);
|
|
127
|
-
fs.mkdirSync(path.dirname(composePath), { recursive: true });
|
|
128
|
-
const content = `services:
|
|
129
|
-
postgres:
|
|
130
|
-
image: pgvector/pgvector:pg16
|
|
131
|
-
container_name: construct-postgres
|
|
132
|
-
restart: unless-stopped
|
|
133
|
-
environment:
|
|
134
|
-
POSTGRES_USER: ${LOCAL_POSTGRES_USER}
|
|
135
|
-
POSTGRES_PASSWORD: ${LOCAL_POSTGRES_PASSWORD}
|
|
136
|
-
POSTGRES_DB: ${LOCAL_POSTGRES_DB}
|
|
137
|
-
ports:
|
|
138
|
-
- "127.0.0.1:${LOCAL_POSTGRES_PORT}:5432"
|
|
139
|
-
volumes:
|
|
140
|
-
- construct-postgres-data:/var/lib/postgresql/data
|
|
141
|
-
healthcheck:
|
|
142
|
-
test: ["CMD-SHELL", "pg_isready -U ${LOCAL_POSTGRES_USER} -d ${LOCAL_POSTGRES_DB}"]
|
|
143
|
-
interval: 5s
|
|
144
|
-
timeout: 5s
|
|
145
|
-
retries: 20
|
|
146
|
-
|
|
147
|
-
volumes:
|
|
148
|
-
construct-postgres-data:
|
|
149
|
-
`;
|
|
150
|
-
fs.writeFileSync(composePath, content, 'utf8');
|
|
151
|
-
return composePath;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
115
|
function runQuiet(command, args, { env = process.env, spawn = spawnSync } = {}) {
|
|
155
116
|
const result = spawn(command, args, {
|
|
156
117
|
env,
|
|
@@ -159,26 +120,6 @@ function runQuiet(command, args, { env = process.env, spawn = spawnSync } = {})
|
|
|
159
120
|
return result;
|
|
160
121
|
}
|
|
161
122
|
|
|
162
|
-
async function runAsyncQuiet(command, args, { env = process.env, spawn = spawn } = {}) {
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
const timeout = setTimeout(() => {
|
|
165
|
-
resolve({ status: -1, signal: 'SIGTERM' }); // treat timeout as error
|
|
166
|
-
}, 5000);
|
|
167
|
-
const child = spawn(command, args, {
|
|
168
|
-
env,
|
|
169
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
-
});
|
|
171
|
-
let stdout = '';
|
|
172
|
-
let stderr = '';
|
|
173
|
-
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
174
|
-
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
175
|
-
child.on('close', (code, signal) => {
|
|
176
|
-
clearTimeout(timeout);
|
|
177
|
-
resolve({ status: code || 0, signal, stdout, stderr });
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
123
|
export function commandExists(command, { env = process.env, spawn = spawnSync } = {}) {
|
|
183
124
|
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
184
125
|
return runQuiet(checker, [command], { env, spawn }).status === 0;
|
|
@@ -212,8 +153,6 @@ export function ensureCmInstalled({ env = process.env, spawn = spawnSync } = {})
|
|
|
212
153
|
};
|
|
213
154
|
}
|
|
214
155
|
|
|
215
|
-
// Version 0.3.0 has a known issue where the FTS-rebuild loop can OOM on large corpora and fail to converge.
|
|
216
|
-
// Version 0.4.0 resolves this. Below this version, we fail closed to avoid the problem.
|
|
217
156
|
const CASS_MIN_VERSION = '0.4.0';
|
|
218
157
|
|
|
219
158
|
function parseSemver(s) {
|
|
@@ -247,7 +186,7 @@ export function ensureCassInstalled({ env = process.env, spawn = spawnSync } = {
|
|
|
247
186
|
}
|
|
248
187
|
return {
|
|
249
188
|
status: 'outdated',
|
|
250
|
-
message: `cass ${version || 'unknown'} is older than required ${CASS_MIN_VERSION}
|
|
189
|
+
message: `cass ${version || 'unknown'} is older than required ${CASS_MIN_VERSION}.`,
|
|
251
190
|
installCommand: 'brew upgrade dicklesworthstone/tap/cass || brew install dicklesworthstone/tap/cass',
|
|
252
191
|
};
|
|
253
192
|
}
|
|
@@ -285,121 +224,11 @@ export function ensureCassInstalled({ env = process.env, spawn = spawnSync } = {
|
|
|
285
224
|
};
|
|
286
225
|
}
|
|
287
226
|
|
|
288
|
-
/**
|
|
289
|
-
* Platform-aware install hint shown when Docker isn't on PATH. Mirrors
|
|
290
|
-
* what `supabase start` / `prisma init` print: don't auto-install, but
|
|
291
|
-
* point at the canonical option for the user's OS so they have a one-line
|
|
292
|
-
* command to copy.
|
|
293
|
-
*/
|
|
294
|
-
export function dockerInstallHint(platform = process.platform) {
|
|
295
|
-
if (platform === 'darwin') {
|
|
296
|
-
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`).';
|
|
297
|
-
}
|
|
298
|
-
if (platform === 'win32') {
|
|
299
|
-
return 'Install Docker Desktop for Windows (https://www.docker.com/products/docker-desktop).';
|
|
300
|
-
}
|
|
301
|
-
return 'Install Docker Engine (https://docs.docker.com/engine/install/) — your distribution likely has a `docker` package (apt/dnf/pacman).';
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function detectDockerCompose({ env = process.env, spawn = spawnSync } = {}) {
|
|
305
|
-
// Use a timeout wrapper so we don't block forever if Docker daemon is hung/missing
|
|
306
|
-
const timeoutWrap = (fn) => {
|
|
307
|
-
let timedOut = false;
|
|
308
|
-
const timer = setTimeout(() => { timedOut = true; }, 5000);
|
|
309
|
-
try {
|
|
310
|
-
const result = fn();
|
|
311
|
-
clearTimeout(timer);
|
|
312
|
-
return timedOut ? null : result;
|
|
313
|
-
} catch {
|
|
314
|
-
clearTimeout(timer);
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const docker = timeoutWrap(() => runQuiet('docker', ['info'], { env, spawn }));
|
|
320
|
-
if (!docker || docker.status !== 0) return null;
|
|
321
|
-
const compose = timeoutWrap(() => runQuiet('docker', ['compose', 'version'], { env, spawn }));
|
|
322
|
-
if (compose && compose.status === 0) return { command: 'docker', argsPrefix: ['compose'] };
|
|
323
|
-
const dockerCompose = timeoutWrap(() => runQuiet('docker-compose', ['version'], { env, spawn }));
|
|
324
|
-
if (dockerCompose && dockerCompose.status === 0) return { command: 'docker-compose', argsPrefix: [] };
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Duplicated from service-manager.spawnDetached to avoid the import cycle
|
|
329
|
-
// setup ↔ service-manager (service-manager already imports detectDockerCompose
|
|
330
|
-
// from setup, so the reverse path can't exist).
|
|
331
|
-
|
|
332
|
-
function spawnDetachedForSetup(command, args, homeDir, logFile, options = {}) {
|
|
333
|
-
const logDir = path.join(homeDir, '.construct', 'runtime');
|
|
334
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
335
|
-
const logPath = path.join(logDir, logFile);
|
|
336
|
-
const fd = fs.openSync(logPath, 'a');
|
|
337
|
-
const child = spawn(command, args, {
|
|
338
|
-
detached: true,
|
|
339
|
-
stdio: ['ignore', fd, fd],
|
|
340
|
-
cwd: options.cwd,
|
|
341
|
-
env: options.env,
|
|
342
|
-
});
|
|
343
|
-
child.unref();
|
|
344
|
-
return { child, logPath };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export function startManagedPostgres({ homeDir = HOME, env = process.env, spawn = spawnSync } = {}) {
|
|
348
|
-
const composeRunner = detectDockerCompose({ env, spawn });
|
|
349
|
-
if (!composeRunner) {
|
|
350
|
-
return {
|
|
351
|
-
status: 'skipped',
|
|
352
|
-
databaseUrl: env.DATABASE_URL || '',
|
|
353
|
-
message: 'Docker is not available; using existing DATABASE_URL if configured.',
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const composePath = writeLocalPostgresCompose(homeDir);
|
|
358
|
-
const result = runQuiet(
|
|
359
|
-
composeRunner.command,
|
|
360
|
-
[...composeRunner.argsPrefix, '-f', composePath, 'up', '-d', 'postgres'],
|
|
361
|
-
{ env, spawn },
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
if (result.status !== 0) {
|
|
365
|
-
return {
|
|
366
|
-
status: 'degraded',
|
|
367
|
-
databaseUrl: env.DATABASE_URL || '',
|
|
368
|
-
composePath,
|
|
369
|
-
message: (result.stderr || result.stdout || 'Docker compose failed').trim(),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
status: 'ok',
|
|
375
|
-
databaseUrl: LOCAL_DATABASE_URL,
|
|
376
|
-
composePath,
|
|
377
|
-
message: 'Managed local Postgres is running.',
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
async function waitForSqlReady(env, { attempts = 20, delayMs = 500 } = {}) {
|
|
382
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
383
|
-
const client = createSqlClient(env);
|
|
384
|
-
try {
|
|
385
|
-
await client`select 1 as ok`;
|
|
386
|
-
await closeSqlClient(client);
|
|
387
|
-
return true;
|
|
388
|
-
} catch {
|
|
389
|
-
await closeSqlClient(client).catch(() => {});
|
|
390
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
227
|
function warnIfGlobalCommandIsUnavailable() {
|
|
397
228
|
const globalConstruct = findCommand('construct');
|
|
398
229
|
if (!globalConstruct) {
|
|
399
230
|
console.log('\nInstall warning: `construct` is not on PATH yet.');
|
|
400
231
|
console.log(' From this checkout, run: npm install -g .');
|
|
401
|
-
console.log(' Without cloning, run: npm install -g github:geraldmaron/construct');
|
|
402
|
-
console.log(' Do not use `npm install -g construct`; that npm name belongs to another project.');
|
|
403
232
|
return;
|
|
404
233
|
}
|
|
405
234
|
|
|
@@ -410,7 +239,6 @@ function warnIfGlobalCommandIsUnavailable() {
|
|
|
410
239
|
if (version.status !== 0 || !/^construct v\d+\./.test(version.stdout.trim())) {
|
|
411
240
|
console.log(`\nInstall warning: PATH resolves \`construct\` to ${globalConstruct}, but it does not look like this CLI.`);
|
|
412
241
|
console.log(' Reinstall from this checkout with: npm install -g .');
|
|
413
|
-
console.log(' Or install from GitHub with: npm install -g github:geraldmaron/construct');
|
|
414
242
|
}
|
|
415
243
|
}
|
|
416
244
|
|
|
@@ -425,61 +253,7 @@ function ensureOpenCodeConfig() {
|
|
|
425
253
|
return getCanonicalOpenCodeConfigPath();
|
|
426
254
|
}
|
|
427
255
|
|
|
428
|
-
|
|
429
|
-
// secret-scan, Construct policy gates, BEADS dispatcher) actually fire on
|
|
430
|
-
// git commit/push. Without this, .beads/hooks/* are inert — present in the
|
|
431
|
-
// repo but never executed by git. Idempotent: leaves a user-set value alone
|
|
432
|
-
// rather than clobbering it.
|
|
433
|
-
|
|
434
|
-
export function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
|
|
435
|
-
const hooksDir = path.join(cwd, '.beads', 'hooks');
|
|
436
|
-
if (!fs.existsSync(path.join(hooksDir, 'pre-commit'))) {
|
|
437
|
-
return { status: 'skipped', reason: 'no .beads/hooks/pre-commit in this directory' };
|
|
438
|
-
}
|
|
439
|
-
const inGit = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, stdio: 'pipe', encoding: 'utf8' });
|
|
440
|
-
if (inGit.status !== 0) {
|
|
441
|
-
return { status: 'skipped', reason: 'not a git working tree' };
|
|
442
|
-
}
|
|
443
|
-
const current = spawnSync('git', ['config', '--get', 'core.hooksPath'], { cwd, stdio: 'pipe', encoding: 'utf8' });
|
|
444
|
-
const currentValue = current.status === 0 ? (current.stdout || '').trim() : '';
|
|
445
|
-
const desired = '.beads/hooks';
|
|
446
|
-
if (currentValue === desired) {
|
|
447
|
-
return { status: 'ok', message: 'core.hooksPath already wired to .beads/hooks' };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Treat the git default (`.git/hooks` or an absolute path to it) as
|
|
451
|
-
// equivalent to unset. A user who has accepted the default has not made an
|
|
452
|
-
// active choice that justifies "leave alone" semantics; without this branch
|
|
453
|
-
// an install that happens to land while `.git/hooks` is already in play
|
|
454
|
-
// leaves policy gates inactive permanently.
|
|
455
|
-
const isGitDefault =
|
|
456
|
-
!currentValue ||
|
|
457
|
-
currentValue === '.git/hooks' ||
|
|
458
|
-
currentValue === path.join(cwd, '.git', 'hooks') ||
|
|
459
|
-
currentValue.replace(/\/+$/, '').endsWith(`${path.sep}.git${path.sep}hooks`);
|
|
460
|
-
if (!isGitDefault) {
|
|
461
|
-
return {
|
|
462
|
-
status: 'warning',
|
|
463
|
-
message: `core.hooksPath is set to '${currentValue}'. Leaving alone. Set to '.beads/hooks' to activate Construct policy gates.`,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
const result = spawnSync('git', ['config', 'core.hooksPath', desired], { cwd, stdio: 'pipe', encoding: 'utf8' });
|
|
467
|
-
if (result.status !== 0) {
|
|
468
|
-
return {
|
|
469
|
-
status: 'error',
|
|
470
|
-
message: `failed to set core.hooksPath: ${(result.stderr || '').trim() || 'unknown error'}`,
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
return {
|
|
474
|
-
status: 'set',
|
|
475
|
-
message: `core.hooksPath set to .beads/hooks (activates pre-commit secret-scan + Construct policy gates + beads dispatcher)`,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// $HOME/.construct/lib/hooks/* is the resolution path for Claude Code
|
|
480
|
-
// Stop-hook commands. Absent symlink → every Stop hook fails silently,
|
|
481
|
-
// including stop-notify (dashboard cost feed). Idempotent: matching
|
|
482
|
-
// symlink left alone, stale symlink replaced, real dir refused.
|
|
256
|
+
export { ensureGitHooksPath } from './git-hooks-path.mjs';
|
|
483
257
|
|
|
484
258
|
export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
|
|
485
259
|
const target = path.join(homeDir, '.construct', 'lib');
|
|
@@ -501,14 +275,37 @@ export function ensureLibSymlink({ homeDir = HOME, rootDir = ROOT_DIR } = {}) {
|
|
|
501
275
|
return {
|
|
502
276
|
status: 'conflict',
|
|
503
277
|
target,
|
|
504
|
-
message: `${target} exists and is not a symlink — leaving alone
|
|
278
|
+
message: `${target} exists and is not a symlink — leaving alone.`,
|
|
505
279
|
};
|
|
506
280
|
}
|
|
507
281
|
|
|
282
|
+
const VALID_SCOPES = new Set(['project', 'user', 'both']);
|
|
283
|
+
const DEFAULT_SCOPE = 'project';
|
|
284
|
+
|
|
285
|
+
function parseScopeFlag(args) {
|
|
286
|
+
for (const a of args) {
|
|
287
|
+
if (a === '--scope') return { invalid: '--scope requires a value (project|user|both)' };
|
|
288
|
+
if (!a.startsWith('--scope=')) continue;
|
|
289
|
+
const v = a.slice('--scope='.length);
|
|
290
|
+
if (!VALID_SCOPES.has(v)) return { invalid: `--scope=${v} is not one of project|user|both` };
|
|
291
|
+
return { scope: v };
|
|
292
|
+
}
|
|
293
|
+
return { scope: DEFAULT_SCOPE };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function printProjectScopeGuidance() {
|
|
297
|
+
console.log('Construct install — scope: project (no-op)');
|
|
298
|
+
console.log('────────────────────────────────────────');
|
|
299
|
+
console.log('Project artifacts are written by `construct init` inside a repo.');
|
|
300
|
+
console.log('To install machine-scope state on this user account:');
|
|
301
|
+
console.log(' construct install --scope=user');
|
|
302
|
+
console.log('To do both project (guidance) and user-scope install:');
|
|
303
|
+
console.log(' construct install --scope=both');
|
|
304
|
+
}
|
|
305
|
+
|
|
508
306
|
export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME } = {}) {
|
|
509
307
|
const argSet = new Set(args);
|
|
510
308
|
const isYes = argSet.has('--yes');
|
|
511
|
-
const skipDocker = argSet.has('--no-docker');
|
|
512
309
|
const reconfigure = argSet.has('--reconfigure');
|
|
513
310
|
|
|
514
311
|
if (argSet.has('--help') || argSet.has('-h')) {
|
|
@@ -516,19 +313,39 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
516
313
|
return;
|
|
517
314
|
}
|
|
518
315
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
316
|
+
const KNOWN_FLAGS = new Set(['--yes', '--no-launch-agent', '--reconfigure', '--help', '-h']);
|
|
317
|
+
const unknownFlags = args.filter((a) => {
|
|
318
|
+
if (!a.startsWith('-')) return false;
|
|
319
|
+
if (a.startsWith('--scope=') || a === '--scope') return false;
|
|
320
|
+
return !KNOWN_FLAGS.has(a);
|
|
321
|
+
});
|
|
524
322
|
if (unknownFlags.length) {
|
|
525
323
|
console.error(`Unknown flag(s): ${unknownFlags.join(', ')}`);
|
|
526
324
|
printHelp();
|
|
527
325
|
throw new Error(`Unknown setup flag(s): ${unknownFlags.join(', ')}`);
|
|
528
326
|
}
|
|
529
327
|
|
|
328
|
+
const scopeResult = parseScopeFlag(args);
|
|
329
|
+
if (scopeResult.invalid) {
|
|
330
|
+
console.error(scopeResult.invalid);
|
|
331
|
+
printHelp();
|
|
332
|
+
throw new Error(`Invalid setup scope: ${scopeResult.invalid}`);
|
|
333
|
+
}
|
|
334
|
+
const scope = scopeResult.scope;
|
|
335
|
+
|
|
336
|
+
if (scope === 'project') {
|
|
337
|
+
printProjectScopeGuidance();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (scope === 'both') {
|
|
342
|
+
printProjectScopeGuidance();
|
|
343
|
+
console.log('');
|
|
344
|
+
}
|
|
345
|
+
|
|
530
346
|
console.log('Construct setup');
|
|
531
347
|
console.log('────────────────');
|
|
348
|
+
console.log(`Scope: ${scope}`);
|
|
532
349
|
|
|
533
350
|
const envPath = ensureUserConfig(homeDir);
|
|
534
351
|
const opencodePath = ensureOpenCodeConfig();
|
|
@@ -538,68 +355,25 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
538
355
|
console.log(`OpenCode config: ${opencodePath}`);
|
|
539
356
|
if (libLink.status === 'created' || libLink.status === 'replaced') {
|
|
540
357
|
console.log(`Hook lib link: ${libLink.target} → ${libLink.source} (${libLink.status})`);
|
|
541
|
-
} else if (libLink.status === 'kept') {
|
|
542
|
-
console.log(`Hook lib link: ${libLink.target} already in place`);
|
|
543
|
-
} else if (libLink.status === 'conflict') {
|
|
544
|
-
console.log(`Hook lib link: ${libLink.message}`);
|
|
545
358
|
}
|
|
546
359
|
warnIfGlobalCommandIsUnavailable();
|
|
547
360
|
|
|
548
361
|
const cmInstall = ensureCmInstalled({ env: process.env });
|
|
549
|
-
if (cmInstall.status === 'installed') {
|
|
550
|
-
console.log(
|
|
551
|
-
} else if (cmInstall.status === 'available') {
|
|
552
|
-
console.log('Memory CLI: cm available');
|
|
362
|
+
if (cmInstall.status === 'installed' || cmInstall.status === 'available') {
|
|
363
|
+
console.log(`Memory CLI: ${cmInstall.message}`);
|
|
553
364
|
} else {
|
|
554
365
|
console.log(`Memory CLI: ${cmInstall.message}`);
|
|
555
366
|
if (cmInstall.installCommand) console.log(` Install with: ${cmInstall.installCommand}`);
|
|
556
367
|
}
|
|
557
368
|
|
|
558
369
|
const cassInstall = ensureCassInstalled({ env: process.env });
|
|
559
|
-
if (cassInstall.status === 'installed') {
|
|
370
|
+
if (cassInstall.status === 'installed' || cassInstall.status === 'available') {
|
|
560
371
|
console.log(`Session search: ${cassInstall.message}`);
|
|
561
|
-
} else if (cassInstall.status === 'available') {
|
|
562
|
-
console.log('Session search: cass available');
|
|
563
372
|
} else {
|
|
564
373
|
console.log(`Session search: ${cassInstall.message}`);
|
|
565
374
|
if (cassInstall.installCommand) console.log(` Install with: ${cassInstall.installCommand}`);
|
|
566
375
|
}
|
|
567
376
|
|
|
568
|
-
// Local Postgres — consent-driven. Interactive default-yes; --yes accepts
|
|
569
|
-
// without prompting. Skipped when --no-docker, when DATABASE_URL is already
|
|
570
|
-
// set (caller has an external DB), or when user declines.
|
|
571
|
-
|
|
572
|
-
const dockerRunner = detectDockerCompose();
|
|
573
|
-
const dockerAvailable = !skipDocker && Boolean(dockerRunner);
|
|
574
|
-
let serviceResult = {
|
|
575
|
-
status: 'skipped',
|
|
576
|
-
databaseUrl: process.env.DATABASE_URL || '',
|
|
577
|
-
message: skipDocker ? 'Docker service setup skipped by flag.' : 'Docker not detected — skipping local Postgres.',
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
if (dockerAvailable) {
|
|
581
|
-
const pgConsent = await consentToInstall({
|
|
582
|
-
name: 'postgres',
|
|
583
|
-
isYes,
|
|
584
|
-
force: reconfigure,
|
|
585
|
-
alreadyConfigured: Boolean(process.env.DATABASE_URL),
|
|
586
|
-
alreadyConfiguredNote: 'DATABASE_URL already set — using external database.',
|
|
587
|
-
envPath,
|
|
588
|
-
});
|
|
589
|
-
if (pgConsent.decision) {
|
|
590
|
-
writeLocalPostgresCompose(homeDir);
|
|
591
|
-
serviceResult = startManagedPostgres({ homeDir, env: process.env });
|
|
592
|
-
console.log(`Postgres: ${serviceResult.status === 'ok' ? 'started locally' : serviceResult.message}`);
|
|
593
|
-
} else {
|
|
594
|
-
console.log(`Postgres: skipped (${pgConsent.note})`);
|
|
595
|
-
}
|
|
596
|
-
} else if (skipDocker) {
|
|
597
|
-
console.log('Postgres: skipped (--no-docker)');
|
|
598
|
-
} else {
|
|
599
|
-
console.log('Postgres: skipped (Docker not detected — local Postgres unavailable)');
|
|
600
|
-
console.log(` ${dockerInstallHint()}`);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
377
|
const telemetryResult = process.env.CONSTRUCT_TELEMETRY_URL
|
|
604
378
|
? { status: 'configured', note: `remote export configured (${process.env.CONSTRUCT_TELEMETRY_URL})` }
|
|
605
379
|
: { status: 'local', note: 'local JSONL traces in .cx/traces; remote export optional' };
|
|
@@ -607,13 +381,6 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
607
381
|
|
|
608
382
|
fs.mkdirSync(path.dirname(defaultVectorIndexPath(homeDir)), { recursive: true });
|
|
609
383
|
|
|
610
|
-
// Pre-warm the embedding model so the first agent query doesn't stall on
|
|
611
|
-
// a one-time ONNX download. Same pattern as `supabase start` pre-pulling
|
|
612
|
-
// images and `playwright install` pre-fetching browsers — make the wait
|
|
613
|
-
// visible during setup, not during user-facing work. Best-effort: a
|
|
614
|
-
// download failure here degrades gracefully via the engine's hash fallback
|
|
615
|
-
// and is non-fatal to setup.
|
|
616
|
-
|
|
617
384
|
try {
|
|
618
385
|
const warm = await warmupEmbeddingModel({ env: process.env });
|
|
619
386
|
if (warm.degraded) {
|
|
@@ -625,12 +392,7 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
625
392
|
console.log(`Embeddings: warmup skipped (${err?.message || 'unknown error'}) — model will load on first use`);
|
|
626
393
|
}
|
|
627
394
|
|
|
628
|
-
|
|
629
|
-
// evaluate all configured providers, pick the lowest-cost model per tier,
|
|
630
|
-
// and write to config.env. On subsequent runs the preference is persisted
|
|
631
|
-
// so the prompt is skipped.
|
|
632
|
-
|
|
633
|
-
const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference, formatCheapestProviderMessage } =
|
|
395
|
+
const { isCheapestProviderEnabled, selectCheapestForAllTiers, setCheapestProviderPreference } =
|
|
634
396
|
await import('./model-cheapest-provider.mjs');
|
|
635
397
|
const cheapestAlreadyEnabled = isCheapestProviderEnabled(envPath, { env: process.env });
|
|
636
398
|
if (!cheapestAlreadyEnabled) {
|
|
@@ -657,166 +419,54 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
657
419
|
const label = selections[tier]?.providerLabel || '';
|
|
658
420
|
console.log(` ${tier.padEnd(11)} ${model} (${label})`);
|
|
659
421
|
}
|
|
660
|
-
} else {
|
|
661
|
-
console.log('\nCheapest provider: no configured providers found — nothing to apply.');
|
|
662
422
|
}
|
|
663
423
|
setCheapestProviderPreference(envPath, true);
|
|
664
424
|
} catch (err) {
|
|
665
425
|
console.log(`Cheapest provider: skipped (${err?.message || 'unknown error'})`);
|
|
666
426
|
}
|
|
667
|
-
} else {
|
|
668
|
-
console.log(`Cheapest provider: skipped (${cheapestConsent.note})`);
|
|
669
427
|
}
|
|
670
|
-
} else {
|
|
671
|
-
console.log('Cheapest provider: already enabled — skipping prompt.');
|
|
672
428
|
}
|
|
673
429
|
|
|
674
|
-
const hooksResult = ensureGitHooksPath({ cwd: process.cwd() });
|
|
675
|
-
if (hooksResult.status === 'set') console.log(`Git hooks: ${hooksResult.message}`);
|
|
676
|
-
else if (hooksResult.status === 'ok') console.log('Git hooks: wired');
|
|
677
|
-
else if (hooksResult.status === 'warning') console.log(`Git hooks: ${hooksResult.message}`);
|
|
678
|
-
else if (hooksResult.status === 'error') console.log(`Git hooks: ${hooksResult.message}`);
|
|
679
|
-
|
|
680
|
-
// Ensure workspace directory with docs lanes exists
|
|
681
430
|
ensureWorkspace(homeDir);
|
|
682
431
|
|
|
683
432
|
const managedValues = await buildManagedSetupValues({
|
|
684
433
|
homeDir,
|
|
685
434
|
env: process.env,
|
|
686
|
-
databaseUrl: serviceResult.databaseUrl,
|
|
687
435
|
});
|
|
688
436
|
writeEnvValues(envPath, managedValues);
|
|
437
|
+
|
|
689
438
|
let pressureGuardAgent = null;
|
|
690
439
|
let pressureGuardLoad = null;
|
|
691
|
-
if (process.platform === 'darwin') {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
440
|
+
if (process.platform === 'darwin' && !argSet.has('--no-launch-agent')) {
|
|
441
|
+
const laConsent = await consentToInstall({
|
|
442
|
+
name: 'launchagent',
|
|
443
|
+
question: 'Install background pressure-release service (macOS LaunchAgent)?',
|
|
444
|
+
isYes,
|
|
445
|
+
force: reconfigure,
|
|
446
|
+
alreadyConfigured: fs.existsSync(path.join(homeDir, 'Library', 'LaunchAgents', 'dev.construct.pressure-release.plist')),
|
|
447
|
+
alreadyConfiguredNote: 'Background pressure-release service already installed.',
|
|
448
|
+
envPath,
|
|
697
449
|
});
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}`);
|
|
707
|
-
if (managedValues.CONSTRUCT_TELEMETRY_URL) console.log(` Trace URL: ${managedValues.CONSTRUCT_TELEMETRY_URL}`);
|
|
708
|
-
console.log(` Pressure guard: every ${managedValues.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS}s, swap threshold ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB`);
|
|
709
|
-
if (serviceResult.status === 'ok') {
|
|
710
|
-
console.log(` Postgres: ${serviceResult.databaseUrl}`);
|
|
711
|
-
console.log(` Compose file: ${serviceResult.composePath}`);
|
|
712
|
-
} else {
|
|
713
|
-
console.log(` Postgres: ${serviceResult.message}`);
|
|
714
|
-
}
|
|
715
|
-
if (pressureGuardAgent?.plistPath) {
|
|
716
|
-
console.log(` LaunchAgent: ${pressureGuardAgent.plistPath}`);
|
|
717
|
-
console.log(` LaunchAgent load: ${pressureGuardLoad?.loaded ? 'active' : pressureGuardLoad?.reason || 'pending manual load'}`);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const sqlClient = createSqlClient({ ...process.env, ...managedValues });
|
|
721
|
-
if (sqlClient) {
|
|
722
|
-
let readyClient = null;
|
|
723
|
-
let sqlClientClosed = false;
|
|
724
|
-
try {
|
|
725
|
-
if (serviceResult.status === 'ok') {
|
|
726
|
-
await closeSqlClient(sqlClient);
|
|
727
|
-
sqlClientClosed = true;
|
|
728
|
-
await waitForSqlReady({ ...process.env, ...managedValues });
|
|
729
|
-
}
|
|
730
|
-
readyClient = createSqlClient({ ...process.env, ...managedValues });
|
|
731
|
-
const migration = await import('node:fs/promises').then((fs) => fs.readFile(new URL('../db/schema/001_init.sql', import.meta.url), 'utf8'));
|
|
732
|
-
await readyClient.unsafe(migration);
|
|
733
|
-
console.log('\nPostgres schema initialized.');
|
|
734
|
-
|
|
735
|
-
// Restore data from the most recent stash (if any) before syncing.
|
|
736
|
-
if (serviceResult.status === 'ok') {
|
|
737
|
-
const restore = restoreConstructDb({ homeDir });
|
|
738
|
-
if (restore.status === 'restored') {
|
|
739
|
-
console.log(`Construct DB restored from stash: ${restore.stashPath}`);
|
|
740
|
-
} else if (restore.status === 'no-stash') {
|
|
741
|
-
console.log('No stash found — starting with empty construct DB.');
|
|
742
|
-
} else if (restore.status !== 'no-stash') {
|
|
743
|
-
console.log(`Construct DB restore: ${restore.status}`);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const syncResult = await syncFileStateToSql(rootDir, { env: { ...process.env, ...managedValues }, project: 'construct' });
|
|
748
|
-
console.log(`Hybrid storage sync: ${syncResult.status}`);
|
|
749
|
-
if (syncResult.embeddingModel) console.log(`Embedding model: ${syncResult.embeddingModel}`);
|
|
750
|
-
|
|
751
|
-
// When setup runs inside a downstream project (cwd is not the
|
|
752
|
-
// Construct package itself), seed the vector index from the
|
|
753
|
-
// project's existing docs / AGENTS / plan so the agent has
|
|
754
|
-
// context to compare new intake against from day one. No-op
|
|
755
|
-
// when there's nothing project-like in cwd.
|
|
756
|
-
|
|
757
|
-
const projectCwd = process.cwd();
|
|
758
|
-
const isDownstreamProject = projectCwd !== rootDir && (
|
|
759
|
-
fs.existsSync(path.join(projectCwd, 'docs')) ||
|
|
760
|
-
fs.existsSync(path.join(projectCwd, 'AGENTS.md')) ||
|
|
761
|
-
fs.existsSync(path.join(projectCwd, 'package.json'))
|
|
762
|
-
);
|
|
763
|
-
if (isDownstreamProject) {
|
|
764
|
-
try {
|
|
765
|
-
const projectName = path.basename(projectCwd);
|
|
766
|
-
const projectSync = await syncFileStateToSql(projectCwd, {
|
|
767
|
-
env: { ...process.env, ...managedValues },
|
|
768
|
-
project: projectName,
|
|
769
|
-
});
|
|
770
|
-
console.log(`Project corpus indexed: ${projectName} (${projectSync.documentsSynced || 0} docs, ${projectSync.embeddingsSynced || 0} embeddings) — agent can now compare new intake against existing material.`);
|
|
771
|
-
} catch (err) {
|
|
772
|
-
console.log(`Project corpus index skipped: ${err?.message || 'unknown error'} — run \`construct ingest\` later to seed manually.`);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
} catch (error) {
|
|
776
|
-
console.log(`Hybrid storage init failed: ${error?.message || 'unknown error'}`);
|
|
777
|
-
} finally {
|
|
778
|
-
await closeSqlClient(readyClient).catch(() => {});
|
|
779
|
-
if (!sqlClientClosed) await closeSqlClient(sqlClient);
|
|
780
|
-
}
|
|
450
|
+
if (laConsent.decision) {
|
|
451
|
+
pressureGuardAgent = installPressureGuardLaunchAgent({
|
|
452
|
+
homeDir,
|
|
453
|
+
rootDir,
|
|
454
|
+
intervalSeconds: Number(managedValues.CONSTRUCT_PRESSURE_GUARD_INTERVAL_SECONDS || 300),
|
|
455
|
+
nodePath: process.execPath,
|
|
456
|
+
});
|
|
457
|
+
pressureGuardLoad = loadPressureGuardLaunchAgent({ plistPath: pressureGuardAgent.plistPath });
|
|
781
458
|
}
|
|
782
459
|
}
|
|
783
460
|
|
|
784
461
|
if (isYes) {
|
|
785
462
|
runConstruct(['mcp', 'add', 'memory', '--auto'], { optional: true });
|
|
786
463
|
runConstruct(['mcp', 'add', 'github', '--auto'], { optional: true });
|
|
787
|
-
} else {
|
|
788
|
-
console.log('\nManaged defaults written:');
|
|
789
|
-
console.log(` Deployment mode: ${getDeploymentMode(process.env) || DEFAULT_DEPLOYMENT_MODE} (set in construct.config.json — runtime env override available via ${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
790
|
-
console.log(` Vector index: ${managedValues.CONSTRUCT_VECTOR_INDEX_PATH}`);
|
|
791
|
-
console.log(` Trace backend: ${managedValues.CONSTRUCT_TRACE_BACKEND}${managedValues.CONSTRUCT_TELEMETRY_URL ? ` (${managedValues.CONSTRUCT_TELEMETRY_URL})` : ''}`);
|
|
792
|
-
console.log(` Pressure guard: swap ${managedValues.CONSTRUCT_PRESSURE_GUARD_SWAP_GB} GiB, opencode max ${managedValues.CONSTRUCT_PRESSURE_GUARD_MAX_OPENCODE}`);
|
|
793
|
-
console.log('\nFor unattended setup, including local Postgres when Docker is running:');
|
|
794
|
-
console.log(' construct install --yes');
|
|
795
464
|
}
|
|
796
465
|
|
|
797
|
-
|
|
798
|
-
// user-scope surface (opencode/claude/codex/copilot) or a fresh machine fails
|
|
799
|
-
// cross-surface parity. Plain sync writes global hooks + project tier but not
|
|
800
|
-
// the front-door agent — `--global` is what populates it, so run both.
|
|
801
|
-
|
|
802
|
-
runConstruct(['sync']);
|
|
466
|
+
runConstruct(['sync', '--quiet']);
|
|
803
467
|
runConstruct(['sync', '--global']);
|
|
804
468
|
runConstruct(['doctor']);
|
|
805
469
|
|
|
806
|
-
// Profile the CWD so per-host skill filtering layers downstream have
|
|
807
|
-
// a cached, host-agnostic signal to work from. Non-fatal on failure.
|
|
808
|
-
try {
|
|
809
|
-
const { detectProjectProfile, writeProfile } = await import('./project-profile.mjs');
|
|
810
|
-
const profile = detectProjectProfile(process.cwd());
|
|
811
|
-
if (profile.tags.length > 0) {
|
|
812
|
-
const profilePath = writeProfile(profile, process.cwd());
|
|
813
|
-
console.log(`\nProject profile: ${profile.tags.join(', ')}`);
|
|
814
|
-
console.log(` Saved to ${profilePath}`);
|
|
815
|
-
console.log(` Run \`construct skills scope\` to see which installed skills apply to this project.`);
|
|
816
|
-
}
|
|
817
|
-
} catch { /* best effort */ }
|
|
818
|
-
|
|
819
|
-
// ── Summary panel ────────────────────────────────────────────────────────
|
|
820
470
|
const setupTs = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
|
|
821
471
|
const setupLogPath = path.join(HOME, '.cx', `setup-${setupTs}.log`);
|
|
822
472
|
try {
|
|
@@ -826,42 +476,21 @@ export async function runSetup({ rootDir = ROOT_DIR, args = [], homeDir = HOME }
|
|
|
826
476
|
`Config: ${getUserEnvPath(HOME)}`,
|
|
827
477
|
`OpenCode: ${getCanonicalOpenCodeConfigPath(HOME)}`,
|
|
828
478
|
];
|
|
829
|
-
if (managedValues?.DATABASE_URL) {
|
|
830
|
-
logLines.push(`Database: ${managedValues.DATABASE_URL.replace(/:\/\/[^@]+@/, '://<credentials>@')}`);
|
|
831
|
-
}
|
|
832
479
|
fs.writeFileSync(setupLogPath, logLines.join('\n') + '\n');
|
|
833
480
|
} catch { /* best effort */ }
|
|
834
481
|
|
|
835
482
|
console.log('\n────────────────────────────────────');
|
|
836
483
|
console.log('Setup complete.');
|
|
837
484
|
|
|
838
|
-
// Local-service summary — surfaces running local services. Same pattern
|
|
839
|
-
// as `supabase start`, which prints all local URLs + keys after spin-up.
|
|
840
|
-
|
|
841
|
-
const hasLocalPostgres = serviceResult?.status === 'ok' && serviceResult?.databaseUrl?.includes('127.0.0.1');
|
|
842
|
-
const vectorBackend = hasLocalPostgres
|
|
843
|
-
? { label: 'Postgres + pgvector', detail: `${serviceResult.databaseUrl} (384d embeddings, ${managedValues.CONSTRUCT_VECTOR_MODEL})` }
|
|
844
|
-
: { label: 'JSON fallback', detail: `${managedValues.CONSTRUCT_VECTOR_INDEX_PATH} (${managedValues.CONSTRUCT_VECTOR_MODEL})` };
|
|
845
|
-
|
|
846
485
|
console.log('\nLocal services:');
|
|
847
486
|
console.log(' Traces: local JSONL (.cx/traces)');
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (hasLocalPostgres) {
|
|
851
|
-
console.log(` Postgres: ${serviceResult.databaseUrl}`);
|
|
852
|
-
} else if (process.env.DATABASE_URL) {
|
|
853
|
-
console.log(` Postgres: external (${process.env.DATABASE_URL.replace(/:\/\/[^@]+@/, '://<credentials>@')})`);
|
|
854
|
-
} else {
|
|
855
|
-
console.log(` Postgres: not running — vector retrieval will use JSON fallback`);
|
|
856
|
-
}
|
|
857
|
-
console.log(` Vector: ${vectorBackend.label}`);
|
|
858
|
-
console.log(` ${vectorBackend.detail}`);
|
|
487
|
+
console.log(` Vector: LanceDB (embedded)`);
|
|
488
|
+
console.log(` ${managedValues.CONSTRUCT_LANCEDB_PATH}`);
|
|
859
489
|
console.log(' Credentials are saved to ~/.construct/config.env for later reference.');
|
|
860
490
|
|
|
861
491
|
console.log('\nNext steps:');
|
|
862
492
|
console.log(' construct provider add github # Connect GitHub repository data');
|
|
863
493
|
console.log(' construct doctor # Verify all systems');
|
|
864
|
-
console.log(' construct evals retrieval # Baseline retrieval quality');
|
|
865
494
|
console.log(`\nSetup log: ${setupLogPath}`);
|
|
866
495
|
}
|
|
867
496
|
|
|
@@ -872,17 +501,12 @@ function ensureUserConfig(homeDir = HOME) {
|
|
|
872
501
|
return envPath;
|
|
873
502
|
}
|
|
874
503
|
|
|
875
|
-
/**
|
|
876
|
-
* Create ~/.construct/workspace/ with the standard docs lane structure.
|
|
877
|
-
* Fallback target for embed output that isn't repo-specific.
|
|
878
|
-
*/
|
|
879
504
|
export function ensureWorkspace(homeDir = HOME) {
|
|
880
505
|
const wsPath = path.join(homeDir, '.construct', 'workspace');
|
|
881
506
|
const docsPath = path.join(wsPath, 'docs');
|
|
882
507
|
for (const lane of WORKSPACE_DOCS_LANES) {
|
|
883
508
|
fs.mkdirSync(path.join(docsPath, lane), { recursive: true });
|
|
884
509
|
}
|
|
885
|
-
// Ensure top-level workspace files exist
|
|
886
510
|
const snapshotPath = path.join(wsPath, 'snapshot.md');
|
|
887
511
|
if (!fs.existsSync(snapshotPath)) fs.writeFileSync(snapshotPath, '# Snapshot\n\nNo snapshot yet.\n');
|
|
888
512
|
const roadmapPath = path.join(wsPath, 'roadmap.md');
|