@geraldmaron/construct 1.0.15 → 1.0.17
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 +19 -3
- package/bin/construct +376 -98
- package/bin/construct-postinstall.mjs +17 -2
- package/db/schema/010_cx_scores.sql +51 -0
- package/lib/cli-commands.mjs +333 -147
- package/lib/contracts/validate.mjs +20 -10
- package/lib/contracts/violation-log.mjs +27 -12
- package/lib/embedded-contract/audit.mjs +52 -0
- package/lib/embedded-contract/capability.mjs +179 -0
- package/lib/embedded-contract/contract-version.mjs +39 -0
- package/lib/embedded-contract/envelope.mjs +70 -0
- package/lib/embedded-contract/index.mjs +71 -0
- package/lib/embedded-contract/ingest.mjs +77 -0
- package/lib/embedded-contract/model-resolve.mjs +186 -0
- package/lib/embedded-contract/redaction.mjs +91 -0
- package/lib/embedded-contract/role-facts.mjs +66 -0
- package/lib/embedded-contract/triage.mjs +134 -0
- package/lib/embedded-contract/workflow-defs.mjs +125 -0
- package/lib/embedded-contract/workflow-invoke.mjs +218 -0
- package/lib/hooks/config-protection.mjs +12 -5
- package/lib/hooks/stop-notify.mjs +7 -0
- package/lib/init-unified.mjs +36 -26
- package/lib/install/first-invocation.mjs +5 -1
- package/lib/intake/classify.mjs +6 -0
- package/lib/intake/prepare.mjs +22 -4
- package/lib/intake/tables/rnd.mjs +33 -0
- package/lib/integrations/intake-integrations.mjs +28 -2
- package/lib/mcp/server.mjs +69 -0
- package/lib/mcp/tools/embedded-contract.mjs +77 -0
- package/lib/mcp/tools/telemetry.mjs +30 -0
- package/lib/model-router.mjs +40 -0
- package/lib/op-log.mjs +61 -0
- package/lib/orchestration-policy.mjs +15 -0
- package/lib/roles/catalog.mjs +26 -95
- package/lib/roles/gateway.mjs +30 -1
- package/lib/scheduler/index.mjs +24 -4
- package/lib/server/insights.mjs +12 -0
- package/lib/service-manager.mjs +33 -11
- package/lib/setup.mjs +73 -10
- package/lib/update.mjs +31 -3
- package/lib/upgrade.mjs +31 -4
- package/lib/validators/skills.mjs +21 -0
- package/package.json +9 -3
- package/personas/construct.md +2 -0
- package/platforms/claude/CLAUDE.md +43 -15
- package/scripts/sync-specialists.mjs +32 -5
- package/skills/ai/agent-dev.md +2 -0
- package/skills/ai/llm-security.md +2 -0
- package/skills/ai/ml-ops.md +2 -0
- package/skills/ai/orchestration-workflow.md +2 -0
- package/skills/ai/prompt-and-eval.md +2 -0
- package/skills/ai/prompt-optimizer.md +2 -0
- package/skills/ai/rag-system.md +2 -0
- package/skills/ai/trace-triage.md +36 -0
- package/skills/architecture/api-design.md +2 -0
- package/skills/architecture/caching.md +2 -0
- package/skills/architecture/cloud-native.md +2 -0
- package/skills/architecture/message-queue.md +2 -0
- package/skills/architecture/security-arch.md +2 -0
- package/skills/compliance/ai-disclosure.md +2 -0
- package/skills/compliance/data-privacy.md +2 -0
- package/skills/compliance/license-audit.md +2 -0
- package/skills/compliance/regulatory-review.md +2 -0
- package/skills/development/cpp.md +2 -0
- package/skills/development/go.md +2 -0
- package/skills/development/java.md +2 -0
- package/skills/development/kotlin.md +2 -0
- package/skills/development/mobile-crossplatform.md +2 -0
- package/skills/development/python.md +2 -0
- package/skills/development/rust.md +2 -0
- package/skills/development/shell.md +2 -0
- package/skills/development/swift.md +2 -0
- package/skills/development/typescript.md +2 -0
- package/skills/devops/ci-cd.md +2 -0
- package/skills/devops/containerization.md +2 -0
- package/skills/devops/cost-optimization.md +2 -0
- package/skills/devops/data-engineering.md +2 -0
- package/skills/devops/database.md +2 -0
- package/skills/devops/dependency-management.md +2 -0
- package/skills/devops/devsecops.md +2 -0
- package/skills/devops/git-workflow.md +2 -0
- package/skills/devops/incident-response.md +2 -0
- package/skills/devops/monorepo.md +2 -0
- package/skills/devops/observability.md +2 -0
- package/skills/devops/performance.md +2 -0
- package/skills/devops/testing.md +2 -0
- package/skills/docs/adr-workflow.md +2 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -0
- package/skills/docs/customer-profile-workflow.md +2 -0
- package/skills/docs/document-ingest-workflow.md +2 -0
- package/skills/docs/evidence-ingest-workflow.md +2 -0
- package/skills/docs/init-docs.md +2 -0
- package/skills/docs/init-project.md +2 -0
- package/skills/docs/memo-and-decision-capture.md +45 -0
- package/skills/docs/prd-workflow.md +2 -0
- package/skills/docs/prfaq-workflow.md +2 -0
- package/skills/docs/product-intelligence-review.md +2 -0
- package/skills/docs/product-intelligence-workflow.md +2 -0
- package/skills/docs/product-signal-workflow.md +2 -0
- package/skills/docs/research-workflow.md +2 -0
- package/skills/docs/runbook-workflow.md +2 -0
- package/skills/docs/strategy-workflow.md +2 -0
- package/skills/docs/transcript-synthesis.md +43 -0
- package/skills/exploration/dependency-graph-reading.md +2 -0
- package/skills/exploration/repo-map.md +2 -0
- package/skills/exploration/tracer-bullet-method.md +2 -0
- package/skills/exploration/unknown-codebase-onboarding.md +2 -0
- package/skills/frameworks/django.md +2 -0
- package/skills/frameworks/nextjs.md +2 -0
- package/skills/frameworks/react.md +2 -0
- package/skills/frameworks/spring-boot.md +2 -0
- package/skills/frontend-design/accessibility.md +2 -0
- package/skills/frontend-design/component-patterns.md +2 -0
- package/skills/frontend-design/engineering.md +2 -0
- package/skills/frontend-design/screen-reader-testing.md +34 -0
- package/skills/frontend-design/state-management.md +2 -0
- package/skills/frontend-design/ui-aesthetics.md +2 -0
- package/skills/frontend-design/ux-principles.md +2 -0
- package/skills/operating/change-management.md +2 -0
- package/skills/operating/incident-response.md +2 -0
- package/skills/operating/oncall-rotation.md +2 -0
- package/skills/operating/orchestration-reference.md +2 -0
- package/skills/operating/raw-data-structuring.md +44 -0
- package/skills/operating/unstructured-triage.md +45 -0
- package/skills/quality-gates/premortem.md +37 -0
- package/skills/quality-gates/review-work.md +2 -0
- package/skills/quality-gates/verify-change.md +2 -0
- package/skills/quality-gates/verify-module.md +2 -0
- package/skills/quality-gates/verify-quality.md +2 -0
- package/skills/quality-gates/verify-security.md +2 -0
- package/skills/roles/architect.ai-systems.md +2 -0
- package/skills/roles/architect.data.md +2 -0
- package/skills/roles/architect.enterprise.md +2 -0
- package/skills/roles/architect.integration.md +2 -0
- package/skills/roles/architect.md +2 -0
- package/skills/roles/architect.platform.md +2 -0
- package/skills/roles/data-analyst.experiment.md +2 -0
- package/skills/roles/data-analyst.md +2 -0
- package/skills/roles/data-analyst.product-intelligence.md +2 -0
- package/skills/roles/data-analyst.product.md +2 -0
- package/skills/roles/data-analyst.telemetry.md +2 -0
- package/skills/roles/data-engineer.pipeline.md +2 -0
- package/skills/roles/data-engineer.vector-retrieval.md +2 -0
- package/skills/roles/data-engineer.warehouse.md +2 -0
- package/skills/roles/debugger.md +2 -0
- package/skills/roles/designer.accessibility.md +2 -0
- package/skills/roles/designer.md +2 -0
- package/skills/roles/engineer.ai.md +2 -0
- package/skills/roles/engineer.data.md +2 -0
- package/skills/roles/engineer.md +2 -0
- package/skills/roles/engineer.platform.md +2 -0
- package/skills/roles/operator.docs.md +2 -0
- package/skills/roles/operator.md +2 -0
- package/skills/roles/operator.release.md +2 -0
- package/skills/roles/operator.sre.md +2 -0
- package/skills/roles/orchestrator.md +2 -0
- package/skills/roles/product-manager.ai-product.md +2 -0
- package/skills/roles/product-manager.business-strategy.md +2 -0
- package/skills/roles/product-manager.enterprise.md +2 -0
- package/skills/roles/product-manager.growth.md +2 -0
- package/skills/roles/product-manager.md +2 -0
- package/skills/roles/product-manager.platform.md +2 -0
- package/skills/roles/product-manager.product.md +2 -0
- package/skills/roles/qa.ai-eval.md +2 -0
- package/skills/roles/qa.api-contract.md +2 -0
- package/skills/roles/qa.data-pipeline.md +2 -0
- package/skills/roles/qa.md +2 -0
- package/skills/roles/qa.test-automation.md +2 -0
- package/skills/roles/qa.web-ui.md +2 -0
- package/skills/roles/researcher.explorer.md +2 -0
- package/skills/roles/researcher.md +2 -0
- package/skills/roles/researcher.ux.md +2 -0
- package/skills/roles/reviewer.devil-advocate.md +2 -0
- package/skills/roles/reviewer.evaluator.md +2 -0
- package/skills/roles/reviewer.md +2 -0
- package/skills/roles/reviewer.trace.md +2 -0
- package/skills/roles/security.ai.md +2 -0
- package/skills/roles/security.appsec.md +2 -0
- package/skills/roles/security.cloud.md +2 -0
- package/skills/roles/security.legal-compliance.md +2 -0
- package/skills/roles/security.md +2 -0
- package/skills/roles/security.privacy.md +2 -0
- package/skills/roles/security.supply-chain.md +2 -0
- package/skills/security/blue-team.md +2 -0
- package/skills/security/code-audit.md +2 -0
- package/skills/security/pentest.md +2 -0
- package/skills/security/red-team.md +2 -0
- package/skills/security/threat-intel.md +2 -0
- package/skills/security/vuln-research.md +2 -0
- package/skills/strategy/competitive-landscape.md +2 -0
- package/skills/strategy/jobs-to-be-done.md +38 -0
- package/skills/strategy/market-research-methods.md +2 -0
- package/skills/strategy/narrative-arc.md +2 -0
- package/skills/strategy/pricing-positioning.md +2 -0
- package/skills/utility/clean-code.md +2 -0
- package/specialists/prompts/cx-engineer.md +1 -1
- package/specialists/registry.json +18 -9
- package/specialists/role-manifests.json +1 -1
package/bin/construct
CHANGED
|
@@ -12,7 +12,7 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
|
|
15
|
-
import { CLI_COMMANDS, CLI_COMMANDS_BY_CATEGORY, CATEGORY_ORDER } from '../lib/cli-commands.mjs';
|
|
15
|
+
import { CLI_COMMANDS, CLI_COMMANDS_BY_CATEGORY, CATEGORY_ORDER, formatCommandHelp, isInternalCommand } from '../lib/cli-commands.mjs';
|
|
16
16
|
import { buildStatus, formatStatusReport } from '../lib/status.mjs';
|
|
17
17
|
import { validateRegistry } from '../lib/validator.mjs';
|
|
18
18
|
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig, applyToEnv, resetEnv, setTierModel, setModelWithTierInference } from '../lib/model-router.mjs';
|
|
@@ -104,83 +104,75 @@ function iconColumn(emoji) {
|
|
|
104
104
|
return emoji + trailingSpaces;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function usage() {
|
|
108
|
-
const showAll = restArgsCache.includes('--all') || restArgsCache.includes('-a');
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
function usage(opts = {}) {
|
|
108
|
+
const showAll = opts.all ?? (restArgsCache.includes('--all') || restArgsCache.includes('-a'));
|
|
109
|
+
// Always filter out internal commands; users can still run them directly,
|
|
110
|
+
// but --all is for "everything humans use" not "every handler in the map".
|
|
111
|
+
const visible = CLI_COMMANDS.filter((c) => !c.internal);
|
|
112
|
+
const commands = showAll ? visible : visible.filter((c) => c.core);
|
|
113
|
+
|
|
111
114
|
if (showAll) {
|
|
112
115
|
println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness (all commands)`);
|
|
113
116
|
} else {
|
|
114
117
|
println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness`);
|
|
115
|
-
println(`${COLORS.dim}Tip: Run ${COLORS.reset}construct --all${COLORS.dim} to see all commands${COLORS.reset}`);
|
|
118
|
+
println(`${COLORS.dim}Tip: Run ${COLORS.reset}construct --all${COLORS.dim} to see all commands · ${COLORS.reset}construct <cmd> --help${COLORS.dim} for details${COLORS.reset}`);
|
|
116
119
|
}
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
println(`\n${COLORS.dim}Usage:${COLORS.reset} construct <command> [options]\n`);
|
|
119
|
-
|
|
122
|
+
|
|
120
123
|
for (const category of CATEGORY_ORDER) {
|
|
121
|
-
const categoryCommands = commands.filter(c => c.category === category);
|
|
124
|
+
const categoryCommands = commands.filter((c) => c.category === category);
|
|
122
125
|
if (!categoryCommands.length) continue;
|
|
123
126
|
println(`${COLORS.bold}${category}${COLORS.reset}`);
|
|
124
127
|
for (const command of categoryCommands) {
|
|
125
|
-
|
|
128
|
+
const emoji = command.emoji ?? ' ';
|
|
129
|
+
println(` ${iconColumn(emoji)}${command.name.padEnd(14)} ${command.description}`);
|
|
126
130
|
}
|
|
127
131
|
println('');
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
// Bare-command landing — show the same core-command listing as `construct
|
|
136
|
+
// --help`, prefixed with a context line so users know where they are, and
|
|
137
|
+
// suffixed with the one next-step suggestion that applies to their state.
|
|
138
|
+
|
|
132
139
|
async function showInteractiveMenu() {
|
|
133
|
-
const { loadProjectConfig } = await import('../lib/config/project-config.mjs');
|
|
134
140
|
const { existsSync } = await import('node:fs');
|
|
135
141
|
const { join } = await import('node:path');
|
|
136
|
-
|
|
142
|
+
|
|
137
143
|
const projectRoot = process.cwd();
|
|
138
144
|
const isConstructProject = existsSync(join(projectRoot, 'construct.config.json')) ||
|
|
139
145
|
existsSync(join(projectRoot, '.cx'));
|
|
140
|
-
|
|
141
|
-
println(`${COLORS.bold}construct${COLORS.reset} — AI agent harness`);
|
|
142
|
-
println('');
|
|
143
|
-
|
|
146
|
+
|
|
144
147
|
if (isConstructProject) {
|
|
145
148
|
const projectName = process.env.CX_PROJECT_NAME || path.basename(projectRoot);
|
|
146
149
|
println(`${COLORS.dim}Project:${COLORS.reset} ${projectName}`);
|
|
147
150
|
println('');
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
println(` ${COLORS.green}⏹ stop${COLORS.reset} Stop all running services`);
|
|
153
|
-
println(` ${COLORS.green}📡 status${COLORS.reset} Show system health and credentials`);
|
|
154
|
-
println(` ${COLORS.green}🏗️ init${COLORS.reset} Initialize project and start services`);
|
|
155
|
-
println(` ${COLORS.green}🔄 sync${COLORS.reset} Sync agent adapters to AI tools`);
|
|
156
|
-
println('');
|
|
157
|
-
println(`${COLORS.dim}Run 'construct <command> --help' for command details${COLORS.reset}`);
|
|
158
|
-
println(`${COLORS.dim}Run 'construct --all' to see all commands${COLORS.reset}`);
|
|
159
|
-
println('');
|
|
160
|
-
|
|
161
|
-
// Show context-aware suggestions
|
|
152
|
+
|
|
153
|
+
usage({ all: false });
|
|
154
|
+
|
|
162
155
|
if (isConstructProject) {
|
|
163
156
|
const { readDashboardState } = await import('../lib/service-manager.mjs');
|
|
164
157
|
const dashboard = readDashboardState(HOME);
|
|
165
|
-
|
|
166
158
|
if (!dashboard) {
|
|
167
|
-
println(`${COLORS.yellow}💡 Services not running. Start with:${COLORS.reset}`);
|
|
168
|
-
println(` ${COLORS.green}construct dev${COLORS.reset}`);
|
|
159
|
+
println(`${COLORS.yellow}💡 Services not running. Start with:${COLORS.reset} ${COLORS.green}construct dev${COLORS.reset}`);
|
|
169
160
|
println('');
|
|
170
161
|
}
|
|
171
162
|
} else {
|
|
172
|
-
println(`${COLORS.yellow}💡 Not a Construct project. Initialize with:${COLORS.reset}`);
|
|
173
|
-
println(` ${COLORS.green}construct init${COLORS.reset}`);
|
|
163
|
+
println(`${COLORS.yellow}💡 Not a Construct project. Initialize with:${COLORS.reset} ${COLORS.green}construct init${COLORS.reset}`);
|
|
174
164
|
println('');
|
|
175
165
|
}
|
|
176
166
|
}
|
|
177
167
|
|
|
178
|
-
function runNodeScript(scriptPath, args = [], extraEnv = {}) {
|
|
168
|
+
function runNodeScript(scriptPath, args = [], extraEnv = {}, { exitOnError = true } = {}) {
|
|
179
169
|
const result = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
180
170
|
stdio: 'inherit',
|
|
181
171
|
env: { ...process.env, ...extraEnv },
|
|
182
172
|
});
|
|
183
|
-
|
|
173
|
+
const status = result.status ?? 1;
|
|
174
|
+
if (status !== 0 && exitOnError) process.exit(status);
|
|
175
|
+
return status;
|
|
184
176
|
}
|
|
185
177
|
|
|
186
178
|
async function cmdStatus() {
|
|
@@ -248,12 +240,24 @@ async function cmdShow() {
|
|
|
248
240
|
}
|
|
249
241
|
|
|
250
242
|
async function cmdSync(args) {
|
|
243
|
+
const { startOpLog } = await import('../lib/op-log.mjs');
|
|
244
|
+
const opLog = startOpLog('sync', { homeDir: HOME });
|
|
245
|
+
opLog.event('args', { args });
|
|
246
|
+
|
|
251
247
|
generateCompletions();
|
|
252
|
-
runNodeScript(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs'), args);
|
|
253
|
-
if (
|
|
248
|
+
const syncStatus = runNodeScript(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs'), args, {}, { exitOnError: false });
|
|
249
|
+
if (syncStatus !== 0) {
|
|
250
|
+
opLog.close('failed', { stage: 'sync-specialists', status: syncStatus });
|
|
251
|
+
process.exit(syncStatus);
|
|
252
|
+
}
|
|
253
|
+
if (args.includes('--no-docs')) {
|
|
254
|
+
opLog.close('ok', { docs: 'skipped' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
254
257
|
const { regenerateDocs } = await import('../lib/auto-docs.mjs');
|
|
255
258
|
const { changed } = await regenerateDocs({ rootDir: ROOT_DIR });
|
|
256
259
|
for (const f of changed) ok(`Updated ${path.relative(ROOT_DIR, f)}`);
|
|
260
|
+
opLog.close('ok', { docsChanged: changed.length });
|
|
257
261
|
}
|
|
258
262
|
|
|
259
263
|
async function cmdCompletions(args) {
|
|
@@ -506,35 +510,12 @@ async function cmdDoctor() {
|
|
|
506
510
|
);
|
|
507
511
|
}
|
|
508
512
|
|
|
509
|
-
// Pre-push bypass
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
const bypassLogPath = path.join(userHome, '.construct', 'audit', 'prepush-bypass.log');
|
|
516
|
-
if (fs.existsSync(bypassLogPath)) {
|
|
517
|
-
try {
|
|
518
|
-
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
519
|
-
const recent = fs.readFileSync(bypassLogPath, 'utf8')
|
|
520
|
-
.split('\n')
|
|
521
|
-
.filter(Boolean)
|
|
522
|
-
.map((line) => { try { return JSON.parse(line); } catch { return null; } })
|
|
523
|
-
.filter((e) => e && Date.parse(e.ts) >= cutoff);
|
|
524
|
-
const BYPASS_WARN_THRESHOLD = Number(process.env.CONSTRUCT_DOCTOR_BYPASS_WARN) || 3;
|
|
525
|
-
if (recent.length >= BYPASS_WARN_THRESHOLD) {
|
|
526
|
-
add(
|
|
527
|
-
`Pre-push bypasses in last 7 days: ${recent.length} (gate may be wrong-sized — see ${bypassLogPath})`,
|
|
528
|
-
false,
|
|
529
|
-
true,
|
|
530
|
-
);
|
|
531
|
-
} else {
|
|
532
|
-
add(`Pre-push bypasses in last 7 days: ${recent.length}`, true);
|
|
533
|
-
}
|
|
534
|
-
} catch { /* audit log read is best-effort */ }
|
|
535
|
-
} else {
|
|
536
|
-
add('Pre-push bypass log not present (no bypasses honored yet)', true);
|
|
537
|
-
}
|
|
513
|
+
// (Pre-push bypass doctor check removed in v1.0.16.) v1.0.15 deleted the
|
|
514
|
+
// entire skip-var infrastructure (CONSTRUCT_SKIP_PREPUSH, _COMMENT_LINT,
|
|
515
|
+
// _PR_LINT, _DOCS, CONSTRUCT_ALLOW_CLAUDE_PUSH). Nothing writes to
|
|
516
|
+
// ~/.construct/audit/prepush-bypass.log, so any entries there are stale
|
|
517
|
+
// and would otherwise surface the warning indefinitely. The file itself
|
|
518
|
+
// is harmless — next cleanup tick may sweep it.
|
|
538
519
|
|
|
539
520
|
// Embed daemon log size. The daemon's own scheduler rotates every
|
|
540
521
|
// minute at 50MB, but a freshly-upgraded install with a 34GB legacy
|
|
@@ -671,16 +652,12 @@ async function cmdDoctor() {
|
|
|
671
652
|
path.join(process.env.APPDATA ?? path.join(HOME, 'AppData', 'Roaming'), 'Code - Insiders'),
|
|
672
653
|
];
|
|
673
654
|
if (vscodeAppSupportRoots.some((r) => fs.existsSync(r))) {
|
|
674
|
-
// VS Code
|
|
675
|
-
//
|
|
655
|
+
// VS Code installed → check settings.json presence as a green/info signal.
|
|
656
|
+
// Absence is not a doctor finding — VS Code adapter support hasn't landed
|
|
657
|
+
// yet, so "create one or wait for support" isn't actionable. The line is
|
|
658
|
+
// suppressed entirely when missing; doctor surfaces facts, not roadmap.
|
|
676
659
|
const vscodeSettingsOk = vscodeSettingsPaths.some((candidate) => fs.existsSync(candidate));
|
|
677
|
-
add(
|
|
678
|
-
vscodeSettingsOk
|
|
679
|
-
? 'VS Code settings file'
|
|
680
|
-
: 'VS Code settings missing — create settings.json or wait for `construct sync` VS Code support',
|
|
681
|
-
vscodeSettingsOk,
|
|
682
|
-
true,
|
|
683
|
-
);
|
|
660
|
+
if (vscodeSettingsOk) add('VS Code settings file', true, true);
|
|
684
661
|
}
|
|
685
662
|
add('User config ready', fs.existsSync(getUserEnvPath(HOME)) || fs.existsSync(path.join(HOME, '.construct')), true);
|
|
686
663
|
add('skills/ directory', fs.existsSync(path.join(ROOT_DIR, 'skills')));
|
|
@@ -917,11 +894,15 @@ async function cmdDoctor() {
|
|
|
917
894
|
}
|
|
918
895
|
|
|
919
896
|
try {
|
|
920
|
-
const { recentViolations } = await import('../lib/contracts/violation-log.mjs');
|
|
897
|
+
const { recentViolations, violationLogPath } = await import('../lib/contracts/violation-log.mjs');
|
|
921
898
|
const violations = recentViolations({ windowMs: 24 * 60 * 60 * 1000 });
|
|
899
|
+
// Path is project-scoped (.cx/contract-violations.jsonl in cwd when
|
|
900
|
+
// inside a project, ~/.cx/ otherwise). Print the resolved one so
|
|
901
|
+
// users find the real file, not a stale ~/.cx/ reference.
|
|
902
|
+
const violationPath = violationLogPath();
|
|
922
903
|
const label = violations.length === 0
|
|
923
904
|
? 'Contract violations (none in last 24h)'
|
|
924
|
-
: `Contract violations (${violations.length} in last 24h — see
|
|
905
|
+
: `Contract violations (${violations.length} in last 24h — see ${violationPath})`;
|
|
925
906
|
add(label, violations.length === 0, true);
|
|
926
907
|
} catch {
|
|
927
908
|
add('Contract violations', false, true);
|
|
@@ -1104,15 +1085,77 @@ async function cmdDoctor() {
|
|
|
1104
1085
|
if (failCount > 0) process.exit(1);
|
|
1105
1086
|
}
|
|
1106
1087
|
|
|
1107
|
-
|
|
1088
|
+
// Resolve which services `construct dev` should start. Returns null (start
|
|
1089
|
+
// everything) unless --only= or --select narrows it. Unknown --only keys fail
|
|
1090
|
+
// loud rather than silently starting the wrong set; --select on a non-TTY is an
|
|
1091
|
+
// error pointing at --only so scripts get a deterministic path.
|
|
1092
|
+
async function resolveServiceSelection(args, selectable) {
|
|
1093
|
+
const validKeys = new Set(selectable.map((s) => s.key));
|
|
1094
|
+
const onlyArg = args.find((a) => a.startsWith('--only='));
|
|
1095
|
+
|
|
1096
|
+
if (onlyArg) {
|
|
1097
|
+
const keys = onlyArg.slice('--only='.length).split(',').map((k) => k.trim()).filter(Boolean);
|
|
1098
|
+
const unknown = keys.filter((k) => !validKeys.has(k));
|
|
1099
|
+
if (unknown.length) {
|
|
1100
|
+
errorln(`Unknown service(s) for --only: ${unknown.join(', ')}`);
|
|
1101
|
+
errorln(`Valid services: ${[...validKeys].join(', ')}`);
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
if (keys.length === 0) {
|
|
1105
|
+
errorln('--only requires at least one service.');
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
return new Set(keys);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (args.includes('--select')) {
|
|
1112
|
+
if (!process.stdin.isTTY) {
|
|
1113
|
+
errorln('--select needs an interactive terminal. Use --only=<a,b,c> in scripts.');
|
|
1114
|
+
errorln(`Valid services: ${[...validKeys].join(', ')}`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
const { multiSelect } = await import('../lib/tty-prompts.mjs');
|
|
1118
|
+
try {
|
|
1119
|
+
const chosen = await multiSelect({
|
|
1120
|
+
title: 'Start which services?',
|
|
1121
|
+
instructions: 'Pressure Guard and Doctor always run. Space toggles, Enter confirms.',
|
|
1122
|
+
options: selectable.map((s) => ({ label: s.label, value: s.key, description: s.description, checked: true })),
|
|
1123
|
+
});
|
|
1124
|
+
if (!chosen.length) {
|
|
1125
|
+
errorln('No services selected — nothing to start.');
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
return new Set(chosen);
|
|
1129
|
+
} catch {
|
|
1130
|
+
errorln('Service selection canceled.');
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function cmdUp(args = []) {
|
|
1139
|
+
const { SELECTABLE_SERVICES } = await import('../lib/service-manager.mjs');
|
|
1140
|
+
|
|
1141
|
+
// --only=a,b,c picks services non-interactively; --select opens a checklist.
|
|
1142
|
+
// Both resolve to a Set of service keys (null = start everything).
|
|
1143
|
+
|
|
1144
|
+
const selected = await resolveServiceSelection(args, SELECTABLE_SERVICES);
|
|
1145
|
+
|
|
1146
|
+
const { startOpLog } = await import('../lib/op-log.mjs');
|
|
1147
|
+
const opLog = startOpLog('dev', { homeDir: HOME });
|
|
1148
|
+
opLog.event('selection', { services: selected ? [...selected] : 'all' });
|
|
1149
|
+
|
|
1108
1150
|
// Auto-magic health check — catch missing prerequisites before starting services
|
|
1109
1151
|
const { quickHealthCheck, detectCredentials, resolveCredentials } = await import('../lib/health-check.mjs');
|
|
1110
|
-
|
|
1152
|
+
|
|
1111
1153
|
// Resolve credentials from env, .env files, and 1Password before starting services
|
|
1112
1154
|
const resolvedCreds = await resolveCredentials({ homeDir: HOME });
|
|
1113
|
-
|
|
1155
|
+
|
|
1114
1156
|
const health = await quickHealthCheck({ homeDir: HOME, showCredentials: true });
|
|
1115
|
-
|
|
1157
|
+
opLog.event('prereq', { ok: health.ok, missing: health.missing ?? [] });
|
|
1158
|
+
|
|
1116
1159
|
if (!health.ok) {
|
|
1117
1160
|
errorln('Prerequisites missing for starting services:');
|
|
1118
1161
|
for (const item of health.missing) {
|
|
@@ -1120,10 +1163,12 @@ async function cmdUp() {
|
|
|
1120
1163
|
}
|
|
1121
1164
|
println('');
|
|
1122
1165
|
info('Run `construct install --yes` to install missing dependencies.');
|
|
1166
|
+
opLog.close('failed', { reason: 'prerequisites-missing' });
|
|
1123
1167
|
process.exit(1);
|
|
1124
1168
|
}
|
|
1125
|
-
|
|
1126
|
-
const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME });
|
|
1169
|
+
|
|
1170
|
+
const { results, recovery } = await startServices({ rootDir: ROOT_DIR, homeDir: HOME, selected });
|
|
1171
|
+
opLog.event('services', { results: results.map((r) => ({ name: r.name, status: r.status })) });
|
|
1127
1172
|
for (const svc of results) {
|
|
1128
1173
|
const label = svc.url ? `${svc.name} → ${svc.url}` : svc.name;
|
|
1129
1174
|
if (svc.status === 'started') ok(svc.note ? `${label} (${svc.note})` : label);
|
|
@@ -1161,8 +1206,10 @@ async function cmdUp() {
|
|
|
1161
1206
|
try {
|
|
1162
1207
|
const { runEmbedCli } = await import('../lib/embed/cli.mjs');
|
|
1163
1208
|
await runEmbedCli(['start'], { rootDir: ROOT_DIR });
|
|
1209
|
+
opLog.event('embed', { status: 'started' });
|
|
1164
1210
|
} catch (err) {
|
|
1165
1211
|
warn(`embed start failed: ${err.message} — check ~/.cx/runtime/embed-daemon.log`);
|
|
1212
|
+
opLog.warn('embed', { status: 'failed', error: err.message });
|
|
1166
1213
|
}
|
|
1167
1214
|
}
|
|
1168
1215
|
|
|
@@ -1209,6 +1256,9 @@ async function cmdUp() {
|
|
|
1209
1256
|
println('Dashboard: http://127.0.0.1:4242');
|
|
1210
1257
|
println('Run \x1b[32mconstruct\x1b[0m for commands');
|
|
1211
1258
|
println('');
|
|
1259
|
+
|
|
1260
|
+
const failed = results.filter((r) => r.status === 'error' || r.status === 'failed');
|
|
1261
|
+
opLog.close(failed.length ? 'degraded' : 'ok', { failed: failed.map((r) => r.name) });
|
|
1212
1262
|
}
|
|
1213
1263
|
|
|
1214
1264
|
async function cmdDown() {
|
|
@@ -1337,7 +1387,14 @@ async function cmdReview(args) {
|
|
|
1337
1387
|
process.exit(1);
|
|
1338
1388
|
}
|
|
1339
1389
|
|
|
1340
|
-
async function cmdInit(args) {
|
|
1390
|
+
async function cmdInit(args) {
|
|
1391
|
+
const { startOpLog } = await import('../lib/op-log.mjs');
|
|
1392
|
+
const opLog = startOpLog('init', { homeDir: HOME });
|
|
1393
|
+
opLog.event('args', { args });
|
|
1394
|
+
const status = runNodeScript(path.join(ROOT_DIR, 'lib', 'init-unified.mjs'), args, {}, { exitOnError: false });
|
|
1395
|
+
opLog.close(status === 0 ? 'ok' : 'failed', { status });
|
|
1396
|
+
if (status !== 0) process.exit(status);
|
|
1397
|
+
}
|
|
1341
1398
|
async function cmdDocsVerify(args) { runNodeScript(path.join(ROOT_DIR, 'lib', 'docs-verify.mjs'), args); }
|
|
1342
1399
|
async function cmdInitUpdate(args) {
|
|
1343
1400
|
runNodeScript(path.join(ROOT_DIR, 'lib', 'init-update.mjs'), args);
|
|
@@ -1780,7 +1837,22 @@ async function cmdHosts() {
|
|
|
1780
1837
|
printHostCapabilities();
|
|
1781
1838
|
}
|
|
1782
1839
|
}
|
|
1783
|
-
async function cmdSetup(args) {
|
|
1840
|
+
async function cmdSetup(args) {
|
|
1841
|
+
const { startOpLog } = await import('../lib/op-log.mjs');
|
|
1842
|
+
const opLog = startOpLog('install', { homeDir: HOME });
|
|
1843
|
+
opLog.event('args', { args });
|
|
1844
|
+
try {
|
|
1845
|
+
await runSetup({ rootDir: ROOT_DIR, args, homeDir: HOME });
|
|
1846
|
+
opLog.close('ok');
|
|
1847
|
+
} catch (err) {
|
|
1848
|
+
opLog.error('setup', { error: err.message });
|
|
1849
|
+
opLog.close('failed');
|
|
1850
|
+
errorln(`\nSetup did not complete: ${err.message}`);
|
|
1851
|
+
errorln(' -> Diagnose with: construct doctor');
|
|
1852
|
+
errorln(' -> Re-run setup: construct install (add --reconfigure to re-prompt for services)');
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1784
1856
|
|
|
1785
1857
|
function printConfigModeHelp() {
|
|
1786
1858
|
println('Construct config — inspect and set deployment posture.');
|
|
@@ -1874,6 +1946,45 @@ function formatIntakeTable(rows, { intakeQueueLabel, signalNoun } = { intakeQueu
|
|
|
1874
1946
|
for (const row of rows) println(fmt(row));
|
|
1875
1947
|
}
|
|
1876
1948
|
|
|
1949
|
+
// Embedded triage/planning surface — classifies an artifact and returns a
|
|
1950
|
+
// role-aware plan with no enqueue and no durable write. Shared by
|
|
1951
|
+
// `intake classify` and `graph recommend`. Input comes from --text, --file, or
|
|
1952
|
+
// stdin; --no-enqueue is accepted for explicitness (this path never enqueues).
|
|
1953
|
+
|
|
1954
|
+
async function cmdEmbeddedTriage(args) {
|
|
1955
|
+
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
1956
|
+
const fileArg = flag('--file');
|
|
1957
|
+
let input = flag('--text') || '';
|
|
1958
|
+
let ingestion = null;
|
|
1959
|
+
if (!input && fileArg) {
|
|
1960
|
+
const { resolveInput } = await import('../lib/embedded-contract/ingest.mjs');
|
|
1961
|
+
const resolved = await resolveInput({ filePath: fileArg });
|
|
1962
|
+
input = resolved.text;
|
|
1963
|
+
ingestion = resolved.error ? { ...(resolved.ingestion || {}), error: resolved.error } : resolved.ingestion;
|
|
1964
|
+
}
|
|
1965
|
+
if (!input && !process.stdin.isTTY) {
|
|
1966
|
+
input = fs.readFileSync(0, 'utf8');
|
|
1967
|
+
}
|
|
1968
|
+
if (!input.trim() && !ingestion?.error) {
|
|
1969
|
+
errorln('Usage: construct intake classify --json [--text "..." | --file <path> | <stdin>] [--source <path>] [--artifact-type <t>] [--available-roles a,b]');
|
|
1970
|
+
process.exit(1);
|
|
1971
|
+
}
|
|
1972
|
+
const request = {
|
|
1973
|
+
input,
|
|
1974
|
+
sourcePath: flag('--source') || (fileArg || ''),
|
|
1975
|
+
ingestion,
|
|
1976
|
+
artifactType: flag('--artifact-type'),
|
|
1977
|
+
domain: flag('--domain'),
|
|
1978
|
+
desiredOutcome: flag('--desired-outcome'),
|
|
1979
|
+
constraints: flag('--constraints')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
1980
|
+
availableRoles: flag('--available-roles')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
1981
|
+
};
|
|
1982
|
+
const { recommendPlan } = await import('../lib/embedded-contract/triage.mjs');
|
|
1983
|
+
const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
|
|
1984
|
+
const envelope = wrapContractResult(recommendPlan(request), { surface: 'cli' });
|
|
1985
|
+
println(JSON.stringify(envelope, null, 2));
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1877
1988
|
async function cmdIntake(args) {
|
|
1878
1989
|
const sub = args[0];
|
|
1879
1990
|
|
|
@@ -1882,6 +1993,10 @@ async function cmdIntake(args) {
|
|
|
1882
1993
|
return;
|
|
1883
1994
|
}
|
|
1884
1995
|
|
|
1996
|
+
if (sub === 'classify') {
|
|
1997
|
+
return await cmdEmbeddedTriage(args.slice(1));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
1885
2000
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
1886
2001
|
const { getRebrand } = await import('../lib/profiles/rebrand.mjs');
|
|
1887
2002
|
const cwd = process.cwd();
|
|
@@ -2456,9 +2571,49 @@ async function cmdSkillsUsage(sub, args) {
|
|
|
2456
2571
|
}
|
|
2457
2572
|
|
|
2458
2573
|
if (sub === 'correlate-quality') {
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2574
|
+
// Queries the construct_skill_quality_correlation view (db/schema/010_cx_scores.sql)
|
|
2575
|
+
// which aggregates cx_score values by session_id × skill_id over the past 90 days.
|
|
2576
|
+
// Falls back to a clear "no data" message when the DB is unreachable or the
|
|
2577
|
+
// view is empty rather than printing an empty table.
|
|
2578
|
+
|
|
2579
|
+
const sqlClient = createSqlClient(process.env);
|
|
2580
|
+
if (!sqlClient) {
|
|
2581
|
+
println('Quality correlation requires Postgres (DATABASE_URL or CONSTRUCT_DB_URL).');
|
|
2582
|
+
println('Local JSONL does not carry cx_score data.');
|
|
2583
|
+
println('Run with Postgres configured for full correlation output.');
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
try {
|
|
2587
|
+
const rows = await sqlClient`
|
|
2588
|
+
select skill_id, sessions, score_count, mean_score, median_score, p10_score, p90_score, last_scored_at
|
|
2589
|
+
from construct_skill_quality_correlation
|
|
2590
|
+
order by sessions desc, mean_score desc nulls last
|
|
2591
|
+
limit 50
|
|
2592
|
+
`;
|
|
2593
|
+
if (!rows || rows.length === 0) {
|
|
2594
|
+
println('No correlation data yet. The view aggregates cx_score values from the past 90 days against');
|
|
2595
|
+
println('skill invocations from the same sessions. Once cxScore() runs in a session that also invoked');
|
|
2596
|
+
println('a skill, rows appear here. Verify scoring with: construct telemetry query latency --limit=5');
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const w = Math.max(15, ...rows.map((r) => String(r.skill_id || '').length));
|
|
2600
|
+
println(`${'skill_id'.padEnd(w)} sessions scores mean median p10 p90 last_scored_at`);
|
|
2601
|
+
println('-'.repeat(w + 70));
|
|
2602
|
+
for (const r of rows) {
|
|
2603
|
+
const pad = (v, n) => String(v ?? '').padStart(n);
|
|
2604
|
+
println(
|
|
2605
|
+
`${String(r.skill_id || '').padEnd(w)} ${pad(r.sessions, 8)} ${pad(r.score_count, 6)} ` +
|
|
2606
|
+
`${pad(r.mean_score, 5)} ${pad(r.median_score, 6)} ${pad(r.p10_score, 5)} ${pad(r.p90_score, 5)} ` +
|
|
2607
|
+
`${r.last_scored_at ? new Date(r.last_scored_at).toISOString().slice(0, 19) : ''}`,
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
2610
|
+
} catch (err) {
|
|
2611
|
+
errorln(`Quality correlation query failed: ${err.message}`);
|
|
2612
|
+
errorln('If the construct_skill_quality_correlation view is missing, apply db/schema/010_cx_scores.sql.');
|
|
2613
|
+
process.exit(1);
|
|
2614
|
+
} finally {
|
|
2615
|
+
await closeSqlClient(sqlClient);
|
|
2616
|
+
}
|
|
2462
2617
|
return;
|
|
2463
2618
|
}
|
|
2464
2619
|
|
|
@@ -2594,14 +2749,18 @@ async function cmdIntakeMetrics() {
|
|
|
2594
2749
|
}
|
|
2595
2750
|
|
|
2596
2751
|
async function cmdIntakeIntegrate(id, args, cwd) {
|
|
2597
|
-
if (!id) { errorln('Usage: construct intake integrate <id> <github|jira|confluence>'); process.exit(1); }
|
|
2752
|
+
if (!id) { errorln('Usage: construct intake integrate <id> <github|jira|confluence> [--publish-issues]'); process.exit(1); }
|
|
2598
2753
|
|
|
2599
2754
|
const system = args[0];
|
|
2600
2755
|
if (!system || !['github', 'jira', 'confluence'].includes(system)) {
|
|
2601
|
-
errorln('Usage: construct intake integrate <id> <github|jira|confluence>');
|
|
2756
|
+
errorln('Usage: construct intake integrate <id> <github|jira|confluence> [--publish-issues]');
|
|
2602
2757
|
errorln('Available systems: github, jira, confluence');
|
|
2603
2758
|
process.exit(1);
|
|
2604
2759
|
}
|
|
2760
|
+
// --publish-issues unlocks the demo-source gate (CONSTRUCT_DEMO env or
|
|
2761
|
+
// packet sourcePath under tests/fixtures/ or .cx/intake/demo/). Required
|
|
2762
|
+
// before any demo/fixture intake can publish a real external ticket.
|
|
2763
|
+
const publishDemo = args.includes('--publish-issues');
|
|
2605
2764
|
|
|
2606
2765
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
2607
2766
|
const queue = createIntakeQueue(cwd, process.env);
|
|
@@ -2613,10 +2772,13 @@ async function cmdIntakeIntegrate(id, args, cwd) {
|
|
|
2613
2772
|
if (system === 'github') {
|
|
2614
2773
|
const { createGitHubIssue, tagIntakeWithExternalRef } = await import('../lib/integrations/intake-integrations.mjs');
|
|
2615
2774
|
info(`Creating GitHub issue from intake ${id}...`);
|
|
2616
|
-
result = await createGitHubIssue(entry);
|
|
2775
|
+
result = await createGitHubIssue(entry, { publishDemo });
|
|
2617
2776
|
if (result.ok) {
|
|
2618
2777
|
ok(`GitHub issue created: ${result.externalUrl}`);
|
|
2619
2778
|
tagIntakeWithExternalRef(cwd, id, 'github', result.externalUrl, result.externalId);
|
|
2779
|
+
} else if (result.skipped === 'demo-source') {
|
|
2780
|
+
warn(`Skipped: ${result.error}`);
|
|
2781
|
+
process.exit(0);
|
|
2620
2782
|
} else {
|
|
2621
2783
|
errorln(`Failed: ${result.error}`);
|
|
2622
2784
|
process.exit(1);
|
|
@@ -2927,6 +3089,10 @@ async function cmdGraph(args) {
|
|
|
2927
3089
|
const sub = args[0];
|
|
2928
3090
|
if (!sub || sub === '--help' || sub === '-h') { printGraphHelp(); return; }
|
|
2929
3091
|
|
|
3092
|
+
if (sub === 'recommend') {
|
|
3093
|
+
return await cmdEmbeddedTriage(args.slice(1));
|
|
3094
|
+
}
|
|
3095
|
+
|
|
2930
3096
|
const { FilesystemTaskGraphStore } = await import('../lib/task-graph/store.mjs');
|
|
2931
3097
|
const { generateTaskGraphFromTriage } = await import('../lib/task-graph/generate.mjs');
|
|
2932
3098
|
const cwd = process.cwd();
|
|
@@ -3160,6 +3326,23 @@ async function cmdValidate() {
|
|
|
3160
3326
|
}
|
|
3161
3327
|
|
|
3162
3328
|
async function cmdModels(args) {
|
|
3329
|
+
if (args[0] === 'resolve') {
|
|
3330
|
+
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
3331
|
+
const { resolveEmbeddedModel } = await import('../lib/embedded-contract/model-resolve.mjs');
|
|
3332
|
+
const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
|
|
3333
|
+
const request = {
|
|
3334
|
+
workflowType: flag('--workflow-type'),
|
|
3335
|
+
requestedTier: flag('--tier'),
|
|
3336
|
+
host: flag('--host'),
|
|
3337
|
+
hostModel: flag('--host-model'),
|
|
3338
|
+
hostProvider: flag('--host-provider'),
|
|
3339
|
+
capabilities: flag('--capabilities')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
3340
|
+
allowCrossProviderFallback: args.includes('--allow-cross-provider'),
|
|
3341
|
+
};
|
|
3342
|
+
const envelope = wrapContractResult(resolveEmbeddedModel(request), { surface: 'cli' });
|
|
3343
|
+
println(JSON.stringify(envelope, null, 2));
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3163
3346
|
const tier = args.find((arg) => arg.startsWith('--tier='))?.split('=')[1] ?? '';
|
|
3164
3347
|
const setModel = args.find((arg) => arg.startsWith('--set='))?.split('=')[1] ?? '';
|
|
3165
3348
|
const envPath = getUserEnvPath(HOME);
|
|
@@ -4557,9 +4740,56 @@ async function cmdProviders(args) {
|
|
|
4557
4740
|
process.exit(1);
|
|
4558
4741
|
}
|
|
4559
4742
|
|
|
4743
|
+
// Capability discovery surface — a read-only, secret-free description of the
|
|
4744
|
+
// installed Construct's capabilities, for embedding applications.
|
|
4745
|
+
|
|
4746
|
+
async function cmdCapability(args) {
|
|
4747
|
+
const sub = args[0];
|
|
4748
|
+
if (sub && sub !== 'describe' && !sub.startsWith('--')) {
|
|
4749
|
+
errorln(`Unknown capability subcommand: ${sub}. Available: describe`);
|
|
4750
|
+
process.exit(1);
|
|
4751
|
+
}
|
|
4752
|
+
const { buildCapabilityContract } = await import('../lib/embedded-contract/capability.mjs');
|
|
4753
|
+
const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
|
|
4754
|
+
const envelope = wrapContractResult(buildCapabilityContract({ rootDir: ROOT_DIR }), { surface: 'cli' });
|
|
4755
|
+
println(JSON.stringify(envelope, null, 2));
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4560
4758
|
async function cmdWorkflow(args) {
|
|
4561
4759
|
const sub = args[0] || 'list';
|
|
4562
4760
|
|
|
4761
|
+
if (sub === 'invoke') {
|
|
4762
|
+
const rest = args.slice(1);
|
|
4763
|
+
const flag = (name) => { const i = rest.indexOf(name); return i !== -1 ? rest[i + 1] : undefined; };
|
|
4764
|
+
let input = flag('--text') || '';
|
|
4765
|
+
const fileArg = flag('--file');
|
|
4766
|
+
let ingestion = null;
|
|
4767
|
+
if (!input && fileArg) {
|
|
4768
|
+
const { resolveInput } = await import('../lib/embedded-contract/ingest.mjs');
|
|
4769
|
+
const resolved = await resolveInput({ filePath: fileArg });
|
|
4770
|
+
input = resolved.text;
|
|
4771
|
+
ingestion = resolved.error ? { ...(resolved.ingestion || {}), error: resolved.error } : resolved.ingestion;
|
|
4772
|
+
}
|
|
4773
|
+
if (!input && !process.stdin.isTTY) input = fs.readFileSync(0, 'utf8');
|
|
4774
|
+
const request = {
|
|
4775
|
+
workflowType: flag('--workflow-type'),
|
|
4776
|
+
input,
|
|
4777
|
+
ingestion,
|
|
4778
|
+
roleStrategy: flag('--role-strategy'),
|
|
4779
|
+
requestedRoles: flag('--roles')?.split(',').map((s) => s.trim()).filter(Boolean),
|
|
4780
|
+
approvalMode: flag('--approval-mode'),
|
|
4781
|
+
trace: !rest.includes('--no-trace'),
|
|
4782
|
+
host: flag('--host'),
|
|
4783
|
+
hostModel: flag('--host-model'),
|
|
4784
|
+
hostProvider: flag('--host-provider'),
|
|
4785
|
+
};
|
|
4786
|
+
const { invokeWorkflow } = await import('../lib/embedded-contract/workflow-invoke.mjs');
|
|
4787
|
+
const { wrapContractResult } = await import('../lib/embedded-contract/envelope.mjs');
|
|
4788
|
+
const envelope = wrapContractResult(await invokeWorkflow(request), { surface: 'cli' });
|
|
4789
|
+
println(JSON.stringify(envelope, null, 2));
|
|
4790
|
+
return;
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4563
4793
|
if (sub === 'list') {
|
|
4564
4794
|
const { existsSync, readdirSync } = await import('node:fs');
|
|
4565
4795
|
const { join } = await import('node:path');
|
|
@@ -4937,6 +5167,7 @@ const handlers = new Map([
|
|
|
4937
5167
|
['creds', cmdCreds],
|
|
4938
5168
|
['providers', cmdProviders],
|
|
4939
5169
|
['workflow', cmdWorkflow],
|
|
5170
|
+
['capability', cmdCapability],
|
|
4940
5171
|
['tags', cmdTags],
|
|
4941
5172
|
['scheduler', cmdScheduler],
|
|
4942
5173
|
['lint:research', cmdLintResearch],
|
|
@@ -4980,13 +5211,11 @@ const handlers = new Map([
|
|
|
4980
5211
|
['roles:list', async (args) => {
|
|
4981
5212
|
const { listRoles, formatRoleList } = await import('../lib/roles/catalog.mjs');
|
|
4982
5213
|
const json = args.includes('--json');
|
|
4983
|
-
|
|
4984
|
-
const consolidated = args.includes('--consolidated');
|
|
4985
|
-
|
|
5214
|
+
|
|
4986
5215
|
if (json) {
|
|
4987
|
-
println(JSON.stringify(listRoles(
|
|
5216
|
+
println(JSON.stringify(listRoles(), null, 2));
|
|
4988
5217
|
} else {
|
|
4989
|
-
process.stdout.write(formatRoleList(
|
|
5218
|
+
process.stdout.write(formatRoleList());
|
|
4990
5219
|
}
|
|
4991
5220
|
}],
|
|
4992
5221
|
['roles:set', async (args) => {
|
|
@@ -5850,6 +6079,13 @@ if (command === '--help' || command === '-h' || command === 'help') {
|
|
|
5850
6079
|
process.exit(0);
|
|
5851
6080
|
}
|
|
5852
6081
|
|
|
6082
|
+
// --all / -a as a standalone flag — equivalent to `help --all`. Without
|
|
6083
|
+
// this, `construct --all` (which the help footer advertises) 404s.
|
|
6084
|
+
if (command === '--all' || command === '-a') {
|
|
6085
|
+
usage({ all: true });
|
|
6086
|
+
process.exit(0);
|
|
6087
|
+
}
|
|
6088
|
+
|
|
5853
6089
|
// Version flags → print version and exit
|
|
5854
6090
|
if (command === '--version' || command === '-V') {
|
|
5855
6091
|
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
|
|
@@ -5860,11 +6096,53 @@ if (command === '--version' || command === '-V') {
|
|
|
5860
6096
|
const handler = handlers.get(command);
|
|
5861
6097
|
if (!handler) {
|
|
5862
6098
|
errorln(`Unknown command: ${command}`);
|
|
6099
|
+
// Three-tier suggestion: prefix match → substring match → Levenshtein
|
|
6100
|
+
// distance ≤ 2. Catches single-letter typos, missing-letter typos, and
|
|
6101
|
+
// transpositions ('sycn' → 'sync') without dragging in a full fuzzy lib.
|
|
6102
|
+
const known = CLI_COMMANDS.filter((c) => !c.internal).map((c) => c.name);
|
|
6103
|
+
const editDistance = (a, b) => {
|
|
6104
|
+
if (a === b) return 0;
|
|
6105
|
+
if (!a.length || !b.length) return Math.max(a.length, b.length);
|
|
6106
|
+
const row = Array(b.length + 1).fill(0).map((_, i) => i);
|
|
6107
|
+
for (let i = 1; i <= a.length; i++) {
|
|
6108
|
+
let prev = i;
|
|
6109
|
+
for (let j = 1; j <= b.length; j++) {
|
|
6110
|
+
const cur = a[i - 1] === b[j - 1]
|
|
6111
|
+
? row[j - 1]
|
|
6112
|
+
: 1 + Math.min(row[j - 1], row[j], prev);
|
|
6113
|
+
row[j - 1] = prev;
|
|
6114
|
+
prev = cur;
|
|
6115
|
+
}
|
|
6116
|
+
row[b.length] = prev;
|
|
6117
|
+
}
|
|
6118
|
+
return row[b.length];
|
|
6119
|
+
};
|
|
6120
|
+
let suggestion = known.find((n) => n.startsWith(command)) ||
|
|
6121
|
+
known.find((n) => n.includes(command));
|
|
6122
|
+
if (!suggestion) {
|
|
6123
|
+
let bestDist = 3;
|
|
6124
|
+
for (const name of known) {
|
|
6125
|
+
const d = editDistance(command, name);
|
|
6126
|
+
if (d < bestDist) { bestDist = d; suggestion = name; }
|
|
6127
|
+
}
|
|
6128
|
+
}
|
|
6129
|
+
if (suggestion) errorln(`Did you mean: construct ${suggestion}?`);
|
|
5863
6130
|
println('');
|
|
5864
6131
|
println(`${COLORS.dim}Run 'construct --help' for available commands${COLORS.reset}`);
|
|
5865
6132
|
process.exit(1);
|
|
5866
6133
|
}
|
|
5867
6134
|
|
|
6135
|
+
// Per-command --help / -h intercept. Critical safety: without this guard,
|
|
6136
|
+
// commands like `dev`, `stop`, `init`, `uninstall` silently ignore --help
|
|
6137
|
+
// and execute (running `construct stop --help` actually stops services).
|
|
6138
|
+
// Every command — including internal ones — gets the same shape via
|
|
6139
|
+
// formatCommandHelp(); per-command handlers need no opt-in.
|
|
6140
|
+
|
|
6141
|
+
if (rest.includes('--help') || rest.includes('-h')) {
|
|
6142
|
+
process.stdout.write(formatCommandHelp(command, { colors: true }));
|
|
6143
|
+
process.exit(0);
|
|
6144
|
+
}
|
|
6145
|
+
|
|
5868
6146
|
// Self-maintenance on version change. Skipped for high-frequency hook calls
|
|
5869
6147
|
// (the harness invokes `construct hook <name>` many times per session and
|
|
5870
6148
|
// must stay fast). When the installed version differs from the stamp at
|