@amsterdamdatalabs/enact-extensions 0.1.0 → 0.1.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 +96 -21
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +171 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +402 -49
- package/dist/install.js.map +1 -1
- package/dist/internal/codex.d.ts.map +1 -1
- package/dist/internal/codex.js +7 -1
- package/dist/internal/codex.js.map +1 -1
- package/dist/internal/platform.d.ts +8 -0
- package/dist/internal/platform.d.ts.map +1 -1
- package/dist/internal/platform.js +46 -2
- package/dist/internal/platform.js.map +1 -1
- package/dist/provision.d.ts +30 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +202 -0
- package/dist/provision.js.map +1 -0
- package/dist/validate/index.d.ts +44 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +157 -0
- package/dist/validate/index.js.map +1 -1
- package/extensions/cmux/.agents/plugin.json +37 -0
- package/extensions/cmux/skills/cmux/SKILL.md +82 -0
- package/extensions/cmux/skills/cmux/agents/openai.yaml +4 -0
- package/extensions/cmux/skills/cmux/references/handles-and-identify.md +35 -0
- package/extensions/cmux/skills/cmux/references/panes-surfaces.md +37 -0
- package/extensions/cmux/skills/cmux/references/trigger-flash-and-health.md +23 -0
- package/extensions/cmux/skills/cmux/references/windows-workspaces.md +31 -0
- package/extensions/cmux/skills/cmux-vm-monitor/SKILL.md +122 -0
- package/extensions/cmux/skills/cmux-vm-monitor/agents/openai.yaml +4 -0
- package/extensions/cmux/skills/cmux-vm-monitor/references/cmux-commands.md +66 -0
- package/extensions/cmux/skills/cmux-vm-monitor/scripts/codex_vm_monitor.sh +45 -0
- package/extensions/cmux/skills/cmux-workspace/SKILL.md +93 -0
- package/extensions/dev-state/.agents/plugin.json +35 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/SKILL.md +194 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/agents/openai.yaml +4 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/references/reference.md +130 -0
- package/extensions/devops/.agents/plugin.json +36 -0
- package/extensions/devops/skills/azure-devops-cli/SKILL.md +431 -0
- package/extensions/devops/skills/azure-devops-cli/agents/openai.yaml +4 -0
- package/extensions/devops/skills/ci-pipeline-strategy/SKILL.md +217 -0
- package/extensions/devops/skills/ci-pipeline-strategy/agents/openai.yaml +4 -0
- package/extensions/enact-context/.agents/plugin.json +40 -0
- package/extensions/enact-context/.mcp.json +8 -0
- package/extensions/enact-context/README.md +25 -0
- package/extensions/enact-context/assets/icon.png +0 -0
- package/extensions/enact-context/assets/logo.png +0 -0
- package/extensions/enact-context/hooks/hooks.json +115 -0
- package/extensions/enact-context/skills/enact-context/SKILL.md +149 -0
- package/extensions/enact-context/skills/enact-context/scripts/install.sh +69 -0
- package/extensions/enact-factory/.agents/plugin.json +42 -0
- package/extensions/enact-factory/.mcp.json +8 -0
- package/extensions/enact-factory/assets/icon.png +0 -0
- package/extensions/enact-factory/assets/logo.png +0 -0
- package/extensions/enact-factory/hooks/user-prompt-submit.mjs +67 -0
- package/extensions/enact-factory/skills/testing-strategy/SKILL.md +167 -0
- package/extensions/enact-factory/skills/workitem-triage/SKILL.md +22 -0
- package/extensions/enact-operator/.agents/plugin.json +57 -0
- package/extensions/enact-operator/.app.json +3 -0
- package/extensions/enact-operator/.mcp.json +10 -0
- package/extensions/enact-operator/_taxonomy.md +86 -0
- package/extensions/enact-operator/agents/README.md +5 -0
- package/extensions/enact-operator/agents/architect.toml +25 -0
- package/extensions/enact-operator/agents/code-reviewer.toml +24 -0
- package/extensions/enact-operator/agents/critic.toml +30 -0
- package/extensions/enact-operator/agents/executor.toml +24 -0
- package/extensions/enact-operator/agents/explore.toml +23 -0
- package/extensions/enact-operator/agents/planner.toml +24 -0
- package/extensions/enact-operator/agents/verifier.toml +24 -0
- package/extensions/enact-operator/assets/icon.png +0 -0
- package/extensions/enact-operator/assets/logo.png +0 -0
- package/extensions/enact-operator/commands/doctor.md +39 -0
- package/extensions/enact-operator/commands/setup.md +51 -0
- package/extensions/enact-operator/hooks/hooks.json +146 -0
- package/extensions/enact-operator/skills/_variants.md +44 -0
- package/extensions/enact-operator/skills/ai-slop-cleaner/SKILL.md +50 -0
- package/extensions/enact-operator/skills/analyze/SKILL.md +91 -0
- package/extensions/enact-operator/skills/ask/SKILL.md +47 -0
- package/extensions/enact-operator/skills/autopilot/SKILL.md +170 -0
- package/extensions/enact-operator/skills/autoresearch-goal/SKILL.md +79 -0
- package/extensions/enact-operator/skills/cancel/SKILL.md +99 -0
- package/extensions/enact-operator/skills/configure-notifications/SKILL.md +77 -0
- package/extensions/enact-operator/skills/deep-interview/SKILL.md +80 -0
- package/extensions/enact-operator/skills/doctor/SKILL.md +48 -0
- package/extensions/enact-operator/skills/hud/SKILL.md +49 -0
- package/extensions/enact-operator/skills/hyperplan/SKILL.md +47 -0
- package/extensions/enact-operator/skills/plan/SKILL.md +78 -0
- package/extensions/enact-operator/skills/ralph/SKILL.md +201 -0
- package/extensions/enact-operator/skills/ralph/gemini.md +18 -0
- package/extensions/enact-operator/skills/ralplan/SKILL.md +151 -0
- package/extensions/enact-operator/skills/remove-deadcode/SKILL.md +45 -0
- package/extensions/enact-operator/skills/research/SKILL.md +74 -0
- package/extensions/enact-operator/skills/review/SKILL.md +58 -0
- package/extensions/enact-operator/skills/security-research/SKILL.md +54 -0
- package/extensions/enact-operator/skills/setup/SKILL.md +91 -0
- package/extensions/enact-operator/skills/setup/scripts/install.sh +50 -0
- package/extensions/enact-operator/skills/skill/SKILL.md +82 -0
- package/extensions/enact-operator/skills/tdd/SKILL.md +59 -0
- package/extensions/enact-operator/skills/team/SKILL.md +199 -0
- package/extensions/enact-operator/skills/trace/SKILL.md +41 -0
- package/extensions/enact-operator/skills/ultragoal/SKILL.md +99 -0
- package/extensions/enact-operator/skills/ultraqa/SKILL.md +113 -0
- package/extensions/enact-operator/skills/ultrawork/SKILL.md +145 -0
- package/extensions/enact-operator/skills/ultrawork/planner.md +28 -0
- package/extensions/enact-operator/skills/wiki/SKILL.md +41 -0
- package/extensions/enact-operator/skills/work-with-workitem/SKILL.md +51 -0
- package/extensions/enact-wiki/.agents/plugin.json +42 -0
- package/extensions/enact-wiki/.mcp.json +15 -0
- package/extensions/enact-wiki/README.md +44 -0
- package/extensions/enact-wiki/assets/icon.png +0 -0
- package/extensions/enact-wiki/assets/logo.png +0 -0
- package/extensions/enact-wiki/skills/document-parser/SKILL.md +17 -0
- package/extensions/enact-wiki/skills/document-parser/scripts/parse.sh +60 -0
- package/extensions/enact-wiki/skills/document-parser/skill.json +9 -0
- package/extensions/enact-wiki/skills/enact-wiki/SKILL.md +30 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/ingest.md +62 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/manage.md +34 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/query.md +59 -0
- package/extensions/enact-wiki/skills/search-lab/SKILL.md +57 -0
- package/extensions/enact-wiki/skills/search-lab/scripts/analyze.ts +23 -0
- package/{plugins/net-revenue-management/.codex-plugin → extensions/net-revenue-management/.agents}/plugin.json +10 -6
- package/extensions/plugin-dev/.agents/plugin.json +42 -0
- package/extensions/plugin-dev/.mcp.json +3 -0
- package/extensions/plugin-dev/agents/agent-creator.md +199 -0
- package/extensions/plugin-dev/agents/plugin-validator.md +91 -0
- package/extensions/plugin-dev/agents/skill-reviewer.md +212 -0
- package/extensions/plugin-dev/commands/_archive/create-marketplace.md +427 -0
- package/extensions/plugin-dev/commands/_archive/plugin-dev-guide.md +12 -0
- package/extensions/plugin-dev/commands/create-plugin.md +498 -0
- package/extensions/plugin-dev/commands/start.md +81 -0
- package/extensions/plugin-dev/hooks/hooks.json +3 -0
- package/extensions/plugin-dev/skills/agent-development/SKILL.md +641 -0
- package/extensions/plugin-dev/skills/agent-development/examples/agent-creation-prompt.md +250 -0
- package/extensions/plugin-dev/skills/agent-development/examples/complete-agent-examples.md +461 -0
- package/extensions/plugin-dev/skills/agent-development/references/advanced-agent-fields.md +246 -0
- package/extensions/plugin-dev/skills/agent-development/references/agent-creation-system-prompt.md +216 -0
- package/extensions/plugin-dev/skills/agent-development/references/permission-modes-rules.md +226 -0
- package/extensions/plugin-dev/skills/agent-development/references/system-prompt-design.md +464 -0
- package/extensions/plugin-dev/skills/agent-development/references/triggering-examples.md +474 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/create-agent-skeleton.sh +176 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/test-agent-trigger.sh +227 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/validate-agent.sh +227 -0
- package/extensions/plugin-dev/skills/command-development/SKILL.md +763 -0
- package/extensions/plugin-dev/skills/command-development/examples/plugin-commands.md +612 -0
- package/extensions/plugin-dev/skills/command-development/examples/simple-commands.md +527 -0
- package/extensions/plugin-dev/skills/command-development/references/advanced-workflows.md +762 -0
- package/extensions/plugin-dev/skills/command-development/references/documentation-patterns.md +769 -0
- package/extensions/plugin-dev/skills/command-development/references/frontmatter-reference.md +508 -0
- package/extensions/plugin-dev/skills/command-development/references/interactive-commands.md +966 -0
- package/extensions/plugin-dev/skills/command-development/references/marketplace-considerations.md +943 -0
- package/extensions/plugin-dev/skills/command-development/references/plugin-features-reference.md +637 -0
- package/extensions/plugin-dev/skills/command-development/references/plugin-integration.md +191 -0
- package/extensions/plugin-dev/skills/command-development/references/skill-tool.md +447 -0
- package/extensions/plugin-dev/skills/command-development/references/testing-strategies.md +723 -0
- package/extensions/plugin-dev/skills/command-development/scripts/check-frontmatter.sh +234 -0
- package/extensions/plugin-dev/skills/command-development/scripts/validate-command.sh +160 -0
- package/extensions/plugin-dev/skills/hook-development/SKILL.md +861 -0
- package/extensions/plugin-dev/skills/hook-development/examples/load-context.sh +55 -0
- package/extensions/plugin-dev/skills/hook-development/examples/validate-bash.sh +57 -0
- package/extensions/plugin-dev/skills/hook-development/examples/validate-write.sh +48 -0
- package/extensions/plugin-dev/skills/hook-development/references/advanced.md +871 -0
- package/extensions/plugin-dev/skills/hook-development/references/hook-input-schemas.md +145 -0
- package/extensions/plugin-dev/skills/hook-development/references/migration.md +392 -0
- package/extensions/plugin-dev/skills/hook-development/references/patterns.md +430 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/README.md +181 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/hook-linter.sh +153 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/test-hook.sh +276 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh +159 -0
- package/extensions/plugin-dev/skills/mcp-integration/SKILL.md +775 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/http-server.json +20 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/sse-server.json +19 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/stdio-server.json +38 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/ws-server.json +26 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/authentication.md +601 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/server-discovery.md +190 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/server-types.md +572 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/tool-usage.md +623 -0
- package/extensions/plugin-dev/skills/plugin-dev-guide/SKILL.md +222 -0
- package/extensions/plugin-dev/skills/plugin-structure/SKILL.md +705 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/advanced-plugin.md +774 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/minimal-plugin.md +83 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/standard-plugin.md +611 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/advanced-topics.md +289 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/component-patterns.md +592 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/github-actions.md +233 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/headless-ci-mode.md +193 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/manifest-reference.md +625 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/output-styles.md +116 -0
- package/extensions/plugin-dev/skills/skill-development/SKILL.md +564 -0
- package/extensions/plugin-dev/skills/skill-development/examples/complete-skill.md +465 -0
- package/extensions/plugin-dev/skills/skill-development/examples/frontmatter-templates.md +167 -0
- package/extensions/plugin-dev/skills/skill-development/examples/minimal-skill.md +111 -0
- package/extensions/plugin-dev/skills/skill-development/references/advanced-frontmatter.md +225 -0
- package/extensions/plugin-dev/skills/skill-development/references/commands-vs-skills.md +39 -0
- package/extensions/plugin-dev/skills/skill-development/references/skill-creation-workflow.md +379 -0
- package/extensions/plugin-dev/skills/skill-development/references/skill-creator-original.md +210 -0
- package/package.json +8 -11
- package/scripts/enact-extensions.mjs +823 -21
- package/scripts/hooks/session-start-drift-check.mjs +58 -0
- package/scripts/lib/build-index.mjs +50 -0
- package/scripts/lib/bundle-hash.mjs +137 -0
- package/scripts/lib/hooks.mjs +741 -0
- package/scripts/lib/ledger.mjs +163 -0
- package/scripts/lib/list-bundles.mjs +70 -0
- package/scripts/lib/outdated.mjs +144 -0
- package/scripts/lib/provision-mcp.mjs +16 -0
- package/scripts/lib/resolve-bundle.mjs +121 -0
- package/scripts/lib/run-install.mjs +402 -38
- package/scripts/lib/run-prune.mjs +73 -0
- package/scripts/lib/run-sync.mjs +9 -1
- package/scripts/lib/run-uninstall.mjs +244 -0
- package/scripts/lib/run-update.mjs +152 -0
- package/scripts/lib/run-validate.mjs +21 -18
- package/scripts/lib/serve.mjs +472 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/setup-enact-context.sh +2 -2
- package/scripts/version-bump.sh +463 -0
- package/spec/codex.json +1 -11
- package/spec/index.json +59 -0
- package/web/assets/README.md +111 -0
- package/web/assets/logo-full.png +0 -0
- package/web/assets/logo-slim.png +0 -0
- package/web/assets/tokens/base.css +45 -0
- package/web/assets/tokens/colors.css +248 -0
- package/web/assets/tokens/effects.css +24 -0
- package/web/assets/tokens/fonts.css +8 -0
- package/web/assets/tokens/index.css +18 -0
- package/web/assets/tokens/spacing.css +50 -0
- package/web/index.html +1188 -0
- package/.agents/plugins/marketplace.json +0 -20
- package/catalog/enact-context.json +0 -9
- package/catalog/enact-factory.json +0 -7
- package/catalog/enact-operator.json +0 -7
- package/catalog/enact-wiki.json +0 -7
- package/catalog/net-revenue-management.json +0 -8
- package/scripts/rename-supervisor-to-operator.pl +0 -66
- package/scripts/sync-manifests.mjs +0 -23
- package/scripts/validate-catalog.mjs +0 -37
- package/scripts/validate-plugin.mjs +0 -10
- /package/{plugins → extensions}/net-revenue-management/.mcp.json +0 -0
- /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-risks/SKILL.md +0 -0
- /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-scenario/SKILL.md +0 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serve.mjs — dependency-free localhost API + static-file server for enact-extensions.
|
|
3
|
+
*
|
|
4
|
+
* Security model:
|
|
5
|
+
* - Binds to 127.0.0.1 by default (localhost-only). Never 0.0.0.0 by default.
|
|
6
|
+
* An explicit --host flag may override, but the caller is responsible for the
|
|
7
|
+
* risk; the docs (and help text) note that binding to a non-loopback address
|
|
8
|
+
* exposes the API to other machines on the network.
|
|
9
|
+
* - Static files are restricted to web/assets/ via path normalisation. Any
|
|
10
|
+
* request that resolves outside that directory is rejected with 400.
|
|
11
|
+
* - User input (name/platform/scope) is validated against an allowlist and
|
|
12
|
+
* passed to JS library functions directly — never to a shell.
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* createServer(opts) → http.Server (does NOT auto-listen)
|
|
16
|
+
* startServer(opts) → Promise<{ server, url }> (listens, returns url string)
|
|
17
|
+
*
|
|
18
|
+
* Routes:
|
|
19
|
+
* GET / → web/index.html if present, else placeholder HTML
|
|
20
|
+
* GET /assets/* → web/assets/<path> (path-traversal guarded)
|
|
21
|
+
* GET /api/index → buildIndex() JSON
|
|
22
|
+
* GET /api/installed → { "<name>": [{platform,scope,home,path},...] }
|
|
23
|
+
* POST /api/install → { name, platform, scope } → runInstall
|
|
24
|
+
* POST /api/uninstall → { name, platform, scope } → runUninstall
|
|
25
|
+
* * unknown → 404
|
|
26
|
+
* * wrong method → 405 (for known routes with wrong method)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import http from "node:http";
|
|
30
|
+
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
|
|
31
|
+
import { dirname, join, normalize, resolve } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
|
|
34
|
+
import { buildIndex } from "./build-index.mjs";
|
|
35
|
+
import { findInstalled } from "./ledger.mjs";
|
|
36
|
+
import { listBundles } from "./list-bundles.mjs";
|
|
37
|
+
import { resolveBundlePath } from "./resolve-bundle.mjs";
|
|
38
|
+
import { runInstall } from "./run-install.mjs";
|
|
39
|
+
import { runUninstall } from "./run-uninstall.mjs";
|
|
40
|
+
|
|
41
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the package root (two levels up from scripts/lib/).
|
|
45
|
+
* Can be overridden via opts.packageRoot for tests.
|
|
46
|
+
*/
|
|
47
|
+
const DEFAULT_PACKAGE_ROOT = resolve(__dirname, "..", "..");
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Validation constants
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
const VALID_PLATFORMS = new Set(["codex", "claude", "cursor", "enact", "shared", "all"]);
|
|
54
|
+
const VALID_SCOPES = new Set(["global", "local"]);
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Content-type helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const MIME_TYPES = {
|
|
61
|
+
".html": "text/html; charset=utf-8",
|
|
62
|
+
".css": "text/css; charset=utf-8",
|
|
63
|
+
".js": "text/javascript; charset=utf-8",
|
|
64
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
65
|
+
".json": "application/json; charset=utf-8",
|
|
66
|
+
".png": "image/png",
|
|
67
|
+
".jpg": "image/jpeg",
|
|
68
|
+
".jpeg": "image/jpeg",
|
|
69
|
+
".gif": "image/gif",
|
|
70
|
+
".svg": "image/svg+xml",
|
|
71
|
+
".ico": "image/x-icon",
|
|
72
|
+
".woff": "font/woff",
|
|
73
|
+
".woff2": "font/woff2",
|
|
74
|
+
".ttf": "font/ttf",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function mimeForPath(filePath) {
|
|
78
|
+
const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
79
|
+
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Response helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
function sendJson(res, status, body) {
|
|
87
|
+
const json = JSON.stringify(body, null, 2);
|
|
88
|
+
res.writeHead(status, {
|
|
89
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
90
|
+
"Content-Length": Buffer.byteLength(json),
|
|
91
|
+
});
|
|
92
|
+
res.end(json);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sendHtml(res, status, html) {
|
|
96
|
+
res.writeHead(status, {
|
|
97
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
98
|
+
"Content-Length": Buffer.byteLength(html),
|
|
99
|
+
});
|
|
100
|
+
res.end(html);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sendError(res, status, message) {
|
|
104
|
+
sendJson(res, status, { ok: false, error: message });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Placeholder HTML (served when web/index.html is absent)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function placeholderHtml() {
|
|
112
|
+
return `<!DOCTYPE html>
|
|
113
|
+
<html lang="en">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="UTF-8" />
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
117
|
+
<title>enact-extensions</title>
|
|
118
|
+
<link rel="stylesheet" href="/assets/tokens/colors.css" />
|
|
119
|
+
<link rel="stylesheet" href="/assets/tokens/base.css" />
|
|
120
|
+
<style>
|
|
121
|
+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
|
|
122
|
+
pre { background: #f4f4f4; padding: 1rem; border-radius: 4px; overflow: auto; }
|
|
123
|
+
h1 { color: var(--adl-teal-600, #147f84); }
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<h1>enact-extensions</h1>
|
|
128
|
+
<p>The UI is loading. Discovery index:</p>
|
|
129
|
+
<pre id="index-output">Loading <a href="/api/index">/api/index</a>…</pre>
|
|
130
|
+
<script type="module">
|
|
131
|
+
const res = await fetch('/api/index');
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
document.getElementById('index-output').textContent = JSON.stringify(data, null, 2);
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Parse request body (JSON)
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
const MAX_BODY_BYTES = 64 * 1024; // 64 KiB — more than enough for name+platform+scope
|
|
144
|
+
|
|
145
|
+
function readJsonBody(req) {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const chunks = [];
|
|
148
|
+
let totalBytes = 0;
|
|
149
|
+
req.on("data", (c) => {
|
|
150
|
+
totalBytes += c.length;
|
|
151
|
+
if (totalBytes > MAX_BODY_BYTES) {
|
|
152
|
+
req.destroy();
|
|
153
|
+
reject(new Error("Request body too large"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
chunks.push(c);
|
|
157
|
+
});
|
|
158
|
+
req.on("end", () => {
|
|
159
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
160
|
+
if (!raw.trim()) {
|
|
161
|
+
resolve({});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
resolve(JSON.parse(raw));
|
|
166
|
+
} catch {
|
|
167
|
+
reject(new Error("Invalid JSON body"));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
req.on("error", reject);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Static asset handler — guard against path traversal
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
function serveAsset(_req, res, assetsRoot, urlPath) {
|
|
179
|
+
// Decode URI, then normalise to strip ../ sequences
|
|
180
|
+
let relPath;
|
|
181
|
+
try {
|
|
182
|
+
relPath = decodeURIComponent(urlPath.slice("/assets".length));
|
|
183
|
+
} catch {
|
|
184
|
+
return sendError(res, 400, "Malformed URL");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Normalise and ensure it stays within assetsRoot
|
|
188
|
+
const resolved = resolve(assetsRoot, "." + normalize(relPath));
|
|
189
|
+
if (!resolved.startsWith(assetsRoot + "/") && resolved !== assetsRoot) {
|
|
190
|
+
return sendError(res, 400, "Path traversal detected");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!existsSync(resolved)) {
|
|
194
|
+
return sendError(res, 404, `Asset not found: ${urlPath}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const mime = mimeForPath(resolved);
|
|
198
|
+
let stat;
|
|
199
|
+
try {
|
|
200
|
+
stat = statSync(resolved);
|
|
201
|
+
} catch {
|
|
202
|
+
return sendError(res, 500, "Failed to stat asset");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
res.writeHead(200, {
|
|
206
|
+
"Content-Type": mime,
|
|
207
|
+
"Content-Length": stat.size,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const stream = createReadStream(resolved);
|
|
211
|
+
stream.pipe(res);
|
|
212
|
+
stream.on("error", () => {
|
|
213
|
+
if (!res.headersSent) {
|
|
214
|
+
sendError(res, 500, "Failed to read asset");
|
|
215
|
+
} else {
|
|
216
|
+
res.destroy();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// createServer — build the http.Server (does NOT start listening)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {object} opts
|
|
227
|
+
* @param {string} [opts.packageRoot] - Package root for locating web/ and extensions/.
|
|
228
|
+
* @param {object} [opts.installDefaults] - Option bag merged into every runInstall/runUninstall call.
|
|
229
|
+
* Use this in tests to redirect ledgerHome, sharedHome, etc. to temp dirs.
|
|
230
|
+
* @returns {http.Server}
|
|
231
|
+
*/
|
|
232
|
+
export function createServer(opts = {}) {
|
|
233
|
+
const packageRoot = opts.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
|
234
|
+
const installDefaults = opts.installDefaults ?? {};
|
|
235
|
+
|
|
236
|
+
const webRoot = join(packageRoot, "web");
|
|
237
|
+
const assetsRoot = join(webRoot, "assets");
|
|
238
|
+
const indexHtmlPath = join(webRoot, "index.html");
|
|
239
|
+
const extensionsRoot = join(packageRoot, "extensions");
|
|
240
|
+
|
|
241
|
+
const server = http.createServer(async (req, res) => {
|
|
242
|
+
const url = req.url ?? "/";
|
|
243
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
244
|
+
|
|
245
|
+
// ---- GET / ----------------------------------------------------------
|
|
246
|
+
if (url === "/" || url === "") {
|
|
247
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
248
|
+
return sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
249
|
+
}
|
|
250
|
+
if (existsSync(indexHtmlPath)) {
|
|
251
|
+
const html = readFileSync(indexHtmlPath, "utf8");
|
|
252
|
+
return sendHtml(res, 200, html);
|
|
253
|
+
}
|
|
254
|
+
return sendHtml(res, 200, placeholderHtml());
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---- GET /assets/* --------------------------------------------------
|
|
258
|
+
if (url.startsWith("/assets/") || url === "/assets") {
|
|
259
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
260
|
+
return sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
261
|
+
}
|
|
262
|
+
return serveAsset(req, res, assetsRoot, url);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- GET /api/index -------------------------------------------------
|
|
266
|
+
if (url === "/api/index") {
|
|
267
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
268
|
+
return sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const index = buildIndex([extensionsRoot], {
|
|
272
|
+
now: new Date().toISOString(),
|
|
273
|
+
packageRoot,
|
|
274
|
+
});
|
|
275
|
+
return sendJson(res, 200, index);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return sendError(res, 500, err instanceof Error ? err.message : String(err));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---- GET /api/installed ---------------------------------------------
|
|
282
|
+
if (url === "/api/installed") {
|
|
283
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
284
|
+
return sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const bundles = listBundles([extensionsRoot]);
|
|
288
|
+
const ledgerOpts = installDefaults.ledgerHome
|
|
289
|
+
? { home: installDefaults.ledgerHome }
|
|
290
|
+
: {};
|
|
291
|
+
const result = {};
|
|
292
|
+
for (const bundle of bundles) {
|
|
293
|
+
const entries = findInstalled(bundle.name, ledgerOpts);
|
|
294
|
+
if (entries.length > 0) {
|
|
295
|
+
result[bundle.name] = entries;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return sendJson(res, 200, result);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
return sendError(res, 500, err instanceof Error ? err.message : String(err));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ---- POST /api/install ----------------------------------------------
|
|
305
|
+
if (url === "/api/install") {
|
|
306
|
+
if (method !== "POST") {
|
|
307
|
+
return sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
308
|
+
}
|
|
309
|
+
let body;
|
|
310
|
+
try {
|
|
311
|
+
body = await readJsonBody(req);
|
|
312
|
+
} catch {
|
|
313
|
+
return sendError(res, 400, "Invalid JSON body");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const { name, platform, scope, noProvision } = body;
|
|
317
|
+
|
|
318
|
+
// Validate inputs
|
|
319
|
+
if (!name || typeof name !== "string") {
|
|
320
|
+
return sendError(res, 400, "Missing required field: name");
|
|
321
|
+
}
|
|
322
|
+
if (platform && !VALID_PLATFORMS.has(platform)) {
|
|
323
|
+
return sendError(
|
|
324
|
+
res,
|
|
325
|
+
400,
|
|
326
|
+
`Unknown platform: "${platform}". Valid: codex, claude, cursor, enact, shared, all`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (scope && !VALID_SCOPES.has(scope)) {
|
|
330
|
+
return sendError(res, 400, `Unknown scope: "${scope}". Valid: global, local`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Resolve bundle path
|
|
334
|
+
let bundlePath;
|
|
335
|
+
try {
|
|
336
|
+
bundlePath = resolveBundlePath(name, { cwd: packageRoot });
|
|
337
|
+
} catch (err) {
|
|
338
|
+
return sendError(res, 400, err instanceof Error ? err.message : String(err));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Run install — MCP dependency provisioning is ON by default; the client
|
|
342
|
+
// may opt out with { noProvision: true }.
|
|
343
|
+
try {
|
|
344
|
+
const runOpts = {
|
|
345
|
+
platform: platform ?? "codex",
|
|
346
|
+
scope: scope ?? "global",
|
|
347
|
+
sync: false,
|
|
348
|
+
noProvision: noProvision === true,
|
|
349
|
+
...installDefaults,
|
|
350
|
+
};
|
|
351
|
+
const result = runInstall(bundlePath, runOpts);
|
|
352
|
+
// Surface the provision results so the UI can report them. The result's
|
|
353
|
+
// `provision` array is attached by runInstall (single + multi paths).
|
|
354
|
+
const provision = Array.isArray(result.provision) ? result.provision : [];
|
|
355
|
+
return sendJson(res, 200, { ok: true, result, provision });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
return sendError(res, 500, err instanceof Error ? err.message : String(err));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---- POST /api/uninstall --------------------------------------------
|
|
362
|
+
if (url === "/api/uninstall") {
|
|
363
|
+
if (method !== "POST") {
|
|
364
|
+
return sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
365
|
+
}
|
|
366
|
+
let body;
|
|
367
|
+
try {
|
|
368
|
+
body = await readJsonBody(req);
|
|
369
|
+
} catch {
|
|
370
|
+
return sendError(res, 400, "Invalid JSON body");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { name, platform, scope } = body;
|
|
374
|
+
|
|
375
|
+
// Validate inputs
|
|
376
|
+
if (!name || typeof name !== "string") {
|
|
377
|
+
return sendError(res, 400, "Missing required field: name");
|
|
378
|
+
}
|
|
379
|
+
if (platform && !VALID_PLATFORMS.has(platform)) {
|
|
380
|
+
return sendError(
|
|
381
|
+
res,
|
|
382
|
+
400,
|
|
383
|
+
`Unknown platform: "${platform}". Valid: codex, claude, cursor, enact, shared, all`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (scope && !VALID_SCOPES.has(scope)) {
|
|
387
|
+
return sendError(res, 400, `Unknown scope: "${scope}". Valid: global, local`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Resolve bundle path to get the canonical plugin name
|
|
391
|
+
// (We pass the name string directly to runUninstall since it accepts plugin names)
|
|
392
|
+
// First verify the plugin name is known
|
|
393
|
+
try {
|
|
394
|
+
resolveBundlePath(name, { cwd: packageRoot });
|
|
395
|
+
} catch (err) {
|
|
396
|
+
return sendError(res, 400, err instanceof Error ? err.message : String(err));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Run uninstall
|
|
400
|
+
try {
|
|
401
|
+
const runOpts = {
|
|
402
|
+
platform: platform ?? "codex",
|
|
403
|
+
scope: scope ?? "global",
|
|
404
|
+
sync: false,
|
|
405
|
+
...installDefaults,
|
|
406
|
+
};
|
|
407
|
+
const result = runUninstall(name, runOpts);
|
|
408
|
+
return sendJson(res, 200, { ok: true, result });
|
|
409
|
+
} catch (err) {
|
|
410
|
+
return sendError(res, 500, err instanceof Error ? err.message : String(err));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---- 404 for everything else ----------------------------------------
|
|
415
|
+
return sendError(res, 404, `Not found: ${url}`);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return server;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// startServer — creates + listens; returns { server, url }
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Start the server and return the URL.
|
|
427
|
+
*
|
|
428
|
+
* Security note: host defaults to '127.0.0.1' (loopback only). Passing any
|
|
429
|
+
* other value via --host exposes the API to other machines on the network —
|
|
430
|
+
* document this risk to operators.
|
|
431
|
+
*
|
|
432
|
+
* @param {object} opts
|
|
433
|
+
* @param {number} [opts.port=43217] - Port to listen on. Pass 0 for ephemeral.
|
|
434
|
+
* @param {string} [opts.host='127.0.0.1'] - Bind address. MUST be 127.0.0.1 by default.
|
|
435
|
+
* @param {string} [opts.packageRoot] - Package root override.
|
|
436
|
+
* @param {object} [opts.installDefaults] - Merged into runInstall/runUninstall calls.
|
|
437
|
+
* @returns {Promise<{ server: http.Server, url: string }>}
|
|
438
|
+
*/
|
|
439
|
+
// ENACT OS port-registry convention: dev listeners live in 43xxx, prod in 53xxx.
|
|
440
|
+
export const DEV_PORT = 43217;
|
|
441
|
+
export const PROD_PORT = 53217;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Resolve the serve port: an explicit `--port` always wins; otherwise `--prod`
|
|
445
|
+
* selects the prod-series port (53217) and the default is the dev-series port
|
|
446
|
+
* (43217). Pure (no I/O) so it is unit-testable without binding a socket.
|
|
447
|
+
* @param {object} [opts]
|
|
448
|
+
* @param {number} [opts.port] - Explicit port (wins; 0 = ephemeral).
|
|
449
|
+
* @param {boolean} [opts.prod] - Use the prod-series port (53217).
|
|
450
|
+
* @returns {number}
|
|
451
|
+
*/
|
|
452
|
+
export function resolveServePort(opts = {}) {
|
|
453
|
+
if (opts.port !== undefined && opts.port !== null) return opts.port;
|
|
454
|
+
return opts.prod ? PROD_PORT : DEV_PORT;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function startServer(opts = {}) {
|
|
458
|
+
const host = opts.host ?? "127.0.0.1";
|
|
459
|
+
const port = resolveServePort(opts);
|
|
460
|
+
|
|
461
|
+
const server = createServer(opts);
|
|
462
|
+
|
|
463
|
+
return new Promise((resolve, reject) => {
|
|
464
|
+
server.on("error", reject);
|
|
465
|
+
server.listen(port, host, () => {
|
|
466
|
+
const addr = server.address();
|
|
467
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
468
|
+
const url = `http://${host}:${actualPort}`;
|
|
469
|
+
resolve({ server, url });
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall.mjs — TTY-aware npm postinstall script for enact-extensions.
|
|
4
|
+
*
|
|
5
|
+
* CI-SAFETY CONTRACT (most important property):
|
|
6
|
+
* - If NOT a TTY → print one-line notice and exit 0. NEVER hang.
|
|
7
|
+
* - If CI env var is set → print one-line notice and exit 0.
|
|
8
|
+
* - If ENACT_EXTENSIONS_NO_POSTINSTALL is set → print one-line notice and exit 0.
|
|
9
|
+
* - If interactive TTY (and not CI) → run `hooks setup` interactive picker.
|
|
10
|
+
* - Entire script is wrapped in try/catch → ALWAYS process.exit(0).
|
|
11
|
+
* A postinstall that throws would break `npm ci` in CI.
|
|
12
|
+
*
|
|
13
|
+
* The CI/TTY guards ensure this script is fully safe in:
|
|
14
|
+
* - Docker CI environments (CI=1, no TTY)
|
|
15
|
+
* - npm ci / npm install in CI pipelines
|
|
16
|
+
* - Automated install tooling (pipes, scripts)
|
|
17
|
+
* - Developer machines with interactive TTY (triggers the interactive picker)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { stdin, env } = process;
|
|
22
|
+
|
|
23
|
+
const isCI = !!(env.CI || env.CONTINUOUS_INTEGRATION || env.BUILD_ID || env.GITHUB_ACTIONS);
|
|
24
|
+
const isNoPostinstall = !!(env.ENACT_EXTENSIONS_NO_POSTINSTALL);
|
|
25
|
+
const isTTY = !!(stdin.isTTY);
|
|
26
|
+
|
|
27
|
+
const NOTICE = `Run \`enact-extensions hooks setup\` to enable session-start drift checks.\n`;
|
|
28
|
+
|
|
29
|
+
if (!isTTY || isCI || isNoPostinstall) {
|
|
30
|
+
// Non-interactive or CI: print notice and exit immediately.
|
|
31
|
+
process.stdout.write(NOTICE);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Interactive TTY (non-CI): run the hooks setup interactive picker via CLI.
|
|
36
|
+
// We spawn a child process rather than importing directly to keep postinstall
|
|
37
|
+
// isolated from any import-time side-effects in the CLI modules.
|
|
38
|
+
const { spawnSync } = await import("node:child_process");
|
|
39
|
+
const { dirname, join, resolve } = await import("node:path");
|
|
40
|
+
const { fileURLToPath } = await import("node:url");
|
|
41
|
+
|
|
42
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const CLI = join(resolve(__dirname, ".."), "scripts", "enact-extensions.mjs");
|
|
44
|
+
|
|
45
|
+
// Spawn with stdio inherited so the interactive readline prompt is visible.
|
|
46
|
+
// We spawn and ignore the exit code — postinstall must always exit 0.
|
|
47
|
+
spawnSync(process.execPath, [CLI, "hooks", "setup"], {
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
env: { ...process.env },
|
|
50
|
+
});
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch {
|
|
53
|
+
// CRITICAL: never let an error propagate from postinstall.
|
|
54
|
+
// Printing to stderr is best-effort (safe to fail).
|
|
55
|
+
try {
|
|
56
|
+
process.stderr.write(
|
|
57
|
+
"[enact-extensions postinstall] Non-fatal error during hook setup. Run `enact-extensions hooks setup` manually.\n",
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
// Even stderr.write failed — just exit 0.
|
|
61
|
+
}
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Wire enact-context setup into the enact-extensions workspace.
|
|
3
|
-
# Validates the plugin bundle
|
|
3
|
+
# Validates the plugin-dev bundle then runs consolidated agent setup.
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
6
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
@@ -16,7 +16,7 @@ if ! command -v enact-context >/dev/null 2>&1; then
|
|
|
16
16
|
fi
|
|
17
17
|
|
|
18
18
|
cd "$ROOT"
|
|
19
|
-
npm run validate
|
|
19
|
+
npm run validate
|
|
20
20
|
|
|
21
21
|
echo ""
|
|
22
22
|
echo "Running enact-context setup (claude, codex, opencode, enact, zed)..."
|