@amsterdamdatalabs/enact-extensions 0.1.0 → 0.1.1
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 +94 -20
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +89 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +219 -18
- package/dist/install.js.map +1 -1
- package/dist/validate/index.d.ts +21 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +77 -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/{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 +751 -16
- 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 +389 -0
- package/scripts/lib/ledger.mjs +162 -0
- package/scripts/lib/list-bundles.mjs +70 -0
- package/scripts/lib/outdated.mjs +144 -0
- package/scripts/lib/provision-mcp.mjs +369 -0
- package/scripts/lib/resolve-bundle.mjs +121 -0
- package/scripts/lib/run-install.mjs +321 -39
- package/scripts/lib/run-uninstall.mjs +220 -0
- package/scripts/lib/run-update.mjs +152 -0
- package/scripts/lib/run-validate.mjs +12 -18
- package/scripts/lib/serve.mjs +454 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/setup-enact-context.sh +2 -2
- 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
|
@@ -1,40 +1,137 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolvePluginRoot } from "./lib/resolve-plugin-root.mjs";
|
|
3
|
+
import { resolveBundlePath } from "./lib/resolve-bundle.mjs";
|
|
4
|
+
import { listBundles } from "./lib/list-bundles.mjs";
|
|
5
|
+
import { buildIndex } from "./lib/build-index.mjs";
|
|
3
6
|
import { runValidate } from "./lib/run-validate.mjs";
|
|
4
7
|
import { runSync } from "./lib/run-sync.mjs";
|
|
5
8
|
import { runInstall } from "./lib/run-install.mjs";
|
|
9
|
+
import { runUpdate } from "./lib/run-update.mjs";
|
|
10
|
+
import { runUninstall } from "./lib/run-uninstall.mjs";
|
|
11
|
+
import { readLedger } from "./lib/ledger.mjs";
|
|
12
|
+
import { computeOutdated } from "./lib/outdated.mjs";
|
|
13
|
+
import { startServer } from "./lib/serve.mjs";
|
|
14
|
+
import { registerHook, removeHook, SUPPORTED_SURFACES } from "./lib/hooks.mjs";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { cwd as processCwd } from "node:process";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { Ajv } from "ajv";
|
|
21
|
+
import addFormatsModule from "ajv-formats";
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
24
|
|
|
7
25
|
const HELP = `enact-extensions — Enact multi-platform plugin manifests
|
|
8
26
|
|
|
9
27
|
Usage:
|
|
10
|
-
enact-extensions
|
|
11
|
-
enact-extensions
|
|
12
|
-
enact-extensions
|
|
13
|
-
enact-extensions
|
|
14
|
-
enact-extensions
|
|
15
|
-
enact-extensions
|
|
16
|
-
enact-extensions
|
|
17
|
-
enact-extensions
|
|
28
|
+
enact-extensions list [dir] List available plugin bundles (default: bundled + cwd/extensions/)
|
|
29
|
+
enact-extensions list [dir] --json Emit machine-readable JSON array
|
|
30
|
+
enact-extensions log Print the install ledger (single global file), newest first
|
|
31
|
+
enact-extensions log --json Emit the raw ledger array as JSON
|
|
32
|
+
enact-extensions outdated Report freshness drift: installed vs canonical source (per plugin+surface)
|
|
33
|
+
enact-extensions outdated --json Emit JSON array [{name, platform, scope, home, status, installedHash, currentHash, version}]
|
|
34
|
+
enact-extensions update <name> Refresh an outdated plugin on every surface it is installed on (ledger-driven)
|
|
35
|
+
enact-extensions update --all Refresh EVERY outdated plugin/surface
|
|
36
|
+
enact-extensions update <name> --dry-run Show what would be refreshed; change nothing
|
|
37
|
+
enact-extensions update --all --dry-run Show every outdated surface that would be refreshed
|
|
38
|
+
enact-extensions index [--out <path>] Generate a discovery index of all bundles (generated artifact, never committed)
|
|
39
|
+
enact-extensions index --out - Print the discovery index JSON to stdout
|
|
40
|
+
enact-extensions index --stdout Alias for --out -
|
|
41
|
+
enact-extensions validate [path|name] Validate manifests (default: cwd)
|
|
42
|
+
enact-extensions sync [path|name] Sync host manifests from .agents/plugin.json (default: cwd)
|
|
43
|
+
enact-extensions sync [path|name] --name <id> Create .agents/plugin.json then sync (new plugin)
|
|
44
|
+
enact-extensions install [path|name] Sync and install plugin into Codex-compatible homes (default: cwd)
|
|
45
|
+
enact-extensions install [path|name] --platform claude Install to Claude Code
|
|
46
|
+
enact-extensions install [path|name] --platform cursor Install to Cursor
|
|
47
|
+
enact-extensions install [path|name] --platform enact Install to Enact (Codex fork)
|
|
48
|
+
enact-extensions install [path|name] --platform codex Install to Codex (explicit)
|
|
49
|
+
enact-extensions install [path|name] --platform shared Install skills to host-neutral .agents/skills/
|
|
50
|
+
enact-extensions install [path|name] --platform all Install to every platform (codex, claude, cursor, enact, shared)
|
|
51
|
+
enact-extensions install [path|name] --platform claude,cursor Install to a subset of platforms
|
|
52
|
+
enact-extensions install [path|name] --global Install into the default agent home (default)
|
|
53
|
+
enact-extensions install [path|name] --local Install into a project home under cwd (./.codex, ...)
|
|
54
|
+
enact-extensions uninstall [path|name] Uninstall plugin from Codex-compatible homes (default: enact)
|
|
55
|
+
enact-extensions uninstall [path|name] --platform claude Uninstall from Claude Code
|
|
56
|
+
enact-extensions uninstall [path|name] --platform cursor Uninstall from Cursor
|
|
57
|
+
enact-extensions uninstall [path|name] --platform enact Uninstall from Enact (Codex fork)
|
|
58
|
+
enact-extensions uninstall [path|name] --platform codex Uninstall from Codex (explicit)
|
|
59
|
+
enact-extensions uninstall [path|name] --platform shared Remove skills from host-neutral .agents/skills/
|
|
60
|
+
enact-extensions uninstall [path|name] --platform all Uninstall from every platform
|
|
61
|
+
enact-extensions uninstall [path|name] --platform claude,cursor Uninstall from a subset of platforms
|
|
62
|
+
enact-extensions serve [--port 43217] [--host 127.0.0.1] [--open] Start the localhost management UI + API
|
|
63
|
+
enact-extensions hooks [setup] [--surfaces <list>|--all] [--remove] [--local]
|
|
64
|
+
Register (or remove) the session-start drift-check hook
|
|
65
|
+
for each named surface (claude, codex, cursor, enact).
|
|
66
|
+
|
|
67
|
+
Hooks command:
|
|
68
|
+
enact-extensions hooks setup --surfaces claude,codex Register the drift hook for Claude and Codex
|
|
69
|
+
enact-extensions hooks setup --all Register for all surfaces (claude, codex, cursor, enact)
|
|
70
|
+
enact-extensions hooks setup --surfaces claude --remove Remove the drift hook from Claude
|
|
71
|
+
enact-extensions hooks setup --all --remove Remove the drift hook from all surfaces
|
|
72
|
+
enact-extensions hooks setup --surfaces claude --local Register using project-local home (under cwd)
|
|
73
|
+
enact-extensions hooks setup Interactive surface picker (TTY only)
|
|
18
74
|
|
|
19
75
|
Options:
|
|
20
|
-
path
|
|
21
|
-
|
|
76
|
+
path|name Plugin root path, or a bare plugin name resolved from bundled extensions.
|
|
77
|
+
Defaults to process.cwd() when omitted.
|
|
78
|
+
--out <path> (index only) Output file path. Use - or --stdout to print to stdout.
|
|
79
|
+
Default: generated/index.json under the package root.
|
|
80
|
+
--stdout (index only) Alias for --out -.
|
|
81
|
+
--json (list only) Emit a JSON array to stdout instead of a human table.
|
|
82
|
+
--platform <name> Target platform: codex (default), claude, cursor, enact, shared.
|
|
83
|
+
Use "all" to install to every platform, or a comma-separated list
|
|
84
|
+
(e.g. "claude,cursor") to install to a named subset.
|
|
85
|
+
--global Install into the agent's default home (default)
|
|
86
|
+
--local Install into a project-scoped home under the current dir
|
|
22
87
|
--codex-home <path> Install only into the given Codex-compatible home
|
|
23
88
|
--enact-home <path> Target a specific local Enact home (default: ~/.enact)
|
|
24
89
|
--claude-home <path> Target a specific local Claude home (default: ~/.claude)
|
|
25
90
|
--cursor-home <path> Target a specific local Cursor home (default: ~/.cursor)
|
|
91
|
+
--shared-home <path> Base dir for shared install (skills land at <path>/.agents/skills/)
|
|
26
92
|
--marketplace <name> Marketplace name (default: enact-os-plugins)
|
|
27
93
|
--no-enable Install files without enabling the Codex/Enact plugin
|
|
28
94
|
--skip-sync Install current manifests without regenerating them
|
|
95
|
+
--no-provision Skip auto-provisioning the bundle's MCP-server packages.
|
|
96
|
+
By default, install resolves each declared MCP server
|
|
97
|
+
(.mcp.json) and provisions its package: uvx → uv tool
|
|
98
|
+
install, npx → npm install -g, pipx → pipx install,
|
|
99
|
+
pip/pip3 → pip install. Best-effort; never fails install.
|
|
100
|
+
|
|
101
|
+
Name resolution (bare plugin names):
|
|
102
|
+
When a bare name (no "/" in it) is given, it is resolved in this order:
|
|
103
|
+
1. <this-package-root>/extensions/<name> (bundled extensions in this repo)
|
|
104
|
+
2. <cwd>/extensions/<name> (extensions in a sibling product repo)
|
|
105
|
+
3. <npm-global-root>/@amsterdamdatalabs/enact-extensions/extensions/<name>
|
|
106
|
+
|
|
107
|
+
Multi-platform notes:
|
|
108
|
+
- Platforms are installed sequentially in deterministic order: codex, claude, cursor, enact, shared.
|
|
109
|
+
- Per-platform --<platform>-home flags apply to their respective platform in a multi-target run.
|
|
110
|
+
- If any platform fails, the others still proceed; a non-zero exit is returned and the summary
|
|
111
|
+
of succeeded/failed platforms is printed to stderr.
|
|
29
112
|
|
|
30
113
|
Examples:
|
|
114
|
+
enact-extensions list
|
|
115
|
+
enact-extensions list --json
|
|
116
|
+
enact-extensions list /path/to/extensions
|
|
117
|
+
enact-extensions index Generate generated/index.json (gitignored, local discovery artifact)
|
|
118
|
+
enact-extensions index --out - Print discovery index JSON to stdout (pipe-friendly)
|
|
119
|
+
enact-extensions index --stdout Same as --out -
|
|
120
|
+
enact-extensions index --out /tmp/my-index.json
|
|
31
121
|
cd ../enact-operator/extensions && enact-extensions validate
|
|
32
122
|
enact-extensions sync . --name my-plugin
|
|
33
|
-
enact-extensions install
|
|
123
|
+
enact-extensions install net-revenue-management --platform enact --global
|
|
124
|
+
enact-extensions install net-revenue-management --platform enact --local
|
|
125
|
+
enact-extensions install extensions/net-revenue-management --platform enact --global
|
|
34
126
|
enact-extensions install ../enact-operator/extensions --platform claude
|
|
35
127
|
enact-extensions install ../enact-operator/extensions --platform cursor
|
|
36
|
-
enact-extensions install
|
|
37
|
-
enact-extensions
|
|
128
|
+
enact-extensions install net-revenue-management --platform shared
|
|
129
|
+
enact-extensions install net-revenue-management --platform shared --local
|
|
130
|
+
enact-extensions install net-revenue-management --platform all
|
|
131
|
+
enact-extensions install net-revenue-management --platform claude,cursor
|
|
132
|
+
enact-extensions install net-revenue-management --platform all --enact-home /path/to/enact
|
|
133
|
+
enact-extensions validate extensions/plugin-dev
|
|
134
|
+
enact-extensions validate plugin-dev
|
|
38
135
|
`;
|
|
39
136
|
|
|
40
137
|
function parseArgs(argv) {
|
|
@@ -43,7 +140,11 @@ function parseArgs(argv) {
|
|
|
43
140
|
|
|
44
141
|
for (let i = 0; i < argv.length; i++) {
|
|
45
142
|
const arg = argv[i];
|
|
46
|
-
if (arg === "--
|
|
143
|
+
if (arg === "--out" && argv[i + 1]) {
|
|
144
|
+
options.out = argv[++i];
|
|
145
|
+
} else if (arg === "--stdout") {
|
|
146
|
+
options.out = "-";
|
|
147
|
+
} else if (arg === "--name" && argv[i + 1]) {
|
|
47
148
|
options.name = argv[++i];
|
|
48
149
|
} else if (arg === "--codex-home" && argv[i + 1]) {
|
|
49
150
|
options.codexHome = argv[++i];
|
|
@@ -53,6 +154,8 @@ function parseArgs(argv) {
|
|
|
53
154
|
options.enable = false;
|
|
54
155
|
} else if (arg === "--skip-sync") {
|
|
55
156
|
options.sync = false;
|
|
157
|
+
} else if (arg === "--no-provision") {
|
|
158
|
+
options.noProvision = true;
|
|
56
159
|
} else if (arg === "--platform" && argv[i + 1]) {
|
|
57
160
|
options.platform = argv[++i];
|
|
58
161
|
} else if (arg === "--claude-home" && argv[i + 1]) {
|
|
@@ -61,6 +164,29 @@ function parseArgs(argv) {
|
|
|
61
164
|
options.cursorHome = argv[++i];
|
|
62
165
|
} else if (arg === "--enact-home" && argv[i + 1]) {
|
|
63
166
|
options.enactHome = argv[++i];
|
|
167
|
+
} else if (arg === "--shared-home" && argv[i + 1]) {
|
|
168
|
+
options.sharedHome = argv[++i];
|
|
169
|
+
} else if (arg === "--local") {
|
|
170
|
+
options.scope = "local";
|
|
171
|
+
} else if (arg === "--global") {
|
|
172
|
+
options.scope = "global";
|
|
173
|
+
} else if (arg === "--all") {
|
|
174
|
+
options.all = true;
|
|
175
|
+
} else if (arg === "--dry-run") {
|
|
176
|
+
options.dryRun = true;
|
|
177
|
+
} else if (arg === "--json") {
|
|
178
|
+
options.json = true;
|
|
179
|
+
} else if (arg === "--port" && argv[i + 1]) {
|
|
180
|
+
options.port = parseInt(argv[++i], 10);
|
|
181
|
+
} else if (arg === "--host" && argv[i + 1]) {
|
|
182
|
+
options.host = argv[++i];
|
|
183
|
+
} else if (arg === "--open") {
|
|
184
|
+
options.open = true;
|
|
185
|
+
} else if (arg === "--surfaces" && argv[i + 1]) {
|
|
186
|
+
// Comma-separated list of surfaces: claude,codex,cursor,enact
|
|
187
|
+
options.surfaces = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
188
|
+
} else if (arg === "--remove") {
|
|
189
|
+
options.remove = true;
|
|
64
190
|
} else if (arg === "-h" || arg === "--help") {
|
|
65
191
|
positional.push("help");
|
|
66
192
|
} else if (!arg.startsWith("-")) {
|
|
@@ -78,7 +204,570 @@ if (!command || command === "help") {
|
|
|
78
204
|
process.exit(command ? 0 : 1);
|
|
79
205
|
}
|
|
80
206
|
|
|
81
|
-
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// `hooks` command — register or remove the session-start drift-check hook.
|
|
209
|
+
//
|
|
210
|
+
// Surfaces: claude, codex, cursor, enact
|
|
211
|
+
// Registration is idempotent and reversible.
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
if (command === "hooks") {
|
|
214
|
+
// Sub-command: `hooks setup` or just `hooks` (treat bare `hooks` as `hooks setup`).
|
|
215
|
+
// path is positional[1] — could be "setup" or undefined.
|
|
216
|
+
const subCommand = path ?? "setup";
|
|
217
|
+
|
|
218
|
+
if (subCommand !== "setup") {
|
|
219
|
+
process.stderr.write(`[enact-extensions hooks] Unknown sub-command: ${subCommand}. Did you mean 'setup'?\n`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const isRemove = options.remove ?? false;
|
|
224
|
+
const isLocal = options.scope === "local";
|
|
225
|
+
const cwd = processCwd();
|
|
226
|
+
|
|
227
|
+
// Determine which surfaces to act on.
|
|
228
|
+
let surfaces = [];
|
|
229
|
+
if (options.all) {
|
|
230
|
+
surfaces = [...SUPPORTED_SURFACES];
|
|
231
|
+
} else if (options.surfaces && options.surfaces.length > 0) {
|
|
232
|
+
// Validate provided surface names.
|
|
233
|
+
const invalid = options.surfaces.filter((s) => !SUPPORTED_SURFACES.includes(s));
|
|
234
|
+
if (invalid.length > 0) {
|
|
235
|
+
process.stderr.write(`[enact-extensions hooks] Unknown surface(s): ${invalid.join(", ")}. Valid: ${SUPPORTED_SURFACES.join(", ")}\n`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
surfaces = options.surfaces;
|
|
239
|
+
} else {
|
|
240
|
+
// No surfaces specified — check if TTY for interactive picker.
|
|
241
|
+
if (process.stdin.isTTY) {
|
|
242
|
+
// Interactive picker via node:readline.
|
|
243
|
+
const { createInterface } = await import("node:readline");
|
|
244
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
245
|
+
|
|
246
|
+
const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
|
|
247
|
+
|
|
248
|
+
process.stdout.write(`\nenact-extensions hooks setup — interactive surface picker\n`);
|
|
249
|
+
process.stdout.write(`Available surfaces: ${SUPPORTED_SURFACES.map((s, i) => `${i + 1}. ${s}`).join(", ")}\n`);
|
|
250
|
+
process.stdout.write(`Enter surface names or numbers (comma-separated), or 'all':\n`);
|
|
251
|
+
|
|
252
|
+
const answer = await question("> ");
|
|
253
|
+
rl.close();
|
|
254
|
+
|
|
255
|
+
if (answer.trim().toLowerCase() === "all") {
|
|
256
|
+
surfaces = [...SUPPORTED_SURFACES];
|
|
257
|
+
} else {
|
|
258
|
+
surfaces = answer
|
|
259
|
+
.split(",")
|
|
260
|
+
.map((s) => s.trim())
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
.map((s) => {
|
|
263
|
+
// Allow numeric selection.
|
|
264
|
+
const idx = parseInt(s, 10);
|
|
265
|
+
if (!isNaN(idx) && idx >= 1 && idx <= SUPPORTED_SURFACES.length) {
|
|
266
|
+
return SUPPORTED_SURFACES[idx - 1];
|
|
267
|
+
}
|
|
268
|
+
return s.toLowerCase();
|
|
269
|
+
})
|
|
270
|
+
.filter((s) => SUPPORTED_SURFACES.includes(s));
|
|
271
|
+
|
|
272
|
+
if (surfaces.length === 0) {
|
|
273
|
+
process.stderr.write(`[enact-extensions hooks] No valid surfaces selected. Exiting.\n`);
|
|
274
|
+
process.exit(0);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// Non-TTY, no surface flags → print notice and exit 0.
|
|
279
|
+
process.stdout.write(
|
|
280
|
+
`[enact-extensions hooks] No surface specified. Run with --surfaces or --all:\n` +
|
|
281
|
+
` enact-extensions hooks setup --surfaces claude,codex\n` +
|
|
282
|
+
` enact-extensions hooks setup --all\n`,
|
|
283
|
+
);
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Execute registration or removal for each surface.
|
|
289
|
+
const hookOpts = {
|
|
290
|
+
claudeHome: options.claudeHome,
|
|
291
|
+
codexHome: options.codexHome,
|
|
292
|
+
cursorHome: options.cursorHome,
|
|
293
|
+
enactHome: options.enactHome,
|
|
294
|
+
local: isLocal,
|
|
295
|
+
cwd,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
for (const surface of surfaces) {
|
|
299
|
+
let res;
|
|
300
|
+
if (isRemove) {
|
|
301
|
+
res = removeHook(surface, hookOpts);
|
|
302
|
+
} else {
|
|
303
|
+
res = registerHook(surface, hookOpts);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Print per-surface result.
|
|
307
|
+
const status = res.result;
|
|
308
|
+
const location = res.location ? ` → ${res.location}` : "";
|
|
309
|
+
const note = res.note ? ` (${res.note})` : "";
|
|
310
|
+
|
|
311
|
+
if (status === "registered") {
|
|
312
|
+
process.stdout.write(` [hooks] ${surface}: registered${location}\n`);
|
|
313
|
+
} else if (status === "already_registered") {
|
|
314
|
+
process.stdout.write(` [hooks] ${surface}: already registered (skipped)${location}\n`);
|
|
315
|
+
} else if (status === "removed") {
|
|
316
|
+
process.stdout.write(` [hooks] ${surface}: removed${location}\n`);
|
|
317
|
+
} else if (status === "not_found") {
|
|
318
|
+
process.stdout.write(` [hooks] ${surface}: not found (nothing to remove)${location}\n`);
|
|
319
|
+
} else {
|
|
320
|
+
process.stdout.write(` [hooks] ${surface}: skipped${note}\n`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (res.note && status !== "skipped") {
|
|
324
|
+
process.stderr.write(` [hooks] ${surface} note: ${res.note}\n`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// `list` command — enumerate available plugin bundles.
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
if (command === "list") {
|
|
335
|
+
// The package's own extensions/ dir, resolved relative to this script.
|
|
336
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
337
|
+
const packageExtensions = join(PACKAGE_ROOT, "extensions");
|
|
338
|
+
|
|
339
|
+
let roots;
|
|
340
|
+
if (path) {
|
|
341
|
+
// Explicit dir argument → scan only that dir.
|
|
342
|
+
roots = [resolve(processCwd(), path)];
|
|
343
|
+
} else {
|
|
344
|
+
// Default: package-bundled extensions/ + cwd/extensions/ (if it differs).
|
|
345
|
+
roots = [packageExtensions];
|
|
346
|
+
const cwdExtensions = join(processCwd(), "extensions");
|
|
347
|
+
if (resolve(cwdExtensions) !== resolve(packageExtensions) && existsSync(cwdExtensions)) {
|
|
348
|
+
roots.push(cwdExtensions);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const entries = listBundles(roots);
|
|
353
|
+
|
|
354
|
+
if (options.json) {
|
|
355
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
356
|
+
process.exit(0);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Human-readable table.
|
|
360
|
+
const TRUNC = 60;
|
|
361
|
+
function trunc(str, len) {
|
|
362
|
+
if (!str) return "";
|
|
363
|
+
return str.length > len ? str.slice(0, len - 1) + "…" : str;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (entries.length === 0) {
|
|
367
|
+
console.log("No plugin bundles found.");
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Header.
|
|
372
|
+
const COL_NAME = 28;
|
|
373
|
+
const COL_VER = 8;
|
|
374
|
+
const COL_CAT = 18;
|
|
375
|
+
const COL_SURF = 22;
|
|
376
|
+
|
|
377
|
+
function padR(str, n) { return String(str ?? "").padEnd(n).slice(0, n); }
|
|
378
|
+
|
|
379
|
+
const hr = "─".repeat(COL_NAME + COL_VER + COL_CAT + COL_SURF + 3 + TRUNC);
|
|
380
|
+
console.log(hr);
|
|
381
|
+
console.log(
|
|
382
|
+
padR("Name", COL_NAME) + " " +
|
|
383
|
+
padR("Ver", COL_VER) + " " +
|
|
384
|
+
padR("Category", COL_CAT) + " " +
|
|
385
|
+
padR("Surfaces", COL_SURF) + " " +
|
|
386
|
+
"Description"
|
|
387
|
+
);
|
|
388
|
+
console.log(hr);
|
|
389
|
+
|
|
390
|
+
for (const e of entries) {
|
|
391
|
+
const surfaces = e.targets.join(", ");
|
|
392
|
+
console.log(
|
|
393
|
+
padR(e.name, COL_NAME) + " " +
|
|
394
|
+
padR(e.version, COL_VER) + " " +
|
|
395
|
+
padR(e.category, COL_CAT) + " " +
|
|
396
|
+
padR(surfaces, COL_SURF) + " " +
|
|
397
|
+
trunc(e.description, TRUNC)
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
console.log(hr);
|
|
401
|
+
console.log(`${entries.length} bundle${entries.length === 1 ? "" : "s"} found.`);
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// `index` command — generate a discovery index of all plugin bundles.
|
|
407
|
+
//
|
|
408
|
+
// The index is a LOCAL GENERATED ARTIFACT (never committed). The generated/
|
|
409
|
+
// directory is gitignored. The schema at spec/index.json is the committed
|
|
410
|
+
// contract. No network, no marketplace, no catalog/ — purely local.
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
if (command === "index") {
|
|
413
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
414
|
+
const packageExtensions = join(PACKAGE_ROOT, "extensions");
|
|
415
|
+
|
|
416
|
+
// Same default roots as `list`.
|
|
417
|
+
const roots = [packageExtensions];
|
|
418
|
+
const cwdExtensions = join(processCwd(), "extensions");
|
|
419
|
+
if (resolve(cwdExtensions) !== resolve(packageExtensions) && existsSync(cwdExtensions)) {
|
|
420
|
+
roots.push(cwdExtensions);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Build the index. The wall-clock timestamp is computed here at the CLI
|
|
424
|
+
// boundary and passed in — buildIndex itself stays pure (never calls Date).
|
|
425
|
+
let index;
|
|
426
|
+
try {
|
|
427
|
+
index = buildIndex(roots, { now: new Date().toISOString(), packageRoot: PACKAGE_ROOT });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
console.error(`[enact-extensions index] Failed to build index: ${err instanceof Error ? err.message : String(err)}`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Validate against spec/index.json before writing.
|
|
434
|
+
const schemaPath = join(PACKAGE_ROOT, "spec", "index.json");
|
|
435
|
+
let schema;
|
|
436
|
+
try {
|
|
437
|
+
schema = JSON.parse(readFileSync(schemaPath, "utf8"));
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error(`[enact-extensions index] Cannot read schema ${schemaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const addFormats = addFormatsModule.default ?? addFormatsModule;
|
|
444
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
445
|
+
addFormats(ajv);
|
|
446
|
+
const validate = ajv.compile(schema);
|
|
447
|
+
const valid = validate(index);
|
|
448
|
+
if (!valid) {
|
|
449
|
+
const errs = (validate.errors ?? []).map((e) => ` ${e.instancePath || "/"}: ${e.message}`).join("\n");
|
|
450
|
+
console.error(`[enact-extensions index] Generated index failed schema validation:\n${errs}`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const json = JSON.stringify(index, null, 2);
|
|
455
|
+
// --stdout is normalized to "-" during arg parsing, so this single check covers both.
|
|
456
|
+
const toStdout = options.out === "-";
|
|
457
|
+
|
|
458
|
+
if (toStdout) {
|
|
459
|
+
// Write JSON to stdout; summary to stderr (keeps stdout pipe-clean).
|
|
460
|
+
process.stdout.write(json + "\n");
|
|
461
|
+
process.stderr.write(`[enact-extensions index] ${index.count} plugin${index.count === 1 ? "" : "s"} indexed.\n`);
|
|
462
|
+
} else {
|
|
463
|
+
const outPath = options.out
|
|
464
|
+
? resolve(processCwd(), options.out)
|
|
465
|
+
: join(PACKAGE_ROOT, "generated", "index.json");
|
|
466
|
+
|
|
467
|
+
// Ensure parent directory exists.
|
|
468
|
+
const outDir = resolve(outPath, "..");
|
|
469
|
+
mkdirSync(outDir, { recursive: true });
|
|
470
|
+
|
|
471
|
+
writeFileSync(outPath, json + "\n", "utf8");
|
|
472
|
+
process.stderr.write(`[enact-extensions index] Wrote ${index.count} plugin${index.count === 1 ? "" : "s"} to ${outPath}\n`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// `log` command — print the install ledger (single global file), newest first.
|
|
480
|
+
//
|
|
481
|
+
// Human table to stdout (chrome to stderr) or `--json` (raw ledger array).
|
|
482
|
+
// Reading a missing ledger yields an empty list — never an error.
|
|
483
|
+
// ---------------------------------------------------------------------------
|
|
484
|
+
if (command === "log") {
|
|
485
|
+
const entries = readLedger();
|
|
486
|
+
// Most-recent-first.
|
|
487
|
+
const sorted = [...entries].sort((a, b) =>
|
|
488
|
+
String(b.ts ?? "").localeCompare(String(a.ts ?? "")),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
if (options.json) {
|
|
492
|
+
process.stdout.write(JSON.stringify(sorted, null, 2) + "\n");
|
|
493
|
+
process.exit(0);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (sorted.length === 0) {
|
|
497
|
+
process.stderr.write("No install ledger entries found.\n");
|
|
498
|
+
process.exit(0);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const COL_TS = 24;
|
|
502
|
+
const COL_ACT = 10;
|
|
503
|
+
const COL_NAME = 28;
|
|
504
|
+
const COL_PLAT = 8;
|
|
505
|
+
const COL_SCOPE = 7;
|
|
506
|
+
function padR(str, n) { return String(str ?? "").padEnd(n).slice(0, n); }
|
|
507
|
+
|
|
508
|
+
const hr = "─".repeat(COL_TS + COL_ACT + COL_NAME + COL_PLAT + COL_SCOPE + 4 + 40);
|
|
509
|
+
console.log(hr);
|
|
510
|
+
console.log(
|
|
511
|
+
padR("Timestamp", COL_TS) + " " +
|
|
512
|
+
padR("Action", COL_ACT) + " " +
|
|
513
|
+
padR("Name", COL_NAME) + " " +
|
|
514
|
+
padR("Platform", COL_PLAT) + " " +
|
|
515
|
+
padR("Scope", COL_SCOPE) + " " +
|
|
516
|
+
"Home",
|
|
517
|
+
);
|
|
518
|
+
console.log(hr);
|
|
519
|
+
for (const e of sorted) {
|
|
520
|
+
console.log(
|
|
521
|
+
padR(e.ts, COL_TS) + " " +
|
|
522
|
+
padR(e.action, COL_ACT) + " " +
|
|
523
|
+
padR(e.name, COL_NAME) + " " +
|
|
524
|
+
padR(e.platform, COL_PLAT) + " " +
|
|
525
|
+
padR(e.scope, COL_SCOPE) + " " +
|
|
526
|
+
String(e.home ?? ""),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
console.log(hr);
|
|
530
|
+
process.stderr.write(`${sorted.length} ledger entr${sorted.length === 1 ? "y" : "ies"}.\n`);
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// `outdated` command — report drift between installed plugins and canonical source.
|
|
536
|
+
//
|
|
537
|
+
// Classifies each installed (plugin, platform) surface as:
|
|
538
|
+
// fresh → installed hash matches canonical source hash
|
|
539
|
+
// outdated → hash mismatch (or recorded hash missing), needs re-install
|
|
540
|
+
// orphaned → canonical source does not exist (can't refresh)
|
|
541
|
+
//
|
|
542
|
+
// Exit code: always 0 (it's a report, not a failure gate).
|
|
543
|
+
// Human chrome → stderr; --json stdout is always pure JSON.
|
|
544
|
+
// ---------------------------------------------------------------------------
|
|
545
|
+
if (command === "outdated") {
|
|
546
|
+
let entries;
|
|
547
|
+
try {
|
|
548
|
+
entries = computeOutdated({ cwd: processCwd() });
|
|
549
|
+
} catch (err) {
|
|
550
|
+
process.stderr.write(
|
|
551
|
+
`[enact-extensions outdated] Error: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
552
|
+
);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (options.json) {
|
|
557
|
+
process.stdout.write(JSON.stringify(entries, null, 2) + "\n");
|
|
558
|
+
process.exit(0);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Human-readable table.
|
|
562
|
+
if (entries.length === 0) {
|
|
563
|
+
process.stderr.write("No installed plugins found in the ledger.\n");
|
|
564
|
+
process.exit(0);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const COL_NAME = 30;
|
|
568
|
+
const COL_PLAT = 10;
|
|
569
|
+
const COL_SCOPE = 8;
|
|
570
|
+
const COL_VER = 10;
|
|
571
|
+
const COL_STAT = 10;
|
|
572
|
+
|
|
573
|
+
function padR(str, n) { return String(str ?? "").padEnd(n).slice(0, n); }
|
|
574
|
+
|
|
575
|
+
const hr = "─".repeat(COL_NAME + COL_PLAT + COL_SCOPE + COL_VER + COL_STAT + 4 + 8);
|
|
576
|
+
console.log(hr);
|
|
577
|
+
console.log(
|
|
578
|
+
padR("Plugin", COL_NAME) + " " +
|
|
579
|
+
padR("Platform", COL_PLAT) + " " +
|
|
580
|
+
padR("Scope", COL_SCOPE) + " " +
|
|
581
|
+
padR("Version", COL_VER) + " " +
|
|
582
|
+
padR("Status", COL_STAT),
|
|
583
|
+
);
|
|
584
|
+
console.log(hr);
|
|
585
|
+
|
|
586
|
+
for (const e of entries) {
|
|
587
|
+
console.log(
|
|
588
|
+
padR(e.name, COL_NAME) + " " +
|
|
589
|
+
padR(e.platform, COL_PLAT) + " " +
|
|
590
|
+
padR(e.scope, COL_SCOPE) + " " +
|
|
591
|
+
padR(e.version, COL_VER) + " " +
|
|
592
|
+
padR(e.status, COL_STAT),
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
console.log(hr);
|
|
596
|
+
process.stderr.write(`${entries.length} surface${entries.length === 1 ? "" : "s"} checked.\n`);
|
|
597
|
+
process.exit(0);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
// `update` command — refresh outdated installed plugins, driven by the ledger.
|
|
602
|
+
//
|
|
603
|
+
// Re-installs each OUTDATED (plugin, surface, home) to the EXACT surface + home
|
|
604
|
+
// recorded in the ledger — never to a surface the plugin isn't already on.
|
|
605
|
+
//
|
|
606
|
+
// update <name> → refresh only that plugin (across its installed surfaces)
|
|
607
|
+
// update --all → refresh every outdated surface
|
|
608
|
+
// update --dry-run→ report what WOULD change; touch nothing
|
|
609
|
+
//
|
|
610
|
+
// Exit code: non-zero ONLY on a real install failure or a missing target (no
|
|
611
|
+
// name + no --all). Orphaned and fresh surfaces are never failures.
|
|
612
|
+
// Human chrome → stderr; the ledger self-updates via runInstall.
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
if (command === "update") {
|
|
615
|
+
let summary;
|
|
616
|
+
try {
|
|
617
|
+
summary = runUpdate({
|
|
618
|
+
name: path, // positional[1]: a bare plugin name (or undefined)
|
|
619
|
+
all: options.all,
|
|
620
|
+
dryRun: options.dryRun,
|
|
621
|
+
home: homedir(), // single global ledger home
|
|
622
|
+
cwd: processCwd(), // canonical bundle resolution
|
|
623
|
+
});
|
|
624
|
+
} catch (err) {
|
|
625
|
+
process.stderr.write(
|
|
626
|
+
`[enact-extensions update] ${err instanceof Error ? err.message : String(err)}\n`,
|
|
627
|
+
);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const verb = summary.dryRun ? "would update" : "updated";
|
|
632
|
+
|
|
633
|
+
if (summary.dryRun) {
|
|
634
|
+
if (summary.planned.length === 0) {
|
|
635
|
+
process.stderr.write("Nothing to update — no outdated surfaces in scope.\n");
|
|
636
|
+
}
|
|
637
|
+
for (const e of summary.planned) {
|
|
638
|
+
const from = e.installedHash ? String(e.installedHash).slice(0, 12) : "(none)";
|
|
639
|
+
const to = e.currentHash ? String(e.currentHash).slice(0, 12) : "(unknown)";
|
|
640
|
+
console.log(`${verb}: ${e.name} [${e.platform}] @ ${e.home} ${from} -> ${to}`);
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
for (const e of summary.updated) {
|
|
644
|
+
console.log(`updated: ${e.name} [${e.platform}] @ ${e.home}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
for (const e of summary.skipped) {
|
|
649
|
+
process.stderr.write(
|
|
650
|
+
`skipped-orphaned: ${e.name} [${e.platform}] @ ${e.home} (canonical source gone)\n`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
for (const f of summary.failed) {
|
|
654
|
+
const e = f.entry;
|
|
655
|
+
process.stderr.write(
|
|
656
|
+
`failed: ${e.name} [${e.platform}] @ ${e.home}: ${f.error.message}\n`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const doneCount = summary.dryRun ? summary.planned.length : summary.updated.length;
|
|
661
|
+
process.stderr.write(
|
|
662
|
+
`${doneCount} ${summary.dryRun ? "would be updated" : "updated"}, ` +
|
|
663
|
+
`${summary.skipped.length} skipped (orphaned), ${summary.failed.length} failed.\n`,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// Non-zero exit ONLY on a real install failure.
|
|
667
|
+
process.exit(summary.failed.length > 0 ? 1 : 0);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// `serve` command — localhost API + static-file server (no pluginRoot needed).
|
|
672
|
+
// Handled here (before resolveRoot) because it is async + long-running.
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
if (command === "serve") {
|
|
675
|
+
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
676
|
+
const port = options.port ?? 43217;
|
|
677
|
+
const host = options.host ?? "127.0.0.1";
|
|
678
|
+
|
|
679
|
+
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
|
|
680
|
+
process.stderr.write(
|
|
681
|
+
`[enact-extensions serve] WARNING: binding to ${host} exposes the API to other machines on the network.\n`,
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Use async IIFE. The http.Server keeps the event loop alive after this IIFE resolves.
|
|
686
|
+
(async () => {
|
|
687
|
+
let serverInstance;
|
|
688
|
+
let url;
|
|
689
|
+
try {
|
|
690
|
+
const started = await startServer({
|
|
691
|
+
port,
|
|
692
|
+
host,
|
|
693
|
+
packageRoot: PACKAGE_ROOT,
|
|
694
|
+
installDefaults: { ledgerHome: homedir() },
|
|
695
|
+
});
|
|
696
|
+
serverInstance = started.server;
|
|
697
|
+
url = started.url;
|
|
698
|
+
} catch (err) {
|
|
699
|
+
process.stderr.write(`[enact-extensions serve] Failed to start server: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
console.log(`enact-extensions serve: listening at ${url}`);
|
|
704
|
+
|
|
705
|
+
if (options.open) {
|
|
706
|
+
// Best-effort browser open; ignore all errors silently.
|
|
707
|
+
const { spawn } = await import("node:child_process");
|
|
708
|
+
const opener =
|
|
709
|
+
process.platform === "darwin"
|
|
710
|
+
? "open"
|
|
711
|
+
: process.platform === "win32"
|
|
712
|
+
? "start"
|
|
713
|
+
: "xdg-open";
|
|
714
|
+
try {
|
|
715
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
716
|
+
} catch {
|
|
717
|
+
// xdg-open / open not available in all environments — ignore
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// SIGINT / SIGTERM → graceful shutdown
|
|
722
|
+
const shutdown = () => serverInstance.close(() => process.exit(0));
|
|
723
|
+
process.on("SIGINT", shutdown);
|
|
724
|
+
process.on("SIGTERM", shutdown);
|
|
725
|
+
})();
|
|
726
|
+
|
|
727
|
+
// Do NOT fall through to resolveRoot / the plugin-root command block.
|
|
728
|
+
// process.exit is intentionally NOT called — the http.Server ref keeps Node alive.
|
|
729
|
+
// Use a throw inside a never-reached block OR just let the module end here.
|
|
730
|
+
// The trick: throw a synthetic signal so the try/catch-less module scope terminates.
|
|
731
|
+
// Actually: we use process.exitCode = 0 and allow the event loop to keep running.
|
|
732
|
+
process.exitCode = 0; // signal intent; overridden to 1 on error inside IIFE
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
// Commands that require resolving a plugin root path.
|
|
737
|
+
// (Only run when command !== "serve" — serve handled above and must not fall through.)
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
if (command !== "serve") {
|
|
740
|
+
|
|
741
|
+
// Resolve the plugin root:
|
|
742
|
+
// - If no path arg given → default to cwd (existing behaviour).
|
|
743
|
+
// - If a path arg is given → resolveBundlePath handles both explicit paths and
|
|
744
|
+
// bare plugin names, throwing meaningful errors when nothing resolves.
|
|
745
|
+
const resolveRoot = (pathArg) => {
|
|
746
|
+
if (!pathArg) return resolvePluginRoot(undefined);
|
|
747
|
+
return resolveBundlePath(pathArg);
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// A bare name has no path separators (so it is not an explicit filesystem path).
|
|
751
|
+
const isBareName = (arg) =>
|
|
752
|
+
typeof arg === "string" && arg.length > 0 && !arg.includes("/") && !arg.includes("\\");
|
|
753
|
+
|
|
754
|
+
let pluginRoot = null;
|
|
755
|
+
try {
|
|
756
|
+
pluginRoot = resolveRoot(path);
|
|
757
|
+
} catch (err) {
|
|
758
|
+
// UNINSTALL-ONLY FALLBACK: uninstall must work even when the source bundle is
|
|
759
|
+
// gone. If the arg is a BARE NAME (not an explicit path) that did not resolve
|
|
760
|
+
// to a bundle, fall back to using the arg verbatim as the plugin name. The
|
|
761
|
+
// uninstall handler reads the name from `path` when pluginRoot is null.
|
|
762
|
+
// For an explicit path that does not exist, or for any other command, this is
|
|
763
|
+
// still a hard error.
|
|
764
|
+
if (command === "uninstall" && isBareName(path)) {
|
|
765
|
+
pluginRoot = null; // signal: use the raw `path` as the plugin name
|
|
766
|
+
} else {
|
|
767
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
82
771
|
|
|
83
772
|
try {
|
|
84
773
|
if (command === "validate") {
|
|
@@ -92,7 +781,51 @@ try {
|
|
|
92
781
|
}
|
|
93
782
|
|
|
94
783
|
if (command === "install") {
|
|
95
|
-
|
|
784
|
+
// Single global ledger: every install (local or global scope) records to
|
|
785
|
+
// <homedir>/.enact/extensions/ledger.jsonl so `log` always finds it,
|
|
786
|
+
// regardless of which per-platform home the bundle was installed to. The
|
|
787
|
+
// local/global distinction is carried in each entry's `scope` field.
|
|
788
|
+
runInstall(pluginRoot, { ...options, ledgerHome: homedir() });
|
|
789
|
+
process.exit(0);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (command === "uninstall") {
|
|
793
|
+
// Resolve plugin name: if the source bundle resolved on disk, read its
|
|
794
|
+
// manifest name from .agents/plugin.json (or .codex-plugin/plugin.json).
|
|
795
|
+
// Otherwise (source bundle gone / bare-name fallback), treat the raw
|
|
796
|
+
// positional arg as the plugin name. This lets uninstall remove already-
|
|
797
|
+
// installed plugins even when their source bundle has been deleted.
|
|
798
|
+
let pluginName = null;
|
|
799
|
+
if (pluginRoot) {
|
|
800
|
+
try {
|
|
801
|
+
const enactManifestPath = join(pluginRoot, ".agents", "plugin.json");
|
|
802
|
+
if (existsSync(enactManifestPath)) {
|
|
803
|
+
const manifest = JSON.parse(readFileSync(enactManifestPath, "utf8"));
|
|
804
|
+
pluginName = typeof manifest.name === "string" ? manifest.name : null;
|
|
805
|
+
}
|
|
806
|
+
// Fall back to .codex-plugin/plugin.json
|
|
807
|
+
if (!pluginName) {
|
|
808
|
+
const codexManifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
|
|
809
|
+
if (existsSync(codexManifestPath)) {
|
|
810
|
+
const manifest = JSON.parse(readFileSync(codexManifestPath, "utf8"));
|
|
811
|
+
pluginName = typeof manifest.name === "string" ? manifest.name : null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
} catch {
|
|
815
|
+
// Source bundle unreadable — fall through to raw name.
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// Fall back: treat the raw positional arg as the plugin name.
|
|
819
|
+
// `path` is the raw positional from the CLI. When the source bundle was gone
|
|
820
|
+
// (pluginRoot === null) this is the only source of the name.
|
|
821
|
+
if (!pluginName) {
|
|
822
|
+
pluginName = path ?? (pluginRoot ? pluginRoot.split("/").pop() : null);
|
|
823
|
+
}
|
|
824
|
+
if (!pluginName) {
|
|
825
|
+
console.error("uninstall requires a plugin name or path.");
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
runUninstall(pluginName, { ...options, ledgerHome: homedir() });
|
|
96
829
|
process.exit(0);
|
|
97
830
|
}
|
|
98
831
|
|
|
@@ -103,3 +836,5 @@ try {
|
|
|
103
836
|
console.error(err instanceof Error ? err.message : String(err));
|
|
104
837
|
process.exit(1);
|
|
105
838
|
}
|
|
839
|
+
|
|
840
|
+
} // end if (command !== "serve")
|