@geraldmaron/construct 1.2.1 → 1.2.3
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 +6 -6
- package/apps/chat/engine/ai-sdk-agent.mjs +183 -0
- package/apps/chat/engine/loop-driver.mjs +211 -0
- package/apps/chat/engine/models.mjs +122 -0
- package/apps/chat/engine/provider-adapters.mjs +171 -0
- package/apps/chat/engine/tools/permission.mjs +54 -0
- package/apps/chat/engine/tools/primitives.mjs +180 -0
- package/apps/chat/engine/tools/registry.mjs +122 -0
- package/apps/chat/engine/turn-controls.mjs +70 -0
- package/bin/construct +93 -79
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +10 -10
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/cli-commands.mjs +4 -6
- package/lib/cli-service-inventory.mjs +1 -1
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +8 -78
- package/lib/config/legacy-config-migration.mjs +59 -0
- package/lib/config/project-config.mjs +95 -5
- package/lib/config/schema.mjs +9 -19
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +1 -1
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/cli.mjs +7 -5
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -37
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +6 -5
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +35 -81
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +5 -32
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +1 -1
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-env.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +24 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/webhook.mjs +2 -1
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +21 -11
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +4 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- package/templates/workflows/new-feature.yml +8 -8
- package/lib/intake/legacy-paths.mjs +0 -5
package/lib/auto-docs.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* buildSite() is retained for compatibility; the public site now renders docs/
|
|
13
13
|
* directly through Next.js (apps/docs/) and buildFumadocsReference() emits
|
|
14
|
-
* generated reference Markdown into docs/reference/.
|
|
14
|
+
* generated reference Markdown into docs/guides/reference/.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import fs from 'node:fs';
|
|
@@ -78,7 +78,7 @@ function buildCoreDocsContract() {
|
|
|
78
78
|
'| `.cx/context.md` | Human-readable resumable project context | Active work, decisions, architecture assumptions, or open questions change |',
|
|
79
79
|
'| `.cx/context.json` | Machine-readable resumable context | Context state needs to stay in sync with `.cx/context.md` |',
|
|
80
80
|
'| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
|
|
81
|
-
'| `docs/concepts/architecture.mdx` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
81
|
+
'| `docs/guides/concepts/architecture.mdx` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
|
|
82
82
|
'',
|
|
83
83
|
'`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed; durable work belongs in the tracker (Beads or external).',
|
|
84
84
|
'',
|
|
@@ -191,7 +191,7 @@ function buildAgentsTable(rootDir) {
|
|
|
191
191
|
// --- public API ---
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
* Regenerate all AUTO regions in README.md, docs/README.md, and docs/concepts/architecture.mdx.
|
|
194
|
+
* Regenerate all AUTO regions in README.md, docs/README.md, and docs/guides/concepts/architecture.mdx.
|
|
195
195
|
* Returns { changed: string[], checked: boolean }.
|
|
196
196
|
* With check:true writes nothing and sets changed to files that would differ.
|
|
197
197
|
*/
|
|
@@ -209,7 +209,7 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
|
|
|
209
209
|
},
|
|
210
210
|
},
|
|
211
211
|
{
|
|
212
|
-
file: path.join(rootDir, 'docs', 'concepts', 'architecture.mdx'),
|
|
212
|
+
file: path.join(rootDir, 'docs', 'guides', 'concepts', 'architecture.mdx'),
|
|
213
213
|
regions: {
|
|
214
214
|
agents: buildAgentsTable(rootDir),
|
|
215
215
|
},
|
|
@@ -268,7 +268,7 @@ export function checkDocsCoverage({ rootDir } = {}) {
|
|
|
268
268
|
// Build a combined corpus: docs/README.md + every linked guide file
|
|
269
269
|
let corpus = docsReadme;
|
|
270
270
|
for (const target of linkTargets) {
|
|
271
|
-
if (!target.startsWith('./
|
|
271
|
+
if (!target.startsWith('./guides/cookbook/')) continue;
|
|
272
272
|
const filePath = path.join(rootDir, 'docs', target.replace(/^\.\//, ''));
|
|
273
273
|
const content = readFile(filePath);
|
|
274
274
|
if (content) corpus += '\n' + content;
|
|
@@ -423,7 +423,7 @@ function renderAgentsPage(rootDir) {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
/**
|
|
426
|
-
* Emit MDX reference pages into docs/reference/ for the Next.js docs site
|
|
426
|
+
* Emit MDX reference pages into docs/guides/reference/ for the Next.js docs site
|
|
427
427
|
* in apps/docs/. Mirrors the data sources behind AUTO regions and buildSite()
|
|
428
428
|
* but writes Markdown files the docs app can pull in directly (no MkDocs
|
|
429
429
|
* intermediate). Name kept for backwards compatibility with callers that
|
|
@@ -432,7 +432,7 @@ function renderAgentsPage(rootDir) {
|
|
|
432
432
|
*/
|
|
433
433
|
export function buildFumadocsReference({ rootDir } = {}) {
|
|
434
434
|
rootDir = rootDir ?? process.cwd();
|
|
435
|
-
const refDir = path.join(rootDir, 'docs', 'reference');
|
|
435
|
+
const refDir = path.join(rootDir, 'docs', 'guides', 'reference');
|
|
436
436
|
const cliDir = path.join(refDir, 'cli');
|
|
437
437
|
fs.mkdirSync(cliDir, { recursive: true });
|
|
438
438
|
|
|
@@ -468,7 +468,7 @@ export function buildFumadocsReference({ rootDir } = {}) {
|
|
|
468
468
|
const slug = slugify(cat);
|
|
469
469
|
const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
|
|
470
470
|
if (!commands.length) return null;
|
|
471
|
-
return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
471
|
+
return `- [${cat}](/guides/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
472
472
|
}).filter(Boolean),
|
|
473
473
|
'',
|
|
474
474
|
].join('\n');
|
|
@@ -499,7 +499,7 @@ export function buildFumadocsReference({ rootDir } = {}) {
|
|
|
499
499
|
|
|
500
500
|
export function checkFumadocsReferenceDrift({ rootDir } = {}) {
|
|
501
501
|
rootDir = rootDir ?? process.cwd();
|
|
502
|
-
const refDir = path.join(rootDir, 'docs', 'reference');
|
|
502
|
+
const refDir = path.join(rootDir, 'docs', 'guides', 'reference');
|
|
503
503
|
const cliDir = path.join(refDir, 'cli');
|
|
504
504
|
const drift = [];
|
|
505
505
|
|
|
@@ -534,7 +534,7 @@ export function checkFumadocsReferenceDrift({ rootDir } = {}) {
|
|
|
534
534
|
const slug = slugify(cat);
|
|
535
535
|
const commands = CLI_COMMANDS_BY_CATEGORY[cat] ?? [];
|
|
536
536
|
if (!commands.length) return null;
|
|
537
|
-
return `- [${cat}](/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
537
|
+
return `- [${cat}](/guides/reference/cli/${slug}) — ${commands.length} command${commands.length === 1 ? '' : 's'}`;
|
|
538
538
|
}).filter(Boolean),
|
|
539
539
|
'',
|
|
540
540
|
].join('\n');
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
-
import os from 'node:os';
|
|
18
17
|
import { registerResource, getResource } from './resources.mjs';
|
|
18
|
+
import { cacheDir } from '../config/xdg.mjs';
|
|
19
19
|
|
|
20
20
|
function probeCommandVersion(cmd, args = ['--version']) {
|
|
21
21
|
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 3000 });
|
|
@@ -56,8 +56,8 @@ async function probePostgresContainer() {
|
|
|
56
56
|
|
|
57
57
|
async function probeEmbeddingModel() {
|
|
58
58
|
const cachePath = join(
|
|
59
|
-
|
|
60
|
-
'
|
|
59
|
+
cacheDir(),
|
|
60
|
+
'embeddings',
|
|
61
61
|
'Xenova', 'all-MiniLM-L6-v2', 'onnx', 'model_quantized.onnx'
|
|
62
62
|
);
|
|
63
63
|
if (existsSync(cachePath)) {
|
package/lib/boundary.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Verifies a parent Construct instance is reachable, validates an HMAC
|
|
5
5
|
* signature over `childInstanceId|nonce` when a shared secret is configured,
|
|
6
|
-
* and persists the binding to
|
|
6
|
+
* and persists the binding to boundary.json in the XDG config dir (mode 0600). A
|
|
7
7
|
* different parent rotates the registration only when explicitly allowed via
|
|
8
8
|
* CONSTRUCT_BOUNDARY_ALLOW_OVERRIDE=1; prior configs are archived alongside
|
|
9
9
|
* the active one. Exposed as a standalone module so the dashboard endpoint
|
|
@@ -16,11 +16,12 @@ import { homedir } from 'node:os';
|
|
|
16
16
|
import { createHmac, timingSafeEqual, randomBytes } from 'node:crypto';
|
|
17
17
|
import { request as httpRequest } from 'node:http';
|
|
18
18
|
import { request as httpsRequest } from 'node:https';
|
|
19
|
+
import { configDir } from './config/xdg.mjs';
|
|
19
20
|
|
|
20
21
|
export const BOUNDARY_VERSION = '1.0';
|
|
21
22
|
|
|
22
23
|
function boundaryDir(home = homedir()) {
|
|
23
|
-
return
|
|
24
|
+
return configDir(home);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export function boundaryConfigPath(home = homedir()) {
|
package/lib/brand-prose.mjs
CHANGED
|
@@ -37,7 +37,7 @@ const EXCLUDE_PATH =
|
|
|
37
37
|
/(node_modules|\.git|audit-artifacts|scripts\/audit|CHANGELOG\.md|fonts\/legacy\/|\.next\/|\/out\/|tests\/e2e\/reports\/|package-lock\.json)/;
|
|
38
38
|
|
|
39
39
|
const DOC_EXEMPT =
|
|
40
|
-
/(?:^|\/)(docs\/STYLE\.md|docs\/reference\/branding\.md|templates\/distribution\/fonts\/README\.md)$/;
|
|
40
|
+
/(?:^|\/)(docs\/STYLE\.md|docs\/guides\/reference\/branding\.md|templates\/distribution\/fonts\/README\.md)$/;
|
|
41
41
|
|
|
42
42
|
const HOOK_SCOPED =
|
|
43
43
|
/^(docs\/|skills\/|specialists\/|personas\/|templates\/|rules\/|apps\/(dashboard|docs|chat)\/)/;
|
package/lib/cache-governor.js
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
13
|
import { normalizeCostEntry } from './cost.mjs';
|
|
14
|
+
import { doctorRoot } from './config/xdg.mjs';
|
|
15
15
|
|
|
16
|
-
const STRATEGY_PATH = join(
|
|
16
|
+
const STRATEGY_PATH = join(doctorRoot(), 'cache-strategy.json');
|
|
17
17
|
const REVIEW_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes between adjustments
|
|
18
18
|
|
|
19
19
|
function readStrategy() {
|
|
@@ -27,7 +27,7 @@ function readStrategy() {
|
|
|
27
27
|
|
|
28
28
|
function writeStrategy(data) {
|
|
29
29
|
try {
|
|
30
|
-
mkdirSync(
|
|
30
|
+
mkdirSync(doctorRoot(), { recursive: true });
|
|
31
31
|
writeFileSync(STRATEGY_PATH, JSON.stringify(data, null, 2));
|
|
32
32
|
} catch { /* best effort */ }
|
|
33
33
|
}
|
|
@@ -153,7 +153,7 @@ export function onPostToolUse({ costLogPath, provider, dryRun = false } = {}) {
|
|
|
153
153
|
// Read cost log
|
|
154
154
|
const entries = [];
|
|
155
155
|
try {
|
|
156
|
-
const logPath = costLogPath || join(
|
|
156
|
+
const logPath = costLogPath || join(doctorRoot(), 'session-cost.jsonl');
|
|
157
157
|
if (!existsSync(logPath)) return { adjusted: false, reason: 'No cost log' };
|
|
158
158
|
|
|
159
159
|
const lines = readFileSync(logPath, 'utf8')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/certification/document-io-fixtures.mjs — intake fixture catalog for document I/O tests.
|
|
3
3
|
*
|
|
4
|
-
* Maps docs/reference/document-io.md supported intake categories to committed
|
|
4
|
+
* Maps docs/guides/reference/document-io.md supported intake categories to committed
|
|
5
5
|
* files under tests/fixtures/document-io/<category>/.
|
|
6
6
|
*/
|
|
7
7
|
|
package/lib/chat/config.mjs
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import fs from 'node:fs';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import os from 'node:os';
|
|
18
17
|
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
18
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
19
19
|
|
|
20
20
|
export const LAYER_KEYS = ['thinking', 'path', 'specialists', 'tools', 'observability'];
|
|
21
21
|
export const PERMISSION_MODES = ['ask', 'allow_once', 'allow_always', 'reject'];
|
|
@@ -64,7 +64,7 @@ function mergeConfig(base, override) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function globalConfigPath() {
|
|
67
|
-
return path.join(
|
|
67
|
+
return path.join(doctorRoot(), CONFIG_BASENAME);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export function projectConfigPath({ cwd = process.cwd() } = {}) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/chat/desktop-binary.mjs — resolve the Construct chat desktop window binary.
|
|
3
3
|
*
|
|
4
|
-
* Search order: CONSTRUCT_CHAT_DESKTOP_BIN, repo dev build,
|
|
5
|
-
* then packaged sidecar next to construct.
|
|
4
|
+
* Search order: CONSTRUCT_CHAT_DESKTOP_BIN, repo dev build, the bin/ subdir of
|
|
5
|
+
* the XDG state dir, then packaged sidecar next to construct.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
@@ -10,6 +10,8 @@ import os from 'node:os';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
|
|
13
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
14
|
+
|
|
13
15
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
14
16
|
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
15
17
|
|
|
@@ -50,7 +52,7 @@ export function desktopBinaryCandidates({ repoRoot = REPO_ROOT, homeDir = os.hom
|
|
|
50
52
|
env.CONSTRUCT_CHAT_DESKTOP_BIN,
|
|
51
53
|
path.join(tauriRoot, 'release', name),
|
|
52
54
|
path.join(tauriRoot, 'debug', name),
|
|
53
|
-
path.join(homeDir, '
|
|
55
|
+
path.join(stateDir(homeDir), 'bin', name),
|
|
54
56
|
path.join(repoRoot, 'apps', 'chat', 'desktop', 'bin', name),
|
|
55
57
|
].filter(Boolean);
|
|
56
58
|
}
|
package/lib/cli-commands.mjs
CHANGED
|
@@ -429,14 +429,12 @@ export const CLI_COMMANDS = [
|
|
|
429
429
|
category: 'Models & Integrations',
|
|
430
430
|
core: false,
|
|
431
431
|
description: 'Show or update model tier assignments',
|
|
432
|
-
usage: 'construct models <list|set|free|reset|
|
|
432
|
+
usage: 'construct models <list|set|free|reset|resolve>',
|
|
433
433
|
subcommands: [
|
|
434
434
|
{ name: 'list', desc: 'Show current tier assignments' },
|
|
435
435
|
{ name: 'set --tier=<reasoning|standard|fast> --model=<model>', desc: 'Set a model for a tier' },
|
|
436
436
|
{ name: 'free', desc: 'List available free models' },
|
|
437
437
|
{ name: 'reset', desc: 'Reset all tier assignments' },
|
|
438
|
-
{ name: 'usage', desc: 'Show token usage per tier' },
|
|
439
|
-
{ name: 'cost', desc: 'Show cost breakdown' },
|
|
440
438
|
{ name: 'resolve --json', desc: 'Resolve the model for an embedded workflow given host context' },
|
|
441
439
|
],
|
|
442
440
|
},
|
|
@@ -1112,7 +1110,7 @@ export const CLI_COMMANDS = [
|
|
|
1112
1110
|
emoji: '📄',
|
|
1113
1111
|
category: 'Diagnostics',
|
|
1114
1112
|
core: false,
|
|
1115
|
-
description: 'Regenerate generated reference pages under docs/reference/',
|
|
1113
|
+
description: 'Regenerate generated reference pages under docs/guides/reference/',
|
|
1116
1114
|
usage: 'construct docs:site [--check]',
|
|
1117
1115
|
},
|
|
1118
1116
|
{
|
|
@@ -1146,7 +1144,7 @@ export const CLI_COMMANDS = [
|
|
|
1146
1144
|
{ name: 'specialist', category: 'Internal', core: false, internal: true, description: 'Maintainer tool: scaffold, edit, and lint specialist prompts', usage: 'construct specialist <create|edit|lint>' },
|
|
1147
1145
|
{ name: 'registry:status', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Dev: capability registry inspector', usage: 'construct registry:status [--json]' },
|
|
1148
1146
|
{ name: 'registry:validate', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Validate registry/capabilities.json against repo reality', usage: 'construct registry:validate [--json]' },
|
|
1149
|
-
{ name: 'registry:generate-docs', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Generate docs/reference/capabilities.md from registry', usage: 'construct registry:generate-docs' },
|
|
1147
|
+
{ name: 'registry:generate-docs', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Generate docs/guides/reference/capabilities.md from registry', usage: 'construct registry:generate-docs' },
|
|
1150
1148
|
{ name: 'oracle', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Oracle meta-controller — fleet health review and bounded-auto maintenance', usage: 'construct oracle start|status|review|pending|approve|gaps|reconcile' },
|
|
1151
1149
|
{ name: 'matrix', category: 'Internal', core: false, internal: true, surface: 'internal', description: 'Living dependency matrix — build/inspect the typed file↔capability↔workflow↔test graph', usage: 'construct matrix build|stat|query <node-id> [--json]' },
|
|
1152
1150
|
{ name: 'impact', category: 'Diagnostics', core: false, internal: false, surface: 'thin-cli', description: 'Change-impact analysis — map changed files to affected tests, capabilities, and workflows', usage: 'construct impact [files…] [--stdin] [--run] [--json]' },
|
|
@@ -1271,7 +1269,7 @@ export function formatCommandHelp(name, { colors = false } = {}) {
|
|
|
1271
1269
|
lines.push(`${c.dim}Next:${c.reset} ${next}`);
|
|
1272
1270
|
lines.push('');
|
|
1273
1271
|
}
|
|
1274
|
-
lines.push(`${c.dim}See also:${c.reset} run \`construct --help\` for all commands; full reference in docs/reference/cli/.`);
|
|
1272
|
+
lines.push(`${c.dim}See also:${c.reset} run \`construct --help\` for all commands; full reference in docs/guides/reference/cli/.`);
|
|
1275
1273
|
lines.push('');
|
|
1276
1274
|
return lines.join('\n');
|
|
1277
1275
|
}
|
|
@@ -25,7 +25,7 @@ function handlerNames(rootDir) {
|
|
|
25
25
|
export function buildCliServiceInventory({ rootDir = process.cwd() } = {}) {
|
|
26
26
|
const handlers = handlerNames(rootDir);
|
|
27
27
|
return CLI_COMMANDS.filter((spec) => !spec.internal).map((spec) => {
|
|
28
|
-
const page = path.join(rootDir, 'docs', 'reference', 'cli', `${slugify(spec.category)}.md`);
|
|
28
|
+
const page = path.join(rootDir, 'docs', 'guides', 'reference', 'cli', `${slugify(spec.category)}.md`);
|
|
29
29
|
const pageText = fs.existsSync(page) ? fs.readFileSync(page, 'utf8') : '';
|
|
30
30
|
return {
|
|
31
31
|
name: spec.name,
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -34,11 +34,15 @@ const MD_HEADER_GLOBS = [
|
|
|
34
34
|
// Scope is deliberately narrow — READMEs, cookbook, concepts, and reference are
|
|
35
35
|
// unaffected. The patterns live in BANNED_ARTIFACT below; the severity (block
|
|
36
36
|
// vs warn) is controlled by CONSTRUCT_ARTIFACT_LINT_MODE.
|
|
37
|
+
// Match this repo's bucketed layout (docs/specs/prd, docs/decisions/adr, …) and
|
|
38
|
+
// the flat init-lane layout that `construct init` scaffolds downstream (docs/prd,
|
|
39
|
+
// docs/adr, …), so artifact-prose linting fires in either project shape.
|
|
40
|
+
|
|
37
41
|
const ARTIFACT_PATH_GLOBS = [
|
|
38
|
-
/^docs\/prd\//,
|
|
39
|
-
/^docs\/adr\//,
|
|
40
|
-
/^docs\/rfc\//,
|
|
41
|
-
/^docs\/research\//,
|
|
42
|
+
/^docs\/(?:specs\/)?prd\//,
|
|
43
|
+
/^docs\/(?:decisions\/)?adr\//,
|
|
44
|
+
/^docs\/(?:decisions\/)?rfc\//,
|
|
45
|
+
/^docs\/(?:notes\/)?research\//,
|
|
42
46
|
/^\.cx\/knowledge\//,
|
|
43
47
|
/^\.cx\/handoffs\//,
|
|
44
48
|
/^\.cx\/research\//,
|
|
@@ -55,7 +59,7 @@ function isArtifactPath(rel) {
|
|
|
55
59
|
// Deliverable surfaces where a tool-identity leak (rules/common/tool-invisibility.md)
|
|
56
60
|
// matters: project docs and the durable .cx knowledge / research / handoff / strategy
|
|
57
61
|
// stores. Broader than the fabrication-scoped ARTIFACT_PATH_GLOBS because a strategy
|
|
58
|
-
// lands in docs/ root, not only docs/prd.
|
|
62
|
+
// lands in docs/ root, not only docs/specs/prd.
|
|
59
63
|
|
|
60
64
|
const DELIVERABLE_LEAK_GLOBS = [
|
|
61
65
|
/^docs\/.*\.md$/,
|
package/lib/config/alias.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Lookup precedence:
|
|
5
5
|
* 1. CONSTRUCT_ALIAS env var (per-session override)
|
|
6
|
-
* 2.
|
|
6
|
+
* 2. config.json.aliasOverride in the XDG config dir (user-level, lets each
|
|
7
7
|
* user on the same project see their own branding without
|
|
8
8
|
* committing it)
|
|
9
9
|
* 3. construct.config.json.alias (project-level, committed)
|
|
@@ -17,6 +17,7 @@ import fs from 'node:fs';
|
|
|
17
17
|
import os from 'node:os';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { loadProjectConfig } from './project-config.mjs';
|
|
20
|
+
import { configDir } from './xdg.mjs';
|
|
20
21
|
|
|
21
22
|
export const DEFAULT_ALIAS = 'Construct';
|
|
22
23
|
export const ALIAS_ENV_KEY = 'CONSTRUCT_ALIAS';
|
|
@@ -29,7 +30,7 @@ export function resolveAlias({ cwd = process.cwd(), env = process.env, homeDir =
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
try {
|
|
32
|
-
const userCfgPath = path.join(homeDir, '
|
|
33
|
+
const userCfgPath = path.join(configDir(homeDir), 'config.json');
|
|
33
34
|
if (fs.existsSync(userCfgPath)) {
|
|
34
35
|
const userCfg = JSON.parse(fs.readFileSync(userCfgPath, 'utf8'));
|
|
35
36
|
const userAlias = userCfg?.aliasOverride;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/config/intake-policy.mjs — project-scoped intake watcher policy.
|
|
3
3
|
*
|
|
4
|
-
* Resolves
|
|
5
|
-
* construct.config.json intakePolicy
|
|
6
|
-
*
|
|
4
|
+
* Resolves the canonical drop zone, scan depth, and additional directories from
|
|
5
|
+
* construct.config.json intakePolicy (the only config source), with env
|
|
6
|
+
* overrides and a default when intakePolicy is absent.
|
|
7
|
+
*
|
|
8
|
+
* Single-zone model (ADR-0045 §C): the only drop zone is the project-root
|
|
9
|
+
* `inbox/`, always watched. `additionalDirs` (opt-in extra dirs) and `maxDepth`
|
|
10
|
+
* remain configurable. There are no other zones — `.cx/inbox/` and `docs/intake/`
|
|
11
|
+
* are not watched and not scaffolded.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
10
14
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
11
15
|
import { loadProjectConfig, writeProjectConfig, findProjectConfigPath, PROJECT_CONFIG_FILENAME } from './project-config.mjs';
|
|
12
16
|
import {
|
|
@@ -15,17 +19,11 @@ import {
|
|
|
15
19
|
INTAKE_DEPTH_GUIDANCE,
|
|
16
20
|
describeIntakeDepth,
|
|
17
21
|
} from '../intake/constants.mjs';
|
|
18
|
-
import { INTAKE_CONFIG_FILE } from '../intake/legacy-paths.mjs';
|
|
19
22
|
|
|
20
23
|
export { INTAKE_DEFAULT_MAX_DEPTH, INTAKE_HARD_MAX_DEPTH, INTAKE_DEPTH_GUIDANCE, describeIntakeDepth };
|
|
21
24
|
|
|
22
25
|
export const DEFAULT_INTAKE_POLICY = Object.freeze({
|
|
23
26
|
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
24
|
-
zones: Object.freeze({
|
|
25
|
-
rootInbox: true,
|
|
26
|
-
projectInbox: true,
|
|
27
|
-
docsIntake: true,
|
|
28
|
-
}),
|
|
29
27
|
additionalDirs: [],
|
|
30
28
|
});
|
|
31
29
|
|
|
@@ -48,69 +46,24 @@ function parseEnvDirs(env) {
|
|
|
48
46
|
return raw.split(':').map((p) => p.trim()).filter(Boolean);
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
function readLegacyIntakeConfig(rootDir) {
|
|
52
|
-
const file = join(rootDir, INTAKE_CONFIG_FILE);
|
|
53
|
-
if (!existsSync(file)) return null;
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(readFileSync(file, 'utf8'));
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
49
|
export function intakePolicyFromProjectConfig(config) {
|
|
62
50
|
const raw = config?.intakePolicy ?? {};
|
|
63
|
-
const zones = raw.zones ?? {};
|
|
64
51
|
return {
|
|
65
52
|
maxDepth: clampDepth(raw.maxDepth ?? DEFAULT_INTAKE_POLICY.maxDepth),
|
|
66
|
-
zones: {
|
|
67
|
-
rootInbox: zones.rootInbox !== false,
|
|
68
|
-
projectInbox: zones.projectInbox !== false,
|
|
69
|
-
docsIntake: zones.docsIntake !== false,
|
|
70
|
-
},
|
|
71
53
|
additionalDirs: Array.isArray(raw.additionalDirs)
|
|
72
54
|
? raw.additionalDirs.map((d) => String(d).trim()).filter(Boolean)
|
|
73
55
|
: [],
|
|
74
56
|
};
|
|
75
57
|
}
|
|
76
58
|
|
|
77
|
-
export function legacyIntakeToPolicy(stored = {}) {
|
|
78
|
-
return {
|
|
79
|
-
maxDepth: clampDepth(stored.maxDepth ?? INTAKE_DEFAULT_MAX_DEPTH),
|
|
80
|
-
zones: {
|
|
81
|
-
rootInbox: stored.includeArchetypeInbox === true || stored.includeRootInbox === true,
|
|
82
|
-
projectInbox: stored.includeProjectInbox !== false,
|
|
83
|
-
docsIntake: stored.includeDocsIntake !== false,
|
|
84
|
-
},
|
|
85
|
-
additionalDirs: Array.isArray(stored.parentDirs) ? stored.parentDirs : [],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function policyToLegacyIntakeShape(policy) {
|
|
90
|
-
return {
|
|
91
|
-
parentDirs: policy.additionalDirs,
|
|
92
|
-
maxDepth: policy.maxDepth,
|
|
93
|
-
includeProjectInbox: policy.zones.projectInbox,
|
|
94
|
-
includeDocsIntake: policy.zones.docsIntake,
|
|
95
|
-
includeArchetypeInbox: policy.zones.rootInbox,
|
|
96
|
-
includeRootInbox: policy.zones.rootInbox,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
59
|
export function loadIntakePolicy(rootDir, env = process.env) {
|
|
101
60
|
const { config, path: configPath, raw } = loadProjectConfig(rootDir, env);
|
|
102
|
-
const legacy = readLegacyIntakeConfig(rootDir);
|
|
103
61
|
let policy;
|
|
104
62
|
let source = 'default';
|
|
105
|
-
let legacyWarning = null;
|
|
106
63
|
|
|
107
64
|
if (configPath && raw?.intakePolicy) {
|
|
108
65
|
policy = intakePolicyFromProjectConfig(config);
|
|
109
66
|
source = 'project-config';
|
|
110
|
-
} else if (legacy) {
|
|
111
|
-
policy = legacyIntakeToPolicy(legacy);
|
|
112
|
-
source = 'legacy-intake-config';
|
|
113
|
-
legacyWarning = `Using deprecated ${INTAKE_CONFIG_FILE}; migrate with \`construct intake config migrate\``;
|
|
114
67
|
} else {
|
|
115
68
|
policy = structuredClone(DEFAULT_INTAKE_POLICY);
|
|
116
69
|
source = 'default';
|
|
@@ -134,10 +87,8 @@ export function loadIntakePolicy(rootDir, env = process.env) {
|
|
|
134
87
|
|
|
135
88
|
return {
|
|
136
89
|
maxDepth,
|
|
137
|
-
zones: { ...policy.zones },
|
|
138
90
|
additionalDirs: uniqueDirs,
|
|
139
91
|
source,
|
|
140
|
-
legacyWarning,
|
|
141
92
|
};
|
|
142
93
|
}
|
|
143
94
|
|
|
@@ -146,12 +97,7 @@ export function resolvedIntakeConfig(rootDir, env = process.env) {
|
|
|
146
97
|
return {
|
|
147
98
|
parentDirs: policy.additionalDirs,
|
|
148
99
|
maxDepth: policy.maxDepth,
|
|
149
|
-
includeProjectInbox: policy.zones.projectInbox,
|
|
150
|
-
includeDocsIntake: policy.zones.docsIntake,
|
|
151
|
-
includeArchetypeInbox: policy.zones.rootInbox,
|
|
152
|
-
includeRootInbox: policy.zones.rootInbox,
|
|
153
100
|
source: policy.source,
|
|
154
|
-
legacyWarning: policy.legacyWarning,
|
|
155
101
|
};
|
|
156
102
|
}
|
|
157
103
|
|
|
@@ -160,14 +106,8 @@ export function saveIntakePolicy(rootDir, patch = {}, options = {}) {
|
|
|
160
106
|
const { config } = loadProjectConfig(rootDir, {});
|
|
161
107
|
const current = intakePolicyFromProjectConfig(config.intakePolicy ? config : { intakePolicy: DEFAULT_INTAKE_POLICY });
|
|
162
108
|
|
|
163
|
-
const nextZones = { ...current.zones, ...(patch.zones ?? {}) };
|
|
164
109
|
const next = {
|
|
165
110
|
maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
|
|
166
|
-
zones: {
|
|
167
|
-
rootInbox: patch.rootInbox !== undefined ? Boolean(patch.rootInbox) : (patch.zones?.rootInbox ?? nextZones.rootInbox),
|
|
168
|
-
projectInbox: patch.projectInbox !== undefined ? Boolean(patch.projectInbox) : (patch.zones?.projectInbox ?? nextZones.projectInbox),
|
|
169
|
-
docsIntake: patch.docsIntake !== undefined ? Boolean(patch.docsIntake) : (patch.zones?.docsIntake ?? nextZones.docsIntake),
|
|
170
|
-
},
|
|
171
111
|
additionalDirs: Array.isArray(patch.additionalDirs)
|
|
172
112
|
? patch.additionalDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
|
|
173
113
|
: current.additionalDirs,
|
|
@@ -197,13 +137,3 @@ export function saveIntakePolicy(rootDir, patch = {}, options = {}) {
|
|
|
197
137
|
writeProjectConfig(configPath, updated);
|
|
198
138
|
return { policy: next, configPath };
|
|
199
139
|
}
|
|
200
|
-
|
|
201
|
-
export function migrateLegacyIntakeConfig(rootDir) {
|
|
202
|
-
const legacy = readLegacyIntakeConfig(rootDir);
|
|
203
|
-
if (!legacy) {
|
|
204
|
-
return { migrated: false, reason: 'no legacy file' };
|
|
205
|
-
}
|
|
206
|
-
const policy = legacyIntakeToPolicy(legacy);
|
|
207
|
-
saveIntakePolicy(rootDir, policy);
|
|
208
|
-
return { migrated: true, policy };
|
|
209
|
-
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/config/legacy-config-migration.mjs — one-time forward migration of model
|
|
3
|
+
* tier overrides from the pre-XDG legacy config into the active XDG config.
|
|
4
|
+
*
|
|
5
|
+
* The config move to $XDG_CONFIG_HOME/construct (see lib/config/xdg.mjs) is a
|
|
6
|
+
* clean break with no legacy read, so any install that set CX_MODEL_* before
|
|
7
|
+
* the move keeps those values stranded in ~/.construct/config.env. Resolution
|
|
8
|
+
* reads only the XDG path, so doctor then reports "no tier configured" until
|
|
9
|
+
* the user hand-copies the keys. This mirrors the stranded tier overrides
|
|
10
|
+
* forward, and never overwrites a value the XDG config already defines.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { parseEnvFile, writeEnvValues } from '../env-config.mjs';
|
|
18
|
+
import { configDir } from './xdg.mjs';
|
|
19
|
+
|
|
20
|
+
// Only model tier overrides migrate. API keys and other secrets are
|
|
21
|
+
// intentionally out of scope — they are re-established through the credential
|
|
22
|
+
// bootstrap, not copied between paths.
|
|
23
|
+
|
|
24
|
+
const MIGRATABLE_KEY = /^(?:CX|CONSTRUCT)_MODEL_[A-Z0-9_]+$/;
|
|
25
|
+
|
|
26
|
+
export function legacyConfigPath(homeDir = os.homedir()) {
|
|
27
|
+
return path.join(homeDir, '.construct', 'config.env');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Migrate stranded CX_MODEL_ and CONSTRUCT_MODEL_ tier overrides from the
|
|
32
|
+
* legacy config into the XDG config. Returns the keys that were written so
|
|
33
|
+
* callers can surface a one-time message and refresh their in-process env.
|
|
34
|
+
*/
|
|
35
|
+
export function migrateLegacyModelConfig({ homeDir = os.homedir(), env = process.env } = {}) {
|
|
36
|
+
const legacyPath = legacyConfigPath(homeDir);
|
|
37
|
+
const xdgPath = path.join(configDir(homeDir, env), 'config.env');
|
|
38
|
+
const result = { performed: false, migrated: {}, legacyPath, xdgPath };
|
|
39
|
+
|
|
40
|
+
if (legacyPath === xdgPath || !fs.existsSync(legacyPath)) return result;
|
|
41
|
+
|
|
42
|
+
const legacyEnv = parseEnvFile(legacyPath);
|
|
43
|
+
const xdgEnv = parseEnvFile(xdgPath);
|
|
44
|
+
|
|
45
|
+
const migrated = {};
|
|
46
|
+
for (const [key, value] of Object.entries(legacyEnv)) {
|
|
47
|
+
if (!MIGRATABLE_KEY.test(key)) continue;
|
|
48
|
+
if (value === '' || value === undefined) continue;
|
|
49
|
+
if (xdgEnv[key] !== undefined && xdgEnv[key] !== '') continue;
|
|
50
|
+
migrated[key] = value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Object.keys(migrated).length === 0) return result;
|
|
54
|
+
|
|
55
|
+
writeEnvValues(xdgPath, migrated);
|
|
56
|
+
result.performed = true;
|
|
57
|
+
result.migrated = migrated;
|
|
58
|
+
return result;
|
|
59
|
+
}
|