@geraldmaron/construct 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/host-capabilities.mjs — <one-line purpose>
|
|
4
|
+
*
|
|
5
|
+
* <2–6 line summary.>
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
8
|
+
import { readOpenCodeConfig } from "./opencode-config.mjs";
|
|
9
|
+
import net from "node:net";
|
|
10
|
+
|
|
11
|
+
export async function findAvailablePort(startPort) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const server = net.createServer();
|
|
14
|
+
server.listen(startPort, () => {
|
|
15
|
+
const { port } = server.address();
|
|
16
|
+
server.close(() => resolve(port));
|
|
17
|
+
});
|
|
18
|
+
server.on("error", () => {
|
|
19
|
+
resolve(findAvailablePort(startPort + 1));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function commandVersion(command, args = ["--version"]) {
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync(command, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
|
|
27
|
+
.toString()
|
|
28
|
+
.trim();
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseClaudeVersion(raw) {
|
|
35
|
+
const match = String(raw || "").match(/(\d+)\.(\d+)\.(\d+)/);
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
return {
|
|
38
|
+
major: Number(match[1]),
|
|
39
|
+
minor: Number(match[2]),
|
|
40
|
+
patch: Number(match[3]),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function versionAtLeast(version, minimum) {
|
|
45
|
+
if (!version) return false;
|
|
46
|
+
for (const part of ["major", "minor", "patch"]) {
|
|
47
|
+
if (version[part] > minimum[part]) return true;
|
|
48
|
+
if (version[part] < minimum[part]) return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function detectHostCapabilities() {
|
|
54
|
+
const claudeRaw = commandVersion("claude");
|
|
55
|
+
const claudeVersion = parseClaudeVersion(claudeRaw);
|
|
56
|
+
const claudeTeamsSupported = versionAtLeast(claudeVersion, { major: 2, minor: 1, patch: 32 });
|
|
57
|
+
const tmuxRaw = commandVersion("tmux", ["-V"]);
|
|
58
|
+
const opencodeRaw = commandVersion("opencode");
|
|
59
|
+
const codexRaw = commandVersion("codex");
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
host: "Claude Code",
|
|
64
|
+
availability: claudeRaw ? "installed" : "missing",
|
|
65
|
+
version: claudeRaw,
|
|
66
|
+
orchestration: claudeTeamsSupported ? "full-multi-agent" : "primary-plus-subagents",
|
|
67
|
+
promptableWorkers: claudeTeamsSupported,
|
|
68
|
+
sharedTaskRuntime: claudeTeamsSupported,
|
|
69
|
+
lifecycleHooks: ["SubagentStop", "TeammateIdle", "TaskCreated", "TaskCompleted", "Stop"],
|
|
70
|
+
notes: claudeTeamsSupported
|
|
71
|
+
? [
|
|
72
|
+
"Best host for full multi-agent orchestration.",
|
|
73
|
+
"Enable CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1.",
|
|
74
|
+
tmuxRaw ? "tmux split-pane display is available." : "tmux is not installed; use in-process teammate mode or install tmux/iTerm2 integration.",
|
|
75
|
+
]
|
|
76
|
+
: ["Upgrade Claude Code to 2.1.32 or newer for Agent Teams."],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
host: "OpenCode",
|
|
80
|
+
availability: opencodeRaw ? "installed" : "missing",
|
|
81
|
+
version: opencodeRaw,
|
|
82
|
+
orchestration: "primary-plus-subagents",
|
|
83
|
+
promptableWorkers: false,
|
|
84
|
+
sharedTaskRuntime: "construct-workflow",
|
|
85
|
+
lifecycleHooks: ["session.error", "session.idle", "tool.execute.before", "tool.execute.after"],
|
|
86
|
+
notes: [
|
|
87
|
+
"Primary agents are promptable; subagents are bounded worker sessions.",
|
|
88
|
+
"Construct uses .cx/workflow.json plus task permissions and plugins to coordinate parallel worker execution.",
|
|
89
|
+
"Use NEEDS_MAIN_INPUT to route user questions back to the primary persona.",
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
host: "Codex",
|
|
94
|
+
availability: codexRaw ? "installed" : "missing",
|
|
95
|
+
version: codexRaw,
|
|
96
|
+
orchestration: "profile-and-mcp",
|
|
97
|
+
promptableWorkers: false,
|
|
98
|
+
sharedTaskRuntime: "construct-workflow",
|
|
99
|
+
lifecycleHooks: [],
|
|
100
|
+
notes: [
|
|
101
|
+
"Use Construct profiles, MCP workflow tools, and the active session.",
|
|
102
|
+
"Native profile switching is not automatic; continue in-session when dispatch is unavailable.",
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function printHostCapabilities(hosts = detectHostCapabilities()) {
|
|
109
|
+
console.log("Construct orchestration host capabilities:");
|
|
110
|
+
for (const host of hosts) {
|
|
111
|
+
console.log("");
|
|
112
|
+
console.log(`${host.host}: ${host.availability}${host.version ? ` (${host.version})` : ""}`);
|
|
113
|
+
console.log(` orchestration: ${host.orchestration}`);
|
|
114
|
+
console.log(` promptable workers: ${host.promptableWorkers === true ? "yes" : "no"}`);
|
|
115
|
+
console.log(` shared task runtime: ${host.sharedTaskRuntime === true ? "native" : host.sharedTaskRuntime || "none"}`);
|
|
116
|
+
if (host.lifecycleHooks.length) console.log(` lifecycle hooks: ${host.lifecycleHooks.join(", ")}`);
|
|
117
|
+
for (const note of host.notes) console.log(` - ${note}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
122
|
+
printHostCapabilities();
|
|
123
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/init-docs.mjs — AI-powered doc structure scaffolding
|
|
4
|
+
*
|
|
5
|
+
* Uses a fast model (Haiku) to analyze the project, ask 2-3 clarifying
|
|
6
|
+
* questions, then generate a tailored documentation structure.
|
|
7
|
+
* Falls back to a minimal static scaffold if no API key is available.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node lib/init-docs.mjs [target-path] [--yes]
|
|
11
|
+
* construct init-docs [path] [--yes]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import readline from "node:readline";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { defaultWorkflow } from "./workflow-state.mjs";
|
|
20
|
+
import { readOpenRouterApiKeyFromOpenCodeConfig } from "./model-router.mjs";
|
|
21
|
+
|
|
22
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
23
|
+
const ROOT_DIR = path.join(__dirname, "..");
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2);
|
|
26
|
+
const skipInteractive = args.includes("--yes") || !process.stdin.isTTY;
|
|
27
|
+
const targetArg = args.find((a) => !a.startsWith("--"));
|
|
28
|
+
const target = path.resolve(targetArg ?? process.cwd());
|
|
29
|
+
|
|
30
|
+
const FAST_MODEL = "claude-haiku-4-5-20251001";
|
|
31
|
+
|
|
32
|
+
// ─── .env loader ──────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function loadEnv(envPath) {
|
|
35
|
+
if (!fs.existsSync(envPath)) return;
|
|
36
|
+
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
39
|
+
const eq = trimmed.indexOf("=");
|
|
40
|
+
if (eq === -1) continue;
|
|
41
|
+
const key = trimmed.slice(0, eq).trim();
|
|
42
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
43
|
+
if (!process.env[key]) process.env[key] = val;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
loadEnv(path.join(ROOT_DIR, ".env"));
|
|
48
|
+
|
|
49
|
+
// ─── API caller ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
async function callModel(messages, system) {
|
|
52
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
53
|
+
if (anthropicKey) {
|
|
54
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
"x-api-key": anthropicKey,
|
|
58
|
+
"anthropic-version": "2023-06-01",
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify({ model: FAST_MODEL, max_tokens: 4096, system, messages }),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${await res.text()}`);
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
return data.content[0].text;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const orKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
|
|
69
|
+
if (orKey) {
|
|
70
|
+
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${orKey}`,
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
"HTTP-Referer": "https://github.com/construct",
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
model: `anthropic/${FAST_MODEL}`,
|
|
79
|
+
max_tokens: 4096,
|
|
80
|
+
messages: [{ role: "system", content: system }, ...messages],
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) throw new Error(`OpenRouter API ${res.status}: ${await res.text()}`);
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
return data.choices[0].message.content;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Project context ──────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
function dirTree(dir, depth = 0, maxDepth = 2) {
|
|
94
|
+
if (depth > maxDepth) return [];
|
|
95
|
+
const skip = new Set(["node_modules", ".git", ".next", "dist", "build", "out",
|
|
96
|
+
"__pycache__", ".venv", "vendor", "target", ".turbo", "coverage"]);
|
|
97
|
+
let items;
|
|
98
|
+
try { items = fs.readdirSync(dir); } catch { return []; }
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (const item of items.sort()) {
|
|
101
|
+
if (skip.has(item) || (item.startsWith(".") && depth === 0 && item !== ".cx")) continue;
|
|
102
|
+
const full = path.join(dir, item);
|
|
103
|
+
let stat;
|
|
104
|
+
try { stat = fs.statSync(full); } catch { continue; }
|
|
105
|
+
if (stat.isDirectory()) {
|
|
106
|
+
entries.push(`${" ".repeat(depth)}${item}/`);
|
|
107
|
+
entries.push(...dirTree(full, depth + 1, maxDepth));
|
|
108
|
+
} else {
|
|
109
|
+
entries.push(`${" ".repeat(depth)}${item}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function gatherContext(targetPath) {
|
|
116
|
+
const ctx = { name: path.basename(targetPath) };
|
|
117
|
+
|
|
118
|
+
for (const name of ["package.json", "pyproject.toml", "Cargo.toml", "go.mod", "build.gradle", "composer.json"]) {
|
|
119
|
+
const p = path.join(targetPath, name);
|
|
120
|
+
if (!fs.existsSync(p)) continue;
|
|
121
|
+
if (name === "package.json") {
|
|
122
|
+
try {
|
|
123
|
+
const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
124
|
+
ctx.name = pkg.name ?? ctx.name;
|
|
125
|
+
ctx.description = pkg.description;
|
|
126
|
+
const deps = Object.keys({ ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) });
|
|
127
|
+
ctx.dependencies = deps.slice(0, 40);
|
|
128
|
+
ctx.scripts = Object.keys(pkg.scripts ?? {});
|
|
129
|
+
} catch {}
|
|
130
|
+
} else {
|
|
131
|
+
ctx.manifest = fs.readFileSync(p, "utf8").slice(0, 600);
|
|
132
|
+
}
|
|
133
|
+
ctx.manifestFile = name;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const name of ["README.md", "readme.md", "README.txt"]) {
|
|
138
|
+
const p = path.join(targetPath, name);
|
|
139
|
+
if (fs.existsSync(p)) {
|
|
140
|
+
ctx.readme = fs.readFileSync(p, "utf8").slice(0, 2000);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ctx.structure = dirTree(targetPath).join("\n");
|
|
146
|
+
|
|
147
|
+
const docsPath = path.join(targetPath, "docs");
|
|
148
|
+
if (fs.existsSync(docsPath)) {
|
|
149
|
+
ctx.existingDocs = fs.readdirSync(docsPath).slice(0, 20).join(", ");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
ctx.gitRemote = execSync("git remote get-url origin", { cwd: targetPath, timeout: 3000, stdio: ["pipe", "pipe", "pipe"] })
|
|
154
|
+
.toString().trim();
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
return ctx;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function contextToText(ctx) {
|
|
161
|
+
const parts = [`Project: ${ctx.name}`];
|
|
162
|
+
if (ctx.description) parts.push(`Description: ${ctx.description}`);
|
|
163
|
+
if (ctx.gitRemote) parts.push(`Git remote: ${ctx.gitRemote}`);
|
|
164
|
+
if (ctx.manifestFile) parts.push(`Manifest: ${ctx.manifestFile}`);
|
|
165
|
+
if (ctx.dependencies?.length) parts.push(`Dependencies: ${ctx.dependencies.join(", ")}`);
|
|
166
|
+
if (ctx.scripts?.length) parts.push(`npm scripts: ${ctx.scripts.join(", ")}`);
|
|
167
|
+
if (ctx.manifest) parts.push(`\nManifest content:\n${ctx.manifest}`);
|
|
168
|
+
if (ctx.readme) parts.push(`\nREADME:\n${ctx.readme}`);
|
|
169
|
+
if (ctx.structure) parts.push(`\nDirectory structure:\n${ctx.structure}`);
|
|
170
|
+
if (ctx.existingDocs) parts.push(`\nExisting docs/: ${ctx.existingDocs}`);
|
|
171
|
+
return parts.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── JSON extraction ──────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function extractJson(text) {
|
|
177
|
+
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
178
|
+
const raw = (fence ? fence[1] : text).trim();
|
|
179
|
+
const start = raw.indexOf("{");
|
|
180
|
+
const end = raw.lastIndexOf("}");
|
|
181
|
+
if (start === -1 || end === -1) throw new Error("No JSON object found in response");
|
|
182
|
+
return JSON.parse(raw.slice(start, end + 1));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── File writing ─────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
const created = [];
|
|
188
|
+
const skipped = [];
|
|
189
|
+
|
|
190
|
+
function writeIfMissing(filePath, content) {
|
|
191
|
+
if (fs.existsSync(filePath)) {
|
|
192
|
+
skipped.push(path.relative(target, filePath));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
196
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
197
|
+
created.push(path.relative(target, filePath));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Static fallback ──────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function scaffoldStatic(projectName, workflowJson) {
|
|
203
|
+
writeIfMissing(path.join(target, ".cx", "context.json"),
|
|
204
|
+
`${JSON.stringify({ format: "json", savedAt: new Date().toISOString(), source: "init-docs", projectName, activeWork: [], recentDecisions: [], architectureNotes: [], openQuestions: [] }, null, 2)}\n`);
|
|
205
|
+
writeIfMissing(path.join(target, ".cx", "context.md"),
|
|
206
|
+
`# Project Context\n\n> Required project state. Keep this file updated. All LLMs working in this repo, including Construct, read it at session start and must keep it current. Keep under 100 lines.\n\n## Active Work\n\n## Recent Decisions\n\n## Architecture Notes\n\n## Open Questions\n`);
|
|
207
|
+
writeIfMissing(path.join(target, ".cx", "workflow.json"), workflowJson);
|
|
208
|
+
writeIfMissing(path.join(target, ".cx", "decisions", "_template.md"),
|
|
209
|
+
`# ADR-{NNN}: {title}\n\nDate: {YYYY-MM-DD}\nStatus: proposed | accepted | deprecated\n\n## Context\n\n## Decision\n\n## Consequences\n`);
|
|
210
|
+
writeIfMissing(path.join(target, "docs", "README.md"),
|
|
211
|
+
`# ${projectName} — Documentation
|
|
212
|
+
|
|
213
|
+
> Required project state. All LLMs working in this repo, including Construct, must keep the core documents below current.
|
|
214
|
+
|
|
215
|
+
## Required core documents
|
|
216
|
+
|
|
217
|
+
| File | Purpose | Update when |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| .cx/context.md | Session-resumable human summary | Active work, decisions, architecture assumptions, or open questions change |
|
|
220
|
+
| .cx/context.json | Machine-readable resumable context | Context state needs to stay in sync with .cx/context.md |
|
|
221
|
+
| .cx/workflow.json | Canonical workflow/task state | Non-trivial work starts, changes phase, or completes |
|
|
222
|
+
| docs/README.md | Docs index and documentation contract | Core docs set or maintenance expectations change |
|
|
223
|
+
| docs/architecture.md | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |
|
|
224
|
+
|
|
225
|
+
## Contents
|
|
226
|
+
|
|
227
|
+
- [Architecture](./architecture.md)
|
|
228
|
+
- [Runbooks](./runbooks/)
|
|
229
|
+
- [ADRs](../.cx/decisions/)
|
|
230
|
+
|
|
231
|
+
## Maintenance rule
|
|
232
|
+
|
|
233
|
+
If work changes project reality, update the affected core document before calling it done.
|
|
234
|
+
`);
|
|
235
|
+
writeIfMissing(path.join(target, "docs", "architecture.md"),
|
|
236
|
+
`# ${projectName} Architecture\n\n> Required project state. Keep this file updated when system shape, contracts, boundaries, or dependencies materially change. All LLMs working in this repo should treat it as canonical architecture context.\n\n## System overview\n\nDescribe the main runtime shape, primary modules, and external dependencies.\n\n## Core layers\n\n- CLI / entrypoints\n- Application/runtime modules\n- State, data, and storage\n- External integrations\n\n## Key invariants\n\n- Public surface and ownership boundaries\n- Data/contract expectations\n- Safety or review gates\n`);
|
|
237
|
+
writeIfMissing(path.join(target, "docs", "runbooks", "README.md"),
|
|
238
|
+
`# Runbooks\n\n## Contents\n\n- Local development startup\n- Verification / health checks\n- Incident recovery\n- Release checklist\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Prompts ──────────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
const SYSTEM_QUESTIONS = `You are a documentation architect. You've been given context about a software project.
|
|
244
|
+
|
|
245
|
+
Ask 2–3 targeted clarifying questions to fill gaps that will materially affect the doc structure.
|
|
246
|
+
Good questions cover things like: team collaboration style, external consumers, operational complexity,
|
|
247
|
+
compliance needs, or release cadence — only when these aren't clear from the context.
|
|
248
|
+
|
|
249
|
+
Do NOT ask about things already obvious from the code. Do NOT ask open-ended questions.
|
|
250
|
+
Each question should have a short, specific answer (a few words or a sentence).
|
|
251
|
+
|
|
252
|
+
Output ONLY valid JSON:
|
|
253
|
+
{
|
|
254
|
+
"questions": [
|
|
255
|
+
{ "id": "q1", "question": "..." },
|
|
256
|
+
{ "id": "q2", "question": "..." }
|
|
257
|
+
]
|
|
258
|
+
}`;
|
|
259
|
+
|
|
260
|
+
const SYSTEM_GENERATE = `You are a documentation architect generating a tailored doc structure.
|
|
261
|
+
|
|
262
|
+
Generate complete, project-specific files — not generic templates. Use real section headings,
|
|
263
|
+
meaningful placeholder content, and examples that match the actual tech stack and project type.
|
|
264
|
+
|
|
265
|
+
Rules:
|
|
266
|
+
- Always include .cx/context.md, .cx/context.json, .cx/workflow.json, docs/README.md, and docs/architecture.md
|
|
267
|
+
- Treat these files as required, maintained project state for all LLMs working in the repo, including Construct
|
|
268
|
+
- .cx/context.md should stay under 100 lines and contain real project sections
|
|
269
|
+
- Use the provided .cx/workflow.json verbatim
|
|
270
|
+
- Only include docs that make sense for THIS project — skip what doesn't apply
|
|
271
|
+
- Prefer fewer, higher-quality files over many hollow ones
|
|
272
|
+
- For template files (e.g. ADR template), use the filename _template.md inside the relevant folder
|
|
273
|
+
|
|
274
|
+
Output ONLY valid JSON:
|
|
275
|
+
{
|
|
276
|
+
"summary": "One sentence: what was generated and why",
|
|
277
|
+
"files": [
|
|
278
|
+
{ "path": "relative/path/file.md", "content": "complete file content" }
|
|
279
|
+
]
|
|
280
|
+
}`;
|
|
281
|
+
|
|
282
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
async function main() {
|
|
285
|
+
console.log(`\nConstruct init-docs → ${target}\n`);
|
|
286
|
+
|
|
287
|
+
const ctx = gatherContext(target);
|
|
288
|
+
const contextText = contextToText(ctx);
|
|
289
|
+
const workflowJson = JSON.stringify(defaultWorkflow(target, ctx.name), null, 2) + "\n";
|
|
290
|
+
|
|
291
|
+
// Attempt AI path
|
|
292
|
+
let questionsResponse = null;
|
|
293
|
+
try {
|
|
294
|
+
process.stdout.write(" Analyzing project...");
|
|
295
|
+
questionsResponse = await callModel([{ role: "user", content: contextText }], SYSTEM_QUESTIONS);
|
|
296
|
+
process.stdout.write(" done\n\n");
|
|
297
|
+
} catch (err) {
|
|
298
|
+
process.stdout.write(` failed (${err.message})\n`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!questionsResponse) {
|
|
302
|
+
console.log(" No API key found — using static scaffold.\n");
|
|
303
|
+
console.log(" Set ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env for AI-tailored output.\n");
|
|
304
|
+
scaffoldStatic(ctx.name, workflowJson);
|
|
305
|
+
printSummary();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Parse and ask questions
|
|
310
|
+
let questions = [];
|
|
311
|
+
try {
|
|
312
|
+
questions = extractJson(questionsResponse).questions ?? [];
|
|
313
|
+
} catch {
|
|
314
|
+
// skip questions if parse fails, go straight to generation
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const answers = {};
|
|
318
|
+
if (questions.length && !skipInteractive) {
|
|
319
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
320
|
+
console.log("A few questions to tailor the structure:\n");
|
|
321
|
+
for (const q of questions) {
|
|
322
|
+
const answer = await new Promise((resolve) => rl.question(` ${q.question}\n > `, resolve));
|
|
323
|
+
answers[q.id] = answer.trim();
|
|
324
|
+
console.log();
|
|
325
|
+
}
|
|
326
|
+
rl.close();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Build generation prompt
|
|
330
|
+
const qaText = questions.length && Object.keys(answers).length
|
|
331
|
+
? "\n\nAnswers to clarifying questions:\n" +
|
|
332
|
+
questions.filter((q) => answers[q.id]).map((q) => `Q: ${q.question}\nA: ${answers[q.id]}`).join("\n\n")
|
|
333
|
+
: "";
|
|
334
|
+
|
|
335
|
+
const genPrompt = `${contextText}${qaText}\n\n---\n.cx/workflow.json (use verbatim):\n${workflowJson}`;
|
|
336
|
+
|
|
337
|
+
let genResponse = null;
|
|
338
|
+
try {
|
|
339
|
+
process.stdout.write(" Generating doc structure...");
|
|
340
|
+
genResponse = await callModel([{ role: "user", content: genPrompt }], SYSTEM_GENERATE);
|
|
341
|
+
process.stdout.write(" done\n\n");
|
|
342
|
+
} catch (err) {
|
|
343
|
+
process.stdout.write(` failed (${err.message})\n`);
|
|
344
|
+
console.log(" Falling back to static scaffold.\n");
|
|
345
|
+
scaffoldStatic(ctx.name, workflowJson);
|
|
346
|
+
printSummary();
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const plan = extractJson(genResponse);
|
|
352
|
+
if (plan.summary) console.log(` ${plan.summary}\n`);
|
|
353
|
+
for (const file of plan.files ?? []) {
|
|
354
|
+
if (file.path && file.content) {
|
|
355
|
+
writeIfMissing(path.join(target, file.path), file.content);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.warn(` Could not parse model output (${err.message}). Falling back to static scaffold.\n`);
|
|
360
|
+
scaffoldStatic(ctx.name, workflowJson);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
printSummary();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function printSummary() {
|
|
367
|
+
console.log(`Doc structure initialized at: ${target}\n`);
|
|
368
|
+
if (created.length) {
|
|
369
|
+
console.log("Created:");
|
|
370
|
+
for (const f of created) console.log(` + ${f}`);
|
|
371
|
+
}
|
|
372
|
+
if (skipped.length) {
|
|
373
|
+
console.log("\nSkipped (already exist):");
|
|
374
|
+
for (const f of skipped) console.log(` ~ ${f}`);
|
|
375
|
+
}
|
|
376
|
+
console.log(`\n${created.length} created, ${skipped.length} skipped.`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
main().catch((err) => {
|
|
380
|
+
console.error("Error:", err.message);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
});
|