@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.
Files changed (152) hide show
  1. package/README.md +94 -20
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/install.d.ts +89 -0
  7. package/dist/install.d.ts.map +1 -1
  8. package/dist/install.js +219 -18
  9. package/dist/install.js.map +1 -1
  10. package/dist/validate/index.d.ts +21 -0
  11. package/dist/validate/index.d.ts.map +1 -1
  12. package/dist/validate/index.js +77 -0
  13. package/dist/validate/index.js.map +1 -1
  14. package/extensions/cmux/.agents/plugin.json +37 -0
  15. package/extensions/cmux/skills/cmux/SKILL.md +82 -0
  16. package/extensions/cmux/skills/cmux/agents/openai.yaml +4 -0
  17. package/extensions/cmux/skills/cmux/references/handles-and-identify.md +35 -0
  18. package/extensions/cmux/skills/cmux/references/panes-surfaces.md +37 -0
  19. package/extensions/cmux/skills/cmux/references/trigger-flash-and-health.md +23 -0
  20. package/extensions/cmux/skills/cmux/references/windows-workspaces.md +31 -0
  21. package/extensions/cmux/skills/cmux-vm-monitor/SKILL.md +122 -0
  22. package/extensions/cmux/skills/cmux-vm-monitor/agents/openai.yaml +4 -0
  23. package/extensions/cmux/skills/cmux-vm-monitor/references/cmux-commands.md +66 -0
  24. package/extensions/cmux/skills/cmux-vm-monitor/scripts/codex_vm_monitor.sh +45 -0
  25. package/extensions/cmux/skills/cmux-workspace/SKILL.md +93 -0
  26. package/extensions/dev-state/.agents/plugin.json +35 -0
  27. package/extensions/dev-state/skills/dev-state-plan-graduation/SKILL.md +194 -0
  28. package/extensions/dev-state/skills/dev-state-plan-graduation/agents/openai.yaml +4 -0
  29. package/extensions/dev-state/skills/dev-state-plan-graduation/references/reference.md +130 -0
  30. package/extensions/devops/.agents/plugin.json +36 -0
  31. package/extensions/devops/skills/azure-devops-cli/SKILL.md +431 -0
  32. package/extensions/devops/skills/azure-devops-cli/agents/openai.yaml +4 -0
  33. package/extensions/devops/skills/ci-pipeline-strategy/SKILL.md +217 -0
  34. package/extensions/devops/skills/ci-pipeline-strategy/agents/openai.yaml +4 -0
  35. package/{plugins/net-revenue-management/.codex-plugin → extensions/net-revenue-management/.agents}/plugin.json +10 -6
  36. package/extensions/plugin-dev/.agents/plugin.json +42 -0
  37. package/extensions/plugin-dev/.mcp.json +3 -0
  38. package/extensions/plugin-dev/agents/agent-creator.md +199 -0
  39. package/extensions/plugin-dev/agents/plugin-validator.md +91 -0
  40. package/extensions/plugin-dev/agents/skill-reviewer.md +212 -0
  41. package/extensions/plugin-dev/commands/_archive/create-marketplace.md +427 -0
  42. package/extensions/plugin-dev/commands/_archive/plugin-dev-guide.md +12 -0
  43. package/extensions/plugin-dev/commands/create-plugin.md +498 -0
  44. package/extensions/plugin-dev/commands/start.md +81 -0
  45. package/extensions/plugin-dev/hooks/hooks.json +3 -0
  46. package/extensions/plugin-dev/skills/agent-development/SKILL.md +641 -0
  47. package/extensions/plugin-dev/skills/agent-development/examples/agent-creation-prompt.md +250 -0
  48. package/extensions/plugin-dev/skills/agent-development/examples/complete-agent-examples.md +461 -0
  49. package/extensions/plugin-dev/skills/agent-development/references/advanced-agent-fields.md +246 -0
  50. package/extensions/plugin-dev/skills/agent-development/references/agent-creation-system-prompt.md +216 -0
  51. package/extensions/plugin-dev/skills/agent-development/references/permission-modes-rules.md +226 -0
  52. package/extensions/plugin-dev/skills/agent-development/references/system-prompt-design.md +464 -0
  53. package/extensions/plugin-dev/skills/agent-development/references/triggering-examples.md +474 -0
  54. package/extensions/plugin-dev/skills/agent-development/scripts/create-agent-skeleton.sh +176 -0
  55. package/extensions/plugin-dev/skills/agent-development/scripts/test-agent-trigger.sh +227 -0
  56. package/extensions/plugin-dev/skills/agent-development/scripts/validate-agent.sh +227 -0
  57. package/extensions/plugin-dev/skills/command-development/SKILL.md +763 -0
  58. package/extensions/plugin-dev/skills/command-development/examples/plugin-commands.md +612 -0
  59. package/extensions/plugin-dev/skills/command-development/examples/simple-commands.md +527 -0
  60. package/extensions/plugin-dev/skills/command-development/references/advanced-workflows.md +762 -0
  61. package/extensions/plugin-dev/skills/command-development/references/documentation-patterns.md +769 -0
  62. package/extensions/plugin-dev/skills/command-development/references/frontmatter-reference.md +508 -0
  63. package/extensions/plugin-dev/skills/command-development/references/interactive-commands.md +966 -0
  64. package/extensions/plugin-dev/skills/command-development/references/marketplace-considerations.md +943 -0
  65. package/extensions/plugin-dev/skills/command-development/references/plugin-features-reference.md +637 -0
  66. package/extensions/plugin-dev/skills/command-development/references/plugin-integration.md +191 -0
  67. package/extensions/plugin-dev/skills/command-development/references/skill-tool.md +447 -0
  68. package/extensions/plugin-dev/skills/command-development/references/testing-strategies.md +723 -0
  69. package/extensions/plugin-dev/skills/command-development/scripts/check-frontmatter.sh +234 -0
  70. package/extensions/plugin-dev/skills/command-development/scripts/validate-command.sh +160 -0
  71. package/extensions/plugin-dev/skills/hook-development/SKILL.md +861 -0
  72. package/extensions/plugin-dev/skills/hook-development/examples/load-context.sh +55 -0
  73. package/extensions/plugin-dev/skills/hook-development/examples/validate-bash.sh +57 -0
  74. package/extensions/plugin-dev/skills/hook-development/examples/validate-write.sh +48 -0
  75. package/extensions/plugin-dev/skills/hook-development/references/advanced.md +871 -0
  76. package/extensions/plugin-dev/skills/hook-development/references/hook-input-schemas.md +145 -0
  77. package/extensions/plugin-dev/skills/hook-development/references/migration.md +392 -0
  78. package/extensions/plugin-dev/skills/hook-development/references/patterns.md +430 -0
  79. package/extensions/plugin-dev/skills/hook-development/scripts/README.md +181 -0
  80. package/extensions/plugin-dev/skills/hook-development/scripts/hook-linter.sh +153 -0
  81. package/extensions/plugin-dev/skills/hook-development/scripts/test-hook.sh +276 -0
  82. package/extensions/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh +159 -0
  83. package/extensions/plugin-dev/skills/mcp-integration/SKILL.md +775 -0
  84. package/extensions/plugin-dev/skills/mcp-integration/examples/http-server.json +20 -0
  85. package/extensions/plugin-dev/skills/mcp-integration/examples/sse-server.json +19 -0
  86. package/extensions/plugin-dev/skills/mcp-integration/examples/stdio-server.json +38 -0
  87. package/extensions/plugin-dev/skills/mcp-integration/examples/ws-server.json +26 -0
  88. package/extensions/plugin-dev/skills/mcp-integration/references/authentication.md +601 -0
  89. package/extensions/plugin-dev/skills/mcp-integration/references/server-discovery.md +190 -0
  90. package/extensions/plugin-dev/skills/mcp-integration/references/server-types.md +572 -0
  91. package/extensions/plugin-dev/skills/mcp-integration/references/tool-usage.md +623 -0
  92. package/extensions/plugin-dev/skills/plugin-dev-guide/SKILL.md +222 -0
  93. package/extensions/plugin-dev/skills/plugin-structure/SKILL.md +705 -0
  94. package/extensions/plugin-dev/skills/plugin-structure/examples/advanced-plugin.md +774 -0
  95. package/extensions/plugin-dev/skills/plugin-structure/examples/minimal-plugin.md +83 -0
  96. package/extensions/plugin-dev/skills/plugin-structure/examples/standard-plugin.md +611 -0
  97. package/extensions/plugin-dev/skills/plugin-structure/references/advanced-topics.md +289 -0
  98. package/extensions/plugin-dev/skills/plugin-structure/references/component-patterns.md +592 -0
  99. package/extensions/plugin-dev/skills/plugin-structure/references/github-actions.md +233 -0
  100. package/extensions/plugin-dev/skills/plugin-structure/references/headless-ci-mode.md +193 -0
  101. package/extensions/plugin-dev/skills/plugin-structure/references/manifest-reference.md +625 -0
  102. package/extensions/plugin-dev/skills/plugin-structure/references/output-styles.md +116 -0
  103. package/extensions/plugin-dev/skills/skill-development/SKILL.md +564 -0
  104. package/extensions/plugin-dev/skills/skill-development/examples/complete-skill.md +465 -0
  105. package/extensions/plugin-dev/skills/skill-development/examples/frontmatter-templates.md +167 -0
  106. package/extensions/plugin-dev/skills/skill-development/examples/minimal-skill.md +111 -0
  107. package/extensions/plugin-dev/skills/skill-development/references/advanced-frontmatter.md +225 -0
  108. package/extensions/plugin-dev/skills/skill-development/references/commands-vs-skills.md +39 -0
  109. package/extensions/plugin-dev/skills/skill-development/references/skill-creation-workflow.md +379 -0
  110. package/extensions/plugin-dev/skills/skill-development/references/skill-creator-original.md +210 -0
  111. package/package.json +8 -11
  112. package/scripts/enact-extensions.mjs +751 -16
  113. package/scripts/hooks/session-start-drift-check.mjs +58 -0
  114. package/scripts/lib/build-index.mjs +50 -0
  115. package/scripts/lib/bundle-hash.mjs +137 -0
  116. package/scripts/lib/hooks.mjs +389 -0
  117. package/scripts/lib/ledger.mjs +162 -0
  118. package/scripts/lib/list-bundles.mjs +70 -0
  119. package/scripts/lib/outdated.mjs +144 -0
  120. package/scripts/lib/provision-mcp.mjs +369 -0
  121. package/scripts/lib/resolve-bundle.mjs +121 -0
  122. package/scripts/lib/run-install.mjs +321 -39
  123. package/scripts/lib/run-uninstall.mjs +220 -0
  124. package/scripts/lib/run-update.mjs +152 -0
  125. package/scripts/lib/run-validate.mjs +12 -18
  126. package/scripts/lib/serve.mjs +454 -0
  127. package/scripts/postinstall.mjs +63 -0
  128. package/scripts/setup-enact-context.sh +2 -2
  129. package/spec/index.json +59 -0
  130. package/web/assets/README.md +111 -0
  131. package/web/assets/logo-full.png +0 -0
  132. package/web/assets/logo-slim.png +0 -0
  133. package/web/assets/tokens/base.css +45 -0
  134. package/web/assets/tokens/colors.css +248 -0
  135. package/web/assets/tokens/effects.css +24 -0
  136. package/web/assets/tokens/fonts.css +8 -0
  137. package/web/assets/tokens/index.css +18 -0
  138. package/web/assets/tokens/spacing.css +50 -0
  139. package/web/index.html +1188 -0
  140. package/.agents/plugins/marketplace.json +0 -20
  141. package/catalog/enact-context.json +0 -9
  142. package/catalog/enact-factory.json +0 -7
  143. package/catalog/enact-operator.json +0 -7
  144. package/catalog/enact-wiki.json +0 -7
  145. package/catalog/net-revenue-management.json +0 -8
  146. package/scripts/rename-supervisor-to-operator.pl +0 -66
  147. package/scripts/sync-manifests.mjs +0 -23
  148. package/scripts/validate-catalog.mjs +0 -37
  149. package/scripts/validate-plugin.mjs +0 -10
  150. /package/{plugins → extensions}/net-revenue-management/.mcp.json +0 -0
  151. /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-risks/SKILL.md +0 -0
  152. /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-scenario/SKILL.md +0 -0
package/web/index.html ADDED
@@ -0,0 +1,1188 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ENACT OS — Extensions</title>
7
+ <link rel="stylesheet" href="/assets/tokens/index.css" />
8
+ <style>
9
+ /* ── Reset & base ───────────────────────────────────────────────────── */
10
+ *, *::before, *::after { box-sizing: border-box; }
11
+
12
+ /* ── Layout ─────────────────────────────────────────────────────────── */
13
+ body {
14
+ min-height: 100vh;
15
+ background: var(--bg-canvas);
16
+ color: var(--text-primary);
17
+ display: flex;
18
+ flex-direction: column;
19
+ }
20
+
21
+ /* ── Header ─────────────────────────────────────────────────────────── */
22
+ .site-header {
23
+ position: sticky;
24
+ top: 0;
25
+ z-index: var(--z-sticky);
26
+ background: var(--surface-default);
27
+ border-bottom: 1px solid var(--border-subtle);
28
+ box-shadow: var(--shadow-sm);
29
+ }
30
+
31
+ .header-inner {
32
+ max-width: var(--container-max);
33
+ margin: 0 auto;
34
+ padding: var(--space-3) var(--space-6);
35
+ display: flex;
36
+ align-items: center;
37
+ gap: var(--space-4);
38
+ }
39
+
40
+ .header-logo {
41
+ height: 32px;
42
+ width: auto;
43
+ display: block;
44
+ flex-shrink: 0;
45
+ }
46
+
47
+ .header-divider {
48
+ width: 1px;
49
+ height: 24px;
50
+ background: var(--border-default);
51
+ flex-shrink: 0;
52
+ }
53
+
54
+ .header-title {
55
+ font-family: var(--font-display);
56
+ font-size: 1rem;
57
+ font-weight: 600;
58
+ letter-spacing: -0.01em;
59
+ color: var(--text-primary);
60
+ flex: 1;
61
+ white-space: nowrap;
62
+ }
63
+
64
+ .header-title span {
65
+ color: var(--brand-primary);
66
+ }
67
+
68
+ .header-actions {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: var(--space-3);
72
+ }
73
+
74
+ /* ── Theme toggle ────────────────────────────────────────────────────── */
75
+ .theme-toggle {
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ width: 36px;
80
+ height: 36px;
81
+ border-radius: var(--radius-sm);
82
+ border: 1px solid var(--border-default);
83
+ background: var(--surface-raised);
84
+ color: var(--text-secondary);
85
+ cursor: pointer;
86
+ transition: background var(--duration-fast) var(--ease-out),
87
+ color var(--duration-fast) var(--ease-out),
88
+ border-color var(--duration-fast) var(--ease-out);
89
+ }
90
+ .theme-toggle:hover {
91
+ background: var(--surface-sunken);
92
+ color: var(--text-primary);
93
+ border-color: var(--border-default);
94
+ }
95
+ .theme-toggle:focus-visible {
96
+ outline: none;
97
+ box-shadow: var(--shadow-focus);
98
+ border-color: var(--border-focus);
99
+ }
100
+ .theme-toggle svg { width: 16px; height: 16px; flex-shrink: 0; }
101
+ .theme-toggle .icon-sun { display: none; }
102
+ .theme-toggle .icon-moon { display: block; }
103
+ [data-theme="dark"] .theme-toggle .icon-sun { display: block; }
104
+ [data-theme="dark"] .theme-toggle .icon-moon { display: none; }
105
+
106
+ /* ── Main content ────────────────────────────────────────────────────── */
107
+ .main-content {
108
+ flex: 1;
109
+ max-width: var(--container-max);
110
+ margin: 0 auto;
111
+ padding: var(--space-8) var(--space-6);
112
+ width: 100%;
113
+ }
114
+
115
+ /* ── Page intro ──────────────────────────────────────────────────────── */
116
+ .page-intro {
117
+ margin-bottom: var(--space-8);
118
+ }
119
+
120
+ .page-heading {
121
+ font-family: var(--font-display);
122
+ font-size: 1.75rem;
123
+ font-weight: 700;
124
+ letter-spacing: -0.03em;
125
+ line-height: 1.2;
126
+ color: var(--text-primary);
127
+ margin: 0 0 var(--space-2);
128
+ }
129
+
130
+ .page-subheading {
131
+ font-size: 0.9375rem;
132
+ color: var(--text-secondary);
133
+ margin: 0;
134
+ }
135
+
136
+ .page-meta {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: var(--space-4);
140
+ margin-top: var(--space-4);
141
+ flex-wrap: wrap;
142
+ }
143
+
144
+ .count-badge {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ gap: var(--space-1-5);
148
+ padding: var(--space-1) var(--space-3);
149
+ background: var(--brand-primary-subtle);
150
+ color: var(--brand-primary);
151
+ border-radius: var(--radius-full);
152
+ font-size: 0.8125rem;
153
+ font-weight: 600;
154
+ font-family: var(--font-mono);
155
+ }
156
+
157
+ /* ── Filter bar ──────────────────────────────────────────────────────── */
158
+ .filter-bar {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: var(--space-3);
162
+ margin-bottom: var(--space-6);
163
+ flex-wrap: wrap;
164
+ }
165
+
166
+ .filter-label {
167
+ font-size: 0.8125rem;
168
+ font-weight: 600;
169
+ color: var(--text-secondary);
170
+ white-space: nowrap;
171
+ }
172
+
173
+ .filter-pills {
174
+ display: flex;
175
+ gap: var(--space-2);
176
+ flex-wrap: wrap;
177
+ }
178
+
179
+ .filter-pill {
180
+ padding: var(--space-1) var(--space-3);
181
+ border-radius: var(--radius-full);
182
+ border: 1px solid var(--border-default);
183
+ background: var(--surface-default);
184
+ color: var(--text-secondary);
185
+ font-size: 0.8125rem;
186
+ font-weight: 500;
187
+ cursor: pointer;
188
+ transition: all var(--duration-fast) var(--ease-out);
189
+ }
190
+ .filter-pill:hover {
191
+ border-color: var(--border-strong);
192
+ color: var(--text-primary);
193
+ }
194
+ .filter-pill:focus-visible {
195
+ outline: none;
196
+ box-shadow: var(--shadow-focus);
197
+ border-color: var(--border-focus);
198
+ }
199
+ .filter-pill.active {
200
+ background: var(--brand-primary);
201
+ border-color: var(--brand-primary);
202
+ color: var(--text-on-brand);
203
+ }
204
+
205
+ /* ── Plugin grid ─────────────────────────────────────────────────────── */
206
+ .plugins-grid {
207
+ display: grid;
208
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
209
+ gap: var(--space-5);
210
+ }
211
+
212
+ @media (max-width: 640px) {
213
+ .plugins-grid {
214
+ grid-template-columns: 1fr;
215
+ }
216
+ }
217
+
218
+ /* ── Plugin card ─────────────────────────────────────────────────────── */
219
+ .plugin-card {
220
+ background: var(--surface-default);
221
+ border: 1px solid var(--border-subtle);
222
+ border-radius: var(--radius-md);
223
+ box-shadow: var(--shadow-sm);
224
+ display: flex;
225
+ flex-direction: column;
226
+ overflow: hidden;
227
+ transition: box-shadow var(--duration-fast) var(--ease-out),
228
+ border-color var(--duration-fast) var(--ease-out),
229
+ transform var(--duration-fast) var(--ease-out);
230
+ }
231
+ .plugin-card:hover {
232
+ box-shadow: var(--shadow-md);
233
+ border-color: var(--border-default);
234
+ transform: translateY(-1px);
235
+ }
236
+
237
+ .card-header {
238
+ padding: var(--space-5) var(--space-5) var(--space-4);
239
+ display: flex;
240
+ align-items: flex-start;
241
+ justify-content: space-between;
242
+ gap: var(--space-3);
243
+ }
244
+
245
+ .card-title-group { flex: 1; min-width: 0; }
246
+
247
+ .card-name {
248
+ font-family: var(--font-display);
249
+ font-size: 1rem;
250
+ font-weight: 600;
251
+ color: var(--text-primary);
252
+ letter-spacing: -0.01em;
253
+ white-space: nowrap;
254
+ overflow: hidden;
255
+ text-overflow: ellipsis;
256
+ margin: 0 0 var(--space-1);
257
+ }
258
+
259
+ .card-version {
260
+ font-family: var(--font-mono);
261
+ font-size: 0.75rem;
262
+ color: var(--text-muted);
263
+ }
264
+
265
+ .card-meta {
266
+ display: flex;
267
+ flex-wrap: wrap;
268
+ gap: var(--space-2);
269
+ padding: 0 var(--space-5) var(--space-3);
270
+ }
271
+
272
+ /* Category badge */
273
+ .badge-category {
274
+ display: inline-flex;
275
+ align-items: center;
276
+ padding: 2px var(--space-2);
277
+ border-radius: var(--radius-xs);
278
+ font-size: 0.6875rem;
279
+ font-weight: 700;
280
+ letter-spacing: 0.04em;
281
+ text-transform: uppercase;
282
+ }
283
+ .badge-cat-devops { background: var(--adl-indigo-50); color: var(--adl-indigo-700); }
284
+ .badge-cat-cmux { background: var(--adl-cyan-50); color: var(--adl-cyan-700); }
285
+ .badge-cat-dev { background: var(--adl-mint-50); color: var(--adl-mint-700); }
286
+ .badge-cat-nrm { background: var(--adl-amber-50); color: var(--adl-amber-700); }
287
+ .badge-cat-default { background: var(--adl-navy-100); color: var(--adl-navy-700); }
288
+ [data-theme="dark"] .badge-cat-devops { background: var(--adl-indigo-900); color: var(--adl-indigo-200); }
289
+ [data-theme="dark"] .badge-cat-cmux { background: var(--adl-navy-800); color: var(--adl-cyan-300); }
290
+ [data-theme="dark"] .badge-cat-dev { background: var(--adl-teal-900); color: var(--adl-mint-300); }
291
+ [data-theme="dark"] .badge-cat-nrm { background: var(--adl-amber-700); color: var(--adl-amber-100); }
292
+ [data-theme="dark"] .badge-cat-default { background: var(--adl-navy-800); color: var(--adl-navy-200); }
293
+
294
+ /* Target chips */
295
+ .chip-target {
296
+ display: inline-flex;
297
+ align-items: center;
298
+ gap: 4px;
299
+ padding: 2px var(--space-2);
300
+ border: 1px solid var(--border-subtle);
301
+ border-radius: var(--radius-xs);
302
+ font-family: var(--font-mono);
303
+ font-size: 0.6875rem;
304
+ font-weight: 500;
305
+ color: var(--text-secondary);
306
+ background: var(--surface-raised);
307
+ }
308
+ .chip-target.codex { border-color: var(--adl-teal-200); color: var(--adl-teal-700); }
309
+ .chip-target.claude { border-color: var(--adl-violet-200); color: var(--adl-violet-700); }
310
+ .chip-target.cursor { border-color: var(--adl-blue-200); color: var(--adl-blue-700); }
311
+ .chip-target.enact { border-color: var(--adl-mint-200); color: var(--adl-mint-700); }
312
+ .chip-target.shared { border-color: var(--adl-cyan-200); color: var(--adl-cyan-700); }
313
+ [data-theme="dark"] .chip-target.codex { border-color: var(--adl-teal-800); color: var(--adl-teal-300); }
314
+ [data-theme="dark"] .chip-target.claude { border-color: var(--adl-violet-800); color: var(--adl-violet-300); }
315
+ [data-theme="dark"] .chip-target.cursor { border-color: var(--adl-blue-800); color: var(--adl-blue-300); }
316
+ [data-theme="dark"] .chip-target.enact { border-color: var(--adl-mint-800); color: var(--adl-mint-300); }
317
+ [data-theme="dark"] .chip-target.shared { border-color: var(--adl-cyan-800); color: var(--adl-cyan-300); }
318
+
319
+ .card-description {
320
+ padding: 0 var(--space-5) var(--space-4);
321
+ font-size: 0.875rem;
322
+ color: var(--text-secondary);
323
+ line-height: 1.55;
324
+ display: -webkit-box;
325
+ -webkit-line-clamp: 3;
326
+ -webkit-box-orient: vertical;
327
+ overflow: hidden;
328
+ flex: 1;
329
+ }
330
+
331
+ /* Installed indicator */
332
+ .installed-indicator {
333
+ margin: 0 var(--space-5) var(--space-3);
334
+ padding: var(--space-2) var(--space-3);
335
+ background: var(--feedback-success-subtle);
336
+ border: 1px solid color-mix(in srgb, var(--feedback-success) 30%, transparent);
337
+ border-radius: var(--radius-sm);
338
+ font-size: 0.8125rem;
339
+ color: var(--feedback-success);
340
+ display: none;
341
+ align-items: center;
342
+ gap: var(--space-2);
343
+ }
344
+ .installed-indicator.visible { display: flex; }
345
+ .installed-indicator svg { width: 14px; height: 14px; flex-shrink: 0; }
346
+ .installed-platforms {
347
+ font-family: var(--font-mono);
348
+ font-size: 0.75rem;
349
+ opacity: 0.85;
350
+ }
351
+
352
+ /* Card controls */
353
+ .card-controls {
354
+ padding: var(--space-4) var(--space-5);
355
+ border-top: 1px solid var(--border-subtle);
356
+ background: var(--surface-raised);
357
+ display: flex;
358
+ flex-direction: column;
359
+ gap: var(--space-3);
360
+ }
361
+
362
+ .controls-row {
363
+ display: flex;
364
+ gap: var(--space-3);
365
+ align-items: center;
366
+ }
367
+
368
+ /* Select */
369
+ .ctrl-select {
370
+ flex: 1;
371
+ min-width: 0;
372
+ }
373
+
374
+ .ctrl-select label {
375
+ display: block;
376
+ font-size: 0.75rem;
377
+ font-weight: 600;
378
+ color: var(--text-muted);
379
+ margin-bottom: var(--space-1);
380
+ letter-spacing: 0.04em;
381
+ text-transform: uppercase;
382
+ }
383
+
384
+ .ctrl-select select {
385
+ width: 100%;
386
+ padding: var(--space-1-5) var(--space-3);
387
+ background: var(--surface-default);
388
+ border: 1px solid var(--border-default);
389
+ border-radius: var(--radius-sm);
390
+ color: var(--text-primary);
391
+ font-size: 0.8125rem;
392
+ font-family: var(--font-body);
393
+ cursor: pointer;
394
+ appearance: none;
395
+ -webkit-appearance: none;
396
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2351647a' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
397
+ background-repeat: no-repeat;
398
+ background-position: right 10px center;
399
+ padding-right: var(--space-7);
400
+ transition: border-color var(--duration-fast) var(--ease-out);
401
+ }
402
+ .ctrl-select select:hover { border-color: var(--border-strong); }
403
+ .ctrl-select select:focus-visible {
404
+ outline: none;
405
+ border-color: var(--border-focus);
406
+ box-shadow: var(--shadow-focus);
407
+ }
408
+
409
+ /* Scope segmented control */
410
+ .ctrl-scope { flex: 1; min-width: 0; }
411
+
412
+ .ctrl-scope label {
413
+ display: block;
414
+ font-size: 0.75rem;
415
+ font-weight: 600;
416
+ color: var(--text-muted);
417
+ margin-bottom: var(--space-1);
418
+ letter-spacing: 0.04em;
419
+ text-transform: uppercase;
420
+ }
421
+
422
+ .scope-toggle {
423
+ display: flex;
424
+ border: 1px solid var(--border-default);
425
+ border-radius: var(--radius-sm);
426
+ overflow: hidden;
427
+ }
428
+
429
+ .scope-toggle button {
430
+ flex: 1;
431
+ padding: var(--space-1-5) var(--space-2);
432
+ border: none;
433
+ background: var(--surface-default);
434
+ color: var(--text-secondary);
435
+ font-size: 0.8125rem;
436
+ font-weight: 500;
437
+ font-family: var(--font-body);
438
+ cursor: pointer;
439
+ transition: background var(--duration-fast) var(--ease-out),
440
+ color var(--duration-fast) var(--ease-out);
441
+ border-right: 1px solid var(--border-default);
442
+ }
443
+ .scope-toggle button:last-child { border-right: none; }
444
+ .scope-toggle button:hover {
445
+ background: var(--surface-sunken);
446
+ color: var(--text-primary);
447
+ }
448
+ .scope-toggle button:focus-visible {
449
+ outline: none;
450
+ box-shadow: inset var(--shadow-focus);
451
+ }
452
+ .scope-toggle button.active {
453
+ background: var(--brand-primary);
454
+ color: var(--text-on-brand);
455
+ }
456
+ .scope-toggle button.active:hover { background: var(--brand-primary-hover); }
457
+
458
+ /* Action buttons */
459
+ .actions-row {
460
+ display: flex;
461
+ gap: var(--space-2);
462
+ }
463
+
464
+ .btn {
465
+ flex: 1;
466
+ display: inline-flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ gap: var(--space-1-5);
470
+ padding: var(--space-2) var(--space-3);
471
+ border-radius: var(--radius-sm);
472
+ font-size: 0.8125rem;
473
+ font-weight: 600;
474
+ font-family: var(--font-body);
475
+ cursor: pointer;
476
+ border: 1px solid transparent;
477
+ transition: all var(--duration-fast) var(--ease-out);
478
+ white-space: nowrap;
479
+ min-height: 34px;
480
+ }
481
+ .btn:focus-visible {
482
+ outline: none;
483
+ box-shadow: var(--shadow-focus);
484
+ }
485
+ .btn:disabled {
486
+ opacity: 0.5;
487
+ cursor: not-allowed;
488
+ pointer-events: none;
489
+ }
490
+
491
+ .btn-primary {
492
+ background: var(--brand-primary);
493
+ color: var(--text-on-brand);
494
+ border-color: transparent;
495
+ }
496
+ .btn-primary:hover { background: var(--brand-primary-hover); }
497
+ .btn-primary:active { background: var(--brand-primary-active); }
498
+
499
+ .btn-danger {
500
+ background: var(--surface-default);
501
+ color: var(--feedback-danger);
502
+ border-color: color-mix(in srgb, var(--feedback-danger) 35%, transparent);
503
+ }
504
+ .btn-danger:hover {
505
+ background: var(--feedback-danger-subtle);
506
+ border-color: var(--feedback-danger);
507
+ }
508
+ .btn-danger:active { opacity: 0.8; }
509
+
510
+ .btn-danger.installed-btn {
511
+ background: var(--feedback-danger-subtle);
512
+ border-color: color-mix(in srgb, var(--feedback-danger) 50%, transparent);
513
+ color: var(--feedback-danger);
514
+ }
515
+
516
+ /* Spinner */
517
+ @keyframes spin {
518
+ to { transform: rotate(360deg); }
519
+ }
520
+ .spinner {
521
+ width: 13px;
522
+ height: 13px;
523
+ border: 2px solid currentColor;
524
+ border-top-color: transparent;
525
+ border-radius: 50%;
526
+ animation: spin 0.6s linear infinite;
527
+ flex-shrink: 0;
528
+ }
529
+
530
+ /* ── Toast region ────────────────────────────────────────────────────── */
531
+ .toast-region {
532
+ position: fixed;
533
+ bottom: var(--space-6);
534
+ right: var(--space-6);
535
+ z-index: var(--z-toast);
536
+ display: flex;
537
+ flex-direction: column;
538
+ gap: var(--space-3);
539
+ pointer-events: none;
540
+ max-width: 400px;
541
+ }
542
+
543
+ .toast {
544
+ display: flex;
545
+ align-items: flex-start;
546
+ gap: var(--space-3);
547
+ padding: var(--space-4) var(--space-5);
548
+ border-radius: var(--radius-md);
549
+ box-shadow: var(--shadow-lg);
550
+ pointer-events: all;
551
+ border: 1px solid transparent;
552
+ opacity: 0;
553
+ transform: translateY(8px);
554
+ transition: opacity var(--duration-fast) var(--ease-out),
555
+ transform var(--duration-fast) var(--ease-out);
556
+ font-size: 0.875rem;
557
+ line-height: 1.4;
558
+ }
559
+ .toast.entering {
560
+ opacity: 1;
561
+ transform: translateY(0);
562
+ }
563
+ .toast.leaving {
564
+ opacity: 0;
565
+ transform: translateY(4px);
566
+ }
567
+
568
+ .toast-success {
569
+ background: var(--feedback-success-subtle);
570
+ border-color: color-mix(in srgb, var(--feedback-success) 30%, transparent);
571
+ color: var(--feedback-success);
572
+ }
573
+
574
+ .toast-error {
575
+ background: var(--feedback-danger-subtle);
576
+ border-color: color-mix(in srgb, var(--feedback-danger) 30%, transparent);
577
+ color: var(--feedback-danger);
578
+ }
579
+
580
+ .toast-icon { width: 18px; height: 18px; flex-shrink: 0; margin-top: 1px; }
581
+ .toast-body { flex: 1; }
582
+ .toast-title { font-weight: 700; margin-bottom: 2px; }
583
+ .toast-msg { opacity: 0.85; }
584
+
585
+ .toast-close {
586
+ width: 20px;
587
+ height: 20px;
588
+ display: flex;
589
+ align-items: center;
590
+ justify-content: center;
591
+ border: none;
592
+ background: transparent;
593
+ color: inherit;
594
+ cursor: pointer;
595
+ opacity: 0.6;
596
+ border-radius: var(--radius-xs);
597
+ padding: 0;
598
+ flex-shrink: 0;
599
+ }
600
+ .toast-close:hover { opacity: 1; }
601
+ .toast-close:focus-visible {
602
+ outline: none;
603
+ box-shadow: var(--shadow-focus);
604
+ }
605
+
606
+ /* ── Empty state ─────────────────────────────────────────────────────── */
607
+ .empty-state {
608
+ text-align: center;
609
+ padding: var(--space-20) var(--space-6);
610
+ color: var(--text-secondary);
611
+ }
612
+ .empty-state-icon {
613
+ font-size: 3rem;
614
+ margin-bottom: var(--space-4);
615
+ opacity: 0.4;
616
+ }
617
+ .empty-state h2 {
618
+ font-family: var(--font-display);
619
+ font-size: 1.25rem;
620
+ color: var(--text-secondary);
621
+ margin-bottom: var(--space-2);
622
+ }
623
+ .empty-state p { font-size: 0.9375rem; color: var(--text-muted); }
624
+
625
+ /* ── Loading skeleton ────────────────────────────────────────────────── */
626
+ @keyframes shimmer {
627
+ 0% { background-position: -400px 0; }
628
+ 100% { background-position: 400px 0; }
629
+ }
630
+ .skeleton {
631
+ background: linear-gradient(
632
+ 90deg,
633
+ var(--surface-raised) 25%,
634
+ var(--surface-sunken) 50%,
635
+ var(--surface-raised) 75%
636
+ );
637
+ background-size: 800px 100%;
638
+ animation: shimmer 1.4s infinite linear;
639
+ border-radius: var(--radius-sm);
640
+ }
641
+ .skeleton-card {
642
+ height: 260px;
643
+ border-radius: var(--radius-md);
644
+ }
645
+
646
+ /* ── Footer ─────────────────────────────────────────────────────────── */
647
+ .site-footer {
648
+ border-top: 1px solid var(--border-subtle);
649
+ padding: var(--space-5) var(--space-6);
650
+ text-align: center;
651
+ font-size: 0.8125rem;
652
+ color: var(--text-muted);
653
+ }
654
+ .site-footer a { color: var(--text-link); }
655
+
656
+ /* ── Misc ────────────────────────────────────────────────────────────── */
657
+ .visually-hidden {
658
+ position: absolute;
659
+ width: 1px; height: 1px;
660
+ padding: 0; margin: -1px;
661
+ overflow: hidden;
662
+ clip: rect(0,0,0,0);
663
+ white-space: nowrap;
664
+ border: 0;
665
+ }
666
+ </style>
667
+ </head>
668
+ <body>
669
+
670
+ <!-- ── Header ──────────────────────────────────────────────────────────── -->
671
+ <header class="site-header" role="banner">
672
+ <div class="header-inner">
673
+ <img src="/assets/logo-slim.png" alt="Amsterdam Data Labs" class="header-logo" />
674
+ <div class="header-divider" aria-hidden="true"></div>
675
+ <h1 class="header-title">ENACT OS — <span>Extensions</span></h1>
676
+ <div class="header-actions">
677
+ <button
678
+ class="theme-toggle"
679
+ id="theme-toggle"
680
+ aria-label="Toggle light/dark theme"
681
+ title="Toggle theme"
682
+ type="button"
683
+ >
684
+ <!-- Moon (shown in light mode) -->
685
+ <svg class="icon-moon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
686
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
687
+ </svg>
688
+ <!-- Sun (shown in dark mode) -->
689
+ <svg class="icon-sun" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
690
+ <circle cx="12" cy="12" r="5"/>
691
+ <line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
692
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
693
+ <line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
694
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
695
+ </svg>
696
+ </button>
697
+ </div>
698
+ </div>
699
+ </header>
700
+
701
+ <!-- ── Main ────────────────────────────────────────────────────────────── -->
702
+ <main class="main-content" id="main" role="main">
703
+
704
+ <!-- Page intro -->
705
+ <div class="page-intro">
706
+ <h2 class="page-heading">Plugin Marketplace</h2>
707
+ <p class="page-subheading">Discover and install Enact OS extensions for Codex, Claude, Cursor, and more.</p>
708
+ <div class="page-meta">
709
+ <span class="count-badge" id="plugin-count" aria-live="polite">Loading…</span>
710
+ </div>
711
+ </div>
712
+
713
+ <!-- Filter bar (populated after data loads) -->
714
+ <div class="filter-bar" id="filter-bar" role="group" aria-label="Filter by category">
715
+ <span class="filter-label">Category:</span>
716
+ <div class="filter-pills" id="filter-pills"></div>
717
+ </div>
718
+
719
+ <!-- Plugin grid -->
720
+ <div
721
+ id="plugins-grid"
722
+ class="plugins-grid"
723
+ role="list"
724
+ aria-label="Available plugins"
725
+ aria-busy="true"
726
+ >
727
+ <!-- Skeleton loaders -->
728
+ <div class="skeleton skeleton-card" role="listitem" aria-label="Loading…"></div>
729
+ <div class="skeleton skeleton-card" role="listitem" aria-label="Loading…"></div>
730
+ <div class="skeleton skeleton-card" role="listitem" aria-label="Loading…"></div>
731
+ </div>
732
+ </main>
733
+
734
+ <!-- ── Footer ──────────────────────────────────────────────────────────── -->
735
+ <footer class="site-footer">
736
+ <span>ENACT OS Extensions &mdash; <a href="/api/index">/api/index</a></span>
737
+ </footer>
738
+
739
+ <!-- ── Toast region (aria-live polite) ─────────────────────────────────── -->
740
+ <div
741
+ class="toast-region"
742
+ id="toast-region"
743
+ aria-label="Notifications"
744
+ ></div>
745
+
746
+ <!-- ── App script ──────────────────────────────────────────────────────── -->
747
+ <script type="module">
748
+ // ─────────────────────────────────────────────────────────────────────────
749
+ // Theme management
750
+ // ─────────────────────────────────────────────────────────────────────────
751
+ const THEME_KEY = 'enact-theme';
752
+ const root = document.documentElement;
753
+
754
+ function applyTheme(theme) {
755
+ root.setAttribute('data-theme', theme);
756
+ localStorage.setItem(THEME_KEY, theme);
757
+ }
758
+
759
+ function initTheme() {
760
+ const saved = localStorage.getItem(THEME_KEY);
761
+ if (saved) {
762
+ applyTheme(saved);
763
+ } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
764
+ applyTheme('dark');
765
+ }
766
+ }
767
+
768
+ initTheme();
769
+
770
+ document.getElementById('theme-toggle').addEventListener('click', () => {
771
+ const current = root.getAttribute('data-theme');
772
+ applyTheme(current === 'dark' ? 'light' : 'dark');
773
+ });
774
+
775
+ // ─────────────────────────────────────────────────────────────────────────
776
+ // Toast system
777
+ // ─────────────────────────────────────────────────────────────────────────
778
+ const toastRegion = document.getElementById('toast-region');
779
+
780
+ function showToast(type, title, message, durationMs = 4000) {
781
+ const toast = document.createElement('div');
782
+ toast.className = `toast toast-${type}`;
783
+ toast.setAttribute('role', 'alert');
784
+
785
+ const isSuccess = type === 'success';
786
+ const iconHtml = isSuccess
787
+ ? `<svg class="toast-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`
788
+ : `<svg class="toast-icon" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`;
789
+
790
+ toast.innerHTML = `
791
+ ${iconHtml}
792
+ <div class="toast-body">
793
+ <div class="toast-title">${escHtml(title)}</div>
794
+ <div class="toast-msg">${escHtml(message)}</div>
795
+ </div>
796
+ <button class="toast-close" aria-label="Dismiss notification" type="button">
797
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true">
798
+ <line x1="1" y1="1" x2="13" y2="13"/><line x1="13" y1="1" x2="1" y2="13"/>
799
+ </svg>
800
+ </button>
801
+ `;
802
+
803
+ toast.querySelector('.toast-close').addEventListener('click', () => dismissToast(toast));
804
+ toastRegion.appendChild(toast);
805
+
806
+ // Animate in
807
+ requestAnimationFrame(() => {
808
+ requestAnimationFrame(() => toast.classList.add('entering'));
809
+ });
810
+
811
+ if (durationMs > 0) {
812
+ setTimeout(() => dismissToast(toast), durationMs);
813
+ }
814
+ return toast;
815
+ }
816
+
817
+ function dismissToast(toast) {
818
+ toast.classList.remove('entering');
819
+ toast.classList.add('leaving');
820
+ toast.addEventListener('transitionend', () => toast.remove(), { once: true });
821
+ }
822
+
823
+ function escHtml(str) {
824
+ return String(str)
825
+ .replace(/&/g, '&amp;')
826
+ .replace(/</g, '&lt;')
827
+ .replace(/>/g, '&gt;')
828
+ .replace(/"/g, '&quot;');
829
+ }
830
+
831
+ // Summarise MCP dependency provisioning for the success toast. Returns a
832
+ // plain-text suffix (showToast escapes it via escHtml, so no markup here).
833
+ function provisionSummary(provision) {
834
+ const interesting = provision.filter(p => p && p.status && p.status !== 'skipped');
835
+ if (interesting.length === 0) return '';
836
+ const parts = interesting.map(p => {
837
+ const pkg = p.package ? ` ${p.package}` : '';
838
+ return `${p.server}:${p.status}${pkg}`;
839
+ });
840
+ return ` | Deps: ${parts.join(', ')}`;
841
+ }
842
+
843
+ // ─────────────────────────────────────────────────────────────────────────
844
+ // Data fetching
845
+ // ─────────────────────────────────────────────────────────────────────────
846
+ let installedMap = {}; // { pluginName: [{platform, scope, home, path}] }
847
+ let allPlugins = [];
848
+ let activeFilter = 'all';
849
+
850
+ async function fetchIndex() {
851
+ const res = await fetch('/api/index');
852
+ if (!res.ok) throw new Error(`/api/index returned ${res.status}`);
853
+ return res.json();
854
+ }
855
+
856
+ async function fetchInstalled() {
857
+ const res = await fetch('/api/installed');
858
+ if (!res.ok) throw new Error(`/api/installed returned ${res.status}`);
859
+ return res.json();
860
+ }
861
+
862
+ // ─────────────────────────────────────────────────────────────────────────
863
+ // Category helpers
864
+ // ─────────────────────────────────────────────────────────────────────────
865
+ function categoryClass(cat = '') {
866
+ const c = cat.toLowerCase();
867
+ if (c.includes('devops') || c.includes('azure')) return 'badge-cat-devops';
868
+ if (c.includes('cmux') || c.includes('terminal')) return 'badge-cat-cmux';
869
+ if (c.includes('dev') || c.includes('plugin')) return 'badge-cat-dev';
870
+ if (c.includes('revenue') || c.includes('nrm') || c.includes('management')) return 'badge-cat-nrm';
871
+ return 'badge-cat-default';
872
+ }
873
+
874
+ function inferCategory(plugin) {
875
+ // Try category field, else derive from name
876
+ if (plugin.category && plugin.category !== 'unknown' && plugin.category !== '') {
877
+ return plugin.category;
878
+ }
879
+ const n = plugin.name.toLowerCase();
880
+ if (n.includes('cmux')) return 'cmux';
881
+ if (n.includes('devops') || n.includes('azure')) return 'devops';
882
+ if (n.includes('revenue') || n.includes('nrm') || n.includes('management')) return 'net-revenue';
883
+ if (n.includes('dev') || n.includes('plugin')) return 'plugin-dev';
884
+ return 'extension';
885
+ }
886
+
887
+ // ─────────────────────────────────────────────────────────────────────────
888
+ // Render filter bar
889
+ // ─────────────────────────────────────────────────────────────────────────
890
+ function renderFilters(plugins) {
891
+ const categories = ['all', ...new Set(plugins.map(inferCategory))];
892
+ const container = document.getElementById('filter-pills');
893
+ container.innerHTML = '';
894
+ categories.forEach(cat => {
895
+ const btn = document.createElement('button');
896
+ btn.className = `filter-pill${cat === activeFilter ? ' active' : ''}`;
897
+ btn.textContent = cat === 'all' ? 'All' : cat;
898
+ btn.setAttribute('type', 'button');
899
+ btn.setAttribute('aria-pressed', cat === activeFilter ? 'true' : 'false');
900
+ btn.addEventListener('click', () => {
901
+ activeFilter = cat;
902
+ renderFilters(plugins);
903
+ renderCards(plugins);
904
+ });
905
+ container.appendChild(btn);
906
+ });
907
+ }
908
+
909
+ // ─────────────────────────────────────────────────────────────────────────
910
+ // Render plugin cards
911
+ // ─────────────────────────────────────────────────────────────────────────
912
+ function renderCards(plugins) {
913
+ const grid = document.getElementById('plugins-grid');
914
+ grid.setAttribute('aria-busy', 'false');
915
+ grid.innerHTML = '';
916
+
917
+ const filtered = activeFilter === 'all'
918
+ ? plugins
919
+ : plugins.filter(p => inferCategory(p) === activeFilter);
920
+
921
+ if (filtered.length === 0) {
922
+ grid.innerHTML = `
923
+ <div class="empty-state" style="grid-column: 1/-1;">
924
+ <div class="empty-state-icon" aria-hidden="true">📦</div>
925
+ <h2>No plugins found</h2>
926
+ <p>${activeFilter === 'all'
927
+ ? 'No extensions are available yet.'
928
+ : `No extensions in the "${escHtml(activeFilter)}" category.`}</p>
929
+ </div>
930
+ `;
931
+ return;
932
+ }
933
+
934
+ filtered.forEach(plugin => {
935
+ const card = buildCard(plugin);
936
+ grid.appendChild(card);
937
+ });
938
+ }
939
+
940
+ function buildCard(plugin) {
941
+ const installed = installedMap[plugin.name] ?? [];
942
+ const isInstalled = installed.length > 0;
943
+ const targets = Array.isArray(plugin.targets) ? plugin.targets : [];
944
+ const cat = inferCategory(plugin);
945
+
946
+ const article = document.createElement('article');
947
+ article.className = 'plugin-card';
948
+ article.setAttribute('role', 'listitem');
949
+ article.setAttribute('aria-label', `${plugin.name} extension`);
950
+ article.dataset.pluginName = plugin.name;
951
+
952
+ const installedPlatforms = installed.map(e => e.platform).join(', ');
953
+
954
+ article.innerHTML = `
955
+ <div class="card-header">
956
+ <div class="card-title-group">
957
+ <h3 class="card-name" title="${escHtml(plugin.name)}">${escHtml(plugin.name)}</h3>
958
+ <span class="card-version">v${escHtml(plugin.version ?? '0.0.0')}</span>
959
+ </div>
960
+ </div>
961
+
962
+ <div class="card-meta">
963
+ <span class="badge-category ${categoryClass(cat)}" aria-label="Category: ${escHtml(cat)}">${escHtml(cat)}</span>
964
+ ${targets.map(t => `<span class="chip-target ${escHtml(t.toLowerCase())}" aria-label="Supports ${escHtml(t)}">${escHtml(t)}</span>`).join('')}
965
+ </div>
966
+
967
+ <p class="card-description" title="${escHtml(plugin.description ?? '')}">${escHtml(plugin.description ?? 'No description available.')}</p>
968
+
969
+ <div class="installed-indicator${isInstalled ? ' visible' : ''}" aria-live="polite" id="installed-indicator-${escHtml(plugin.name)}">
970
+ <svg class="icon-check" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
971
+ <span>Installed</span>
972
+ <span class="installed-platforms" aria-label="Installed platforms">${escHtml(installedPlatforms)}</span>
973
+ </div>
974
+
975
+ <div class="card-controls">
976
+ <div class="controls-row">
977
+ <div class="ctrl-select">
978
+ <label for="platform-${escHtml(plugin.name)}">Platform</label>
979
+ <select id="platform-${escHtml(plugin.name)}" name="platform" aria-label="Select installation platform for ${escHtml(plugin.name)}">
980
+ <option value="codex">codex</option>
981
+ <option value="claude">claude</option>
982
+ <option value="cursor">cursor</option>
983
+ <option value="enact">enact</option>
984
+ <option value="shared">shared</option>
985
+ <option value="all">all</option>
986
+ </select>
987
+ </div>
988
+ <div class="ctrl-scope">
989
+ <label id="scope-label-${escHtml(plugin.name)}">Scope</label>
990
+ <div class="scope-toggle" role="group" aria-labelledby="scope-label-${escHtml(plugin.name)}">
991
+ <button
992
+ type="button"
993
+ class="active"
994
+ data-scope="global"
995
+ aria-pressed="true"
996
+ aria-label="Global scope for ${escHtml(plugin.name)}"
997
+ >Global</button>
998
+ <button
999
+ type="button"
1000
+ data-scope="local"
1001
+ aria-pressed="false"
1002
+ aria-label="Local scope for ${escHtml(plugin.name)}"
1003
+ >Local</button>
1004
+ </div>
1005
+ </div>
1006
+ </div>
1007
+
1008
+ <div class="actions-row">
1009
+ <button
1010
+ type="button"
1011
+ class="btn btn-primary"
1012
+ data-action="install"
1013
+ data-name="${escHtml(plugin.name)}"
1014
+ aria-label="Install ${escHtml(plugin.name)}"
1015
+ aria-busy="false"
1016
+ >
1017
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1018
+ Install
1019
+ </button>
1020
+ <button
1021
+ type="button"
1022
+ class="btn btn-danger${isInstalled ? ' installed-btn' : ''}"
1023
+ data-action="uninstall"
1024
+ data-name="${escHtml(plugin.name)}"
1025
+ aria-label="Uninstall ${escHtml(plugin.name)}"
1026
+ aria-busy="false"
1027
+ >
1028
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
1029
+ Uninstall
1030
+ </button>
1031
+ </div>
1032
+ </div>
1033
+ `;
1034
+
1035
+ // Scope segmented toggle behaviour
1036
+ const scopeBtns = article.querySelectorAll('.scope-toggle button');
1037
+ scopeBtns.forEach(btn => {
1038
+ btn.addEventListener('click', () => {
1039
+ scopeBtns.forEach(b => {
1040
+ b.classList.remove('active');
1041
+ b.setAttribute('aria-pressed', 'false');
1042
+ });
1043
+ btn.classList.add('active');
1044
+ btn.setAttribute('aria-pressed', 'true');
1045
+ });
1046
+ });
1047
+
1048
+ // Install / Uninstall button click
1049
+ article.querySelectorAll('.btn[data-action]').forEach(btn => {
1050
+ btn.addEventListener('click', () => handleAction(btn, article, plugin));
1051
+ });
1052
+
1053
+ return article;
1054
+ }
1055
+
1056
+ // ─────────────────────────────────────────────────────────────────────────
1057
+ // Install / Uninstall action
1058
+ // ─────────────────────────────────────────────────────────────────────────
1059
+ async function handleAction(btn, card, plugin) {
1060
+ const action = btn.dataset.action; // 'install' | 'uninstall'
1061
+ const endpoint = action === 'install' ? '/api/install' : '/api/uninstall';
1062
+
1063
+ const platformSelect = card.querySelector(`#platform-${CSS.escape(plugin.name)}`);
1064
+ const platform = platformSelect?.value ?? 'codex';
1065
+
1066
+ const activeScope = card.querySelector('.scope-toggle button.active');
1067
+ const scope = activeScope?.dataset.scope ?? 'global';
1068
+
1069
+ // Disable both buttons + show spinner
1070
+ const allBtns = card.querySelectorAll('.btn[data-action]');
1071
+ allBtns.forEach(b => {
1072
+ b.disabled = true;
1073
+ b.setAttribute('aria-busy', 'true');
1074
+ });
1075
+
1076
+ const originalHtml = btn.innerHTML;
1077
+ // For install, provisioning MCP-server deps runs server-side after the
1078
+ // plugin files land; reflect that in the busy label.
1079
+ btn.innerHTML = `<span class="spinner" aria-hidden="true"></span> ${action === 'install' ? 'Installing &amp; provisioning deps…' : 'Removing…'}`;
1080
+
1081
+ try {
1082
+ const res = await fetch(endpoint, {
1083
+ method: 'POST',
1084
+ headers: { 'Content-Type': 'application/json' },
1085
+ body: JSON.stringify({ name: plugin.name, platform, scope }),
1086
+ });
1087
+
1088
+ const data = await res.json();
1089
+
1090
+ if (data.ok) {
1091
+ const label = action === 'install' ? 'Installed' : 'Removed';
1092
+ let msg = `Platform: ${platform} | Scope: ${scope}`;
1093
+ // Surface any MCP dependency provisioning that ran during install.
1094
+ if (action === 'install' && Array.isArray(data.provision)) {
1095
+ msg += provisionSummary(data.provision);
1096
+ }
1097
+ showToast('success', `${label}: ${plugin.name}`, msg);
1098
+
1099
+ // Refresh installed state for this card
1100
+ try {
1101
+ installedMap = await fetchInstalled();
1102
+ updateCardInstalledState(card, plugin.name);
1103
+ } catch {/* non-critical */}
1104
+ } else {
1105
+ showToast('error', `${action === 'install' ? 'Install' : 'Uninstall'} failed`, data.error ?? 'Unknown error');
1106
+ }
1107
+ } catch (err) {
1108
+ showToast('error', 'Network error', err.message ?? String(err));
1109
+ } finally {
1110
+ btn.innerHTML = originalHtml;
1111
+ allBtns.forEach(b => {
1112
+ b.disabled = false;
1113
+ b.setAttribute('aria-busy', 'false');
1114
+ });
1115
+ }
1116
+ }
1117
+
1118
+ // Update a single card's installed indicator + uninstall button emphasis
1119
+ function updateCardInstalledState(card, name) {
1120
+ const installed = installedMap[name] ?? [];
1121
+ const isInstalled = installed.length > 0;
1122
+ const installedPlatforms = installed.map(e => e.platform).join(', ');
1123
+
1124
+ const indicator = card.querySelector(`#installed-indicator-${CSS.escape(name)}`);
1125
+ if (indicator) {
1126
+ indicator.classList.toggle('visible', isInstalled);
1127
+ const platformSpan = indicator.querySelector('.installed-platforms');
1128
+ if (platformSpan) platformSpan.textContent = installedPlatforms;
1129
+ }
1130
+
1131
+ const uninstallBtn = card.querySelector('[data-action="uninstall"]');
1132
+ if (uninstallBtn) {
1133
+ uninstallBtn.classList.toggle('installed-btn', isInstalled);
1134
+ }
1135
+ }
1136
+
1137
+ // ─────────────────────────────────────────────────────────────────────────
1138
+ // Initialise
1139
+ // ─────────────────────────────────────────────────────────────────────────
1140
+ async function init() {
1141
+ const grid = document.getElementById('plugins-grid');
1142
+ const countBadge = document.getElementById('plugin-count');
1143
+
1144
+ try {
1145
+ // Parallel fetch
1146
+ const [indexData, installed] = await Promise.all([
1147
+ fetchIndex(),
1148
+ fetchInstalled().catch(() => ({})),
1149
+ ]);
1150
+
1151
+ installedMap = installed;
1152
+ allPlugins = Array.isArray(indexData.plugins) ? indexData.plugins : [];
1153
+
1154
+ countBadge.textContent = `${allPlugins.length} plugin${allPlugins.length !== 1 ? 's' : ''}`;
1155
+
1156
+ if (allPlugins.length === 0) {
1157
+ grid.setAttribute('aria-busy', 'false');
1158
+ grid.innerHTML = `
1159
+ <div class="empty-state" style="grid-column: 1/-1;">
1160
+ <div class="empty-state-icon" aria-hidden="true">📦</div>
1161
+ <h2>No plugins found</h2>
1162
+ <p>No extensions are registered yet.</p>
1163
+ </div>
1164
+ `;
1165
+ return;
1166
+ }
1167
+
1168
+ renderFilters(allPlugins);
1169
+ renderCards(allPlugins);
1170
+
1171
+ } catch (err) {
1172
+ grid.setAttribute('aria-busy', 'false');
1173
+ grid.innerHTML = `
1174
+ <div class="empty-state" style="grid-column: 1/-1;">
1175
+ <div class="empty-state-icon" aria-hidden="true">⚠️</div>
1176
+ <h2>Failed to load extensions</h2>
1177
+ <p>${escHtml(err.message ?? String(err))}</p>
1178
+ </div>
1179
+ `;
1180
+ countBadge.textContent = '0 plugins';
1181
+ showToast('error', 'Load error', err.message ?? String(err));
1182
+ }
1183
+ }
1184
+
1185
+ init();
1186
+ </script>
1187
+ </body>
1188
+ </html>