hac-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +41 -0
- package/README.md +145 -0
- package/bin/hac-mcp.js +88 -0
- package/hac.js +320 -0
- package/package.json +53 -0
- package/server.js +276 -0
- package/static/app.js +650 -0
- package/static/index.html +211 -0
- package/static/style.css +282 -0
- package/storage.js +54 -0
- package/tools/context.js +107 -0
- package/tools/flexible_search.js +161 -0
- package/tools/get_type_info.js +188 -0
- package/tools/groovy_execute.js +60 -0
- package/tools/impex_import.js +180 -0
- package/tools/index.js +40 -0
- package/tools/list_cronjobs.js +74 -0
- package/tools/list_environments.js +25 -0
- package/tools/media_read.js +86 -0
- package/tools/media_write.js +122 -0
- package/tools/read_property.js +51 -0
- package/tools/resolve_pk.js +84 -0
- package/tools/run_cronjob.js +71 -0
- package/tools/search_type.js +40 -0
- package/tools/zodLoose.js +26 -0
- package/type-index.js +92 -0
|
@@ -0,0 +1,211 @@
|
|
|
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"/>
|
|
6
|
+
<title>HAC MCP</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/style.css"/>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="topbar">
|
|
11
|
+
<div>
|
|
12
|
+
<h1>HAC MCP</h1>
|
|
13
|
+
<p class="subtitle">Environment manager</p>
|
|
14
|
+
</div>
|
|
15
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
16
|
+
<button class="btn-theme" id="btnTheme" onclick="toggleTheme()" title="Toggle theme"></button>
|
|
17
|
+
<button class="btn-manifest" onclick="showManifest()">
|
|
18
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
|
|
19
|
+
View Tools Manifest
|
|
20
|
+
</button>
|
|
21
|
+
<button class="btn-setup" onclick="showModal()">
|
|
22
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
|
23
|
+
Setup Guide
|
|
24
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="opacity:.5"><path d="M9 18l6-6-6-6"/></svg>
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="layout">
|
|
30
|
+
<!-- Environments + logs -->
|
|
31
|
+
<div style="display:flex;flex-direction:column;gap:20px">
|
|
32
|
+
<div class="card">
|
|
33
|
+
<div class="card-header">
|
|
34
|
+
<h2>Environments</h2>
|
|
35
|
+
<button class="btn-primary" id="btnAddEnv" onclick="openForm()">+ Add Environment</button>
|
|
36
|
+
</div>
|
|
37
|
+
<div id="envList"><div class="empty">No environments configured yet.<br/>Click <strong>+ Add</strong> to connect your first HAC instance.</div></div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="card">
|
|
41
|
+
<div class="card-header">
|
|
42
|
+
<h2><span class="log-dot" id="logDot"></span>MCP Activity Log</h2>
|
|
43
|
+
<button class="btn-ghost btn-sm" onclick="clearLog('logList')">Clear</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div id="logList" class="log-list"><div class="empty" style="padding:20px">No activity yet.</div></div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="card">
|
|
49
|
+
<div class="card-header">
|
|
50
|
+
<h2><span class="log-dot" id="hacLogDot"></span>HAC Request Log</h2>
|
|
51
|
+
<button class="btn-ghost btn-sm" onclick="clearLog('hacLogList')">Clear</button>
|
|
52
|
+
</div>
|
|
53
|
+
<div id="hacLogList" class="hac-log-list"><div class="empty" style="padding:20px">No requests yet.</div></div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<!-- Status card -->
|
|
58
|
+
<div class="card" id="infoCard" style="align-self:start">
|
|
59
|
+
<div class="card-header">
|
|
60
|
+
<h2>Status</h2>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="card-body" style="padding:16px 18px;display:flex;flex-direction:column;gap:16px">
|
|
63
|
+
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
64
|
+
<div class="status-pill" id="pillEnvs"><span class="status-dot inactive"></span><span id="pillEnvsLabel">0 environments</span></div>
|
|
65
|
+
<div class="status-pill" id="pillClients"><span class="status-dot inactive" id="clientDot"></span><span id="pillClientsLabel">0 clients connected</span></div>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="clientList" style="display:none;flex-direction:column;gap:4px"></div>
|
|
68
|
+
<div id="infoSetup">
|
|
69
|
+
<div style="margin-bottom:12px">
|
|
70
|
+
<div class="info-label">MCP Endpoint</div>
|
|
71
|
+
<div class="copyable"><code class="info-code" id="infoEndpoint"></code><button class="copy-btn" onclick="copyEl('infoEndpoint')">Copy</button></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div style="margin-bottom:12px">
|
|
74
|
+
<div class="info-label">Claude Code</div>
|
|
75
|
+
<div class="copyable"><code class="info-code" id="infoClaudeCmd"></code><button class="copy-btn" onclick="copyEl('infoClaudeCmd')">Copy</button></div>
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<div class="info-label">Other MCP Clients</div>
|
|
79
|
+
<div class="copyable"><pre class="info-pre" id="infoJson"></pre><button class="copy-btn" onclick="copyEl('infoJson')">Copy</button></div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Add/Edit environment modal -->
|
|
87
|
+
<div class="modal-overlay" id="formOverlay" onclick="closeFormModal(event)">
|
|
88
|
+
<div class="modal modal-form">
|
|
89
|
+
<button class="modal-close" onclick="closeForm()">✕</button>
|
|
90
|
+
<div class="modal-header">
|
|
91
|
+
<div class="modal-logo" id="formTitle">Add Environment</div>
|
|
92
|
+
</div>
|
|
93
|
+
<input type="hidden" id="editId"/>
|
|
94
|
+
<label>Name <span class="required">*</span></label>
|
|
95
|
+
<input type="text" id="fName"/>
|
|
96
|
+
<div class="field-error" id="errName"></div>
|
|
97
|
+
<label>Description</label>
|
|
98
|
+
<input type="text" id="fDesc"/>
|
|
99
|
+
<label>HAC URL <span class="required">*</span></label>
|
|
100
|
+
<input type="url" id="fUrl"/>
|
|
101
|
+
<div id="urlTestStatus" style="display:none"></div>
|
|
102
|
+
<div class="field-error" id="errUrl"></div>
|
|
103
|
+
<label>Username <span class="required">*</span></label>
|
|
104
|
+
<input type="text" id="fUser" placeholder="admin"/>
|
|
105
|
+
<div class="field-error" id="errUser"></div>
|
|
106
|
+
<label>Password <span class="required">*</span></label>
|
|
107
|
+
<input type="password" id="fPass" placeholder="••••••••"/>
|
|
108
|
+
<div class="field-error" id="errPass"></div>
|
|
109
|
+
<label>DB Dialect</label>
|
|
110
|
+
<select id="fDbType">
|
|
111
|
+
<option value="MSSQL" selected>MSSQL</option>
|
|
112
|
+
<option value="MySQL">MySQL</option>
|
|
113
|
+
</select>
|
|
114
|
+
<div class="section-divider">Access Rights</div>
|
|
115
|
+
<div class="toggle-section-label">Read Only</div>
|
|
116
|
+
<div class="toggle-group">
|
|
117
|
+
<div class="toggle-item">
|
|
118
|
+
<div class="toggle-label">FlexibleSearch<span class="toggle-desc">Read-only queries</span></div>
|
|
119
|
+
<label class="toggle"><input type="checkbox" id="fFlex" checked/><span class="slider"></span></label>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="toggle-item">
|
|
122
|
+
<div class="toggle-label">Read Property<span class="toggle-desc">Read platform config</span></div>
|
|
123
|
+
<label class="toggle"><input type="checkbox" id="fReadProperty" checked/><span class="slider"></span></label>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="toggle-section-label" style="margin-top:12px">Mixed <span class="toggle-section-note"> - may write</span></div>
|
|
127
|
+
<div class="toggle-group">
|
|
128
|
+
<div class="toggle-item warning">
|
|
129
|
+
<div class="toggle-label">Groovy Execution<span class="toggle-desc">Runs arbitrary code</span></div>
|
|
130
|
+
<label class="toggle"><input type="checkbox" id="fGroovy" onchange="toggleGroovyCommit(this.checked)"/><span class="slider"></span></label>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="toggle-item danger" id="groovyCommitRow">
|
|
133
|
+
<div class="toggle-label">Groovy Commit Mode<span class="toggle-desc">Persists DB changes</span></div>
|
|
134
|
+
<label class="toggle"><input type="checkbox" id="fGroovyCommit" disabled onchange="updateGroovyNote()"/><span class="slider"></span></label>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="toggle-note" id="groovyNote" style="display:none">⚠ Even with Commit Mode off, Groovy scripts can still interact with external systems - sending emails, SMS, calling APIs, or triggering workflows.</div>
|
|
138
|
+
<div class="toggle-section-label" style="margin-top:12px">Write</div>
|
|
139
|
+
<div class="toggle-group">
|
|
140
|
+
<div class="toggle-item danger">
|
|
141
|
+
<div class="toggle-label">ImpEx Import<span class="toggle-desc">Imports & modifies data</span></div>
|
|
142
|
+
<label class="toggle"><input type="checkbox" id="fImpex"/><span class="slider"></span></label>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="form-actions">
|
|
146
|
+
<button class="btn-ghost" onclick="closeForm()">Cancel</button>
|
|
147
|
+
<button class="btn-primary" id="btnSave" onclick="saveEnv()">Save</button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Onboarding modal -->
|
|
153
|
+
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
|
|
154
|
+
<div class="modal">
|
|
155
|
+
<button class="modal-close" onclick="dismissModal()">✕</button>
|
|
156
|
+
<div class="modal-header">
|
|
157
|
+
<div class="modal-logo">HAC MCP</div>
|
|
158
|
+
<p class="modal-subtitle">SAP Commerce Cloud HAC - MCP Server</p>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="modal-steps">
|
|
162
|
+
<div class="modal-step done">
|
|
163
|
+
<div class="step-num">1</div>
|
|
164
|
+
<div class="step-body">
|
|
165
|
+
<div class="step-title">Server is running</div>
|
|
166
|
+
<div class="step-desc">HAC MCP is up and listening for connections.</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div class="modal-step">
|
|
171
|
+
<div class="step-num">2</div>
|
|
172
|
+
<div class="step-body">
|
|
173
|
+
<div class="step-title">Connect your MCP client</div>
|
|
174
|
+
<div class="step-desc">Run this in your terminal to register with Claude Code:</div>
|
|
175
|
+
<code class="modal-code" id="modalClaudeCmd"></code>
|
|
176
|
+
<div class="step-desc" style="margin-top:12px">Or add this to your MCP client config:</div>
|
|
177
|
+
<pre class="modal-pre" id="modalJson"></pre>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="modal-step">
|
|
182
|
+
<div class="step-num">3</div>
|
|
183
|
+
<div class="step-body">
|
|
184
|
+
<div class="step-title">Add your first environment</div>
|
|
185
|
+
<div class="step-desc">Connect a HAC instance (local, staging, or production).</div>
|
|
186
|
+
<button class="btn-primary btn-sm" style="margin-top:10px" onclick="dismissModal();openForm()">+ Add Environment</button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<button class="modal-dismiss" onclick="dismissModal()">Got it, don't show again</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<!-- Manifest modal -->
|
|
196
|
+
<div class="modal-overlay" id="manifestOverlay" onclick="closeManifestModal(event)">
|
|
197
|
+
<div class="modal modal-manifest">
|
|
198
|
+
<button class="modal-close" onclick="closeManifest()">✕</button>
|
|
199
|
+
<div class="modal-header" style="margin-bottom:20px">
|
|
200
|
+
<div class="modal-logo" id="manifestTitle">HAC MCP</div>
|
|
201
|
+
<p class="modal-subtitle" id="manifestSubtitle">SAP Commerce Cloud HAC - MCP Server</p>
|
|
202
|
+
</div>
|
|
203
|
+
<div id="manifestTools"></div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="toast" id="toast"></div>
|
|
208
|
+
<div class="global-tooltip" id="globalTooltip"></div>
|
|
209
|
+
<script src="/static/app.js"></script>
|
|
210
|
+
</body>
|
|
211
|
+
</html>
|
package/static/style.css
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
:root{
|
|
2
|
+
--bg:#0f1117;--bg2:#0a0e17;--bg3:#0d1520;
|
|
3
|
+
--surface:#1e2333;--surface2:#232a3e;--surface3:#1e293b;
|
|
4
|
+
--border:#2d3748;--border2:#1a2235;--border3:#151c2a;--border4:#334155;
|
|
5
|
+
--text:#e2e8f0;--text2:#94a3b8;--text3:#64748b;--text4:#475569;
|
|
6
|
+
--accent:#7dd3fc;--accent-bg:#0c1a2e;--accent-border:#1e3a5f;
|
|
7
|
+
--modal-overlay:rgba(0,0,0,.7);
|
|
8
|
+
}
|
|
9
|
+
[data-theme=light]{
|
|
10
|
+
--bg:#f1f5f9;--bg2:#e2e8f0;--bg3:#e8edf4;
|
|
11
|
+
--surface:#ffffff;--surface2:#f1f5f9;--surface3:#e2e8f0;
|
|
12
|
+
--border:#cbd5e1;--border2:#e2e8f0;--border3:#e2e8f0;--border4:#94a3b8;
|
|
13
|
+
--text:#0f172a;--text2:#334155;--text3:#475569;--text4:#64748b;
|
|
14
|
+
--accent:#0c4a6e;--accent-bg:#e0f2fe;--accent-border:#7dd3fc;
|
|
15
|
+
--modal-overlay:rgba(0,0,0,.4);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
19
|
+
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;padding:32px 24px;transition:background .2s,color .2s}
|
|
20
|
+
.topbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px}
|
|
21
|
+
h1{font-size:1.3rem;font-weight:700;color:var(--accent);margin-bottom:4px}
|
|
22
|
+
.subtitle{font-size:.8rem;color:var(--text3)}
|
|
23
|
+
.subtitle code{background:var(--surface);padding:2px 6px;border-radius:4px;color:var(--text2);font-size:.75rem}
|
|
24
|
+
|
|
25
|
+
.layout{display:grid;grid-template-columns:1fr 320px;gap:20px;align-items:start}
|
|
26
|
+
|
|
27
|
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:10px;overflow:hidden}
|
|
28
|
+
.card-header{padding:12px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
|
29
|
+
.card-header h2{font-size:.7rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text2);display:flex;align-items:center;gap:6px}
|
|
30
|
+
.card-body{padding:18px}
|
|
31
|
+
|
|
32
|
+
/* env list */
|
|
33
|
+
#envList{display:flex;flex-direction:column}
|
|
34
|
+
.env-item{padding:13px 18px;border-bottom:1px solid var(--border2);display:flex;align-items:center;gap:12px;transition:background .1s}
|
|
35
|
+
.env-item:last-child{border-bottom:none}
|
|
36
|
+
.env-item:hover{background:var(--surface2)}
|
|
37
|
+
.env-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:1px}
|
|
38
|
+
.env-dot.active{background:#4ade80}
|
|
39
|
+
.env-dot.inactive{background:var(--border4)}
|
|
40
|
+
.env-dot.testing{background:var(--accent);animation:pulse 0.8s infinite}
|
|
41
|
+
.env-info{flex:1;min-width:0}
|
|
42
|
+
.env-name{font-size:.875rem;font-weight:600}
|
|
43
|
+
.env-url{font-size:.72rem;color:var(--text3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:1px}
|
|
44
|
+
.env-desc{font-size:.72rem;color:var(--text2);margin-top:1px}
|
|
45
|
+
.env-badges{display:flex;gap:4px;margin-top:6px}
|
|
46
|
+
.badge{font-size:.6rem;font-weight:700;padding:2px 7px;border-radius:4px;letter-spacing:.04em}
|
|
47
|
+
.badge.on{background:#052e16;color:#4ade80;border:1px solid #166534}
|
|
48
|
+
[data-theme=light] .badge.on{background:#dcfce7;color:#15803d;border-color:#86efac}
|
|
49
|
+
.badge.off{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
50
|
+
[data-theme=light] .badge.off{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
|
51
|
+
.badge.db{background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
52
|
+
.env-actions{display:flex;gap:6px;flex-shrink:0;align-items:flex-start}
|
|
53
|
+
|
|
54
|
+
/* connection status */
|
|
55
|
+
.conn-status{display:inline-flex;align-items:center;gap:5px;font-size:.68rem;font-weight:600;padding:3px 8px;border-radius:4px;flex-shrink:0}
|
|
56
|
+
.conn-status.unknown{background:var(--surface3);color:var(--text3);border:1px solid var(--border)}
|
|
57
|
+
.conn-status.testing{background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
58
|
+
.conn-status.ok{background:#052e16;color:#4ade80;border:1px solid #166534}
|
|
59
|
+
.conn-status.err{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
60
|
+
[data-theme=light] .conn-status.ok{background:#dcfce7;color:#15803d;border-color:#86efac}
|
|
61
|
+
[data-theme=light] .conn-status.err{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
|
62
|
+
[data-theme=light] .env-dot.active{background:#16a34a}
|
|
63
|
+
[data-theme=light] .status-dot.active{background:#16a34a}
|
|
64
|
+
[data-theme=light] .log-dot.connected{background:#16a34a}
|
|
65
|
+
.conn-dot{width:6px;height:6px;border-radius:50%;background:currentColor}
|
|
66
|
+
.conn-status.testing .conn-dot{animation:pulse 0.8s infinite}
|
|
67
|
+
|
|
68
|
+
/* buttons */
|
|
69
|
+
button{cursor:pointer;border:none;border-radius:6px;font-size:.8rem;font-weight:600;transition:all .15s}
|
|
70
|
+
.btn-primary{background:#3b82f6;color:#fff;padding:8px 16px;font-size:.8rem}
|
|
71
|
+
.btn-primary:hover{background:#2563eb}
|
|
72
|
+
.btn-sm{padding:5px 10px;font-size:.72rem}
|
|
73
|
+
.btn-edit{background:var(--surface3);color:var(--text2);border:1px solid var(--border)}
|
|
74
|
+
.btn-edit:hover{background:var(--border);color:var(--text)}
|
|
75
|
+
.btn-del{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
76
|
+
.btn-del:hover{background:#450a0a}
|
|
77
|
+
[data-theme=light] .btn-del{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
|
78
|
+
[data-theme=light] .btn-del:hover{background:#fee2e2}
|
|
79
|
+
.btn-ghost{background:transparent;color:var(--text2);padding:8px 14px;border:1px solid var(--border)}
|
|
80
|
+
.btn-ghost:hover{color:var(--text);border-color:var(--text3)}
|
|
81
|
+
.btn-setup{display:inline-flex;align-items:center;gap:7px;background:var(--accent-bg);color:var(--accent);padding:8px 16px;border:1px solid var(--accent-border);border-radius:8px;font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s;letter-spacing:.01em}
|
|
82
|
+
.btn-setup:hover{background:var(--accent-border);color:var(--accent);border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.15)}
|
|
83
|
+
[data-theme=light] .btn-setup{background:#e0f2fe;color:#0c4a6e;border-color:#0369a1}
|
|
84
|
+
[data-theme=light] .btn-setup:hover{background:#bae6fd;border-color:#0284c7}
|
|
85
|
+
.btn-theme{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text2);cursor:pointer;transition:all .2s;flex-shrink:0}
|
|
86
|
+
.btn-theme:hover{background:var(--surface2);color:var(--text);border-color:var(--text3)}
|
|
87
|
+
button:disabled{opacity:.35;cursor:not-allowed}
|
|
88
|
+
|
|
89
|
+
/* form */
|
|
90
|
+
label{display:block;font-size:.72rem;color:var(--text2);margin-bottom:4px;margin-top:14px}
|
|
91
|
+
label:first-of-type{margin-top:0}
|
|
92
|
+
input[type=text],input[type=password],input[type=url],select{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);padding:8px 10px;font-size:.8rem;font-family:inherit;outline:none;transition:border-color .15s;appearance:none}
|
|
93
|
+
select{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='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px;cursor:pointer}
|
|
94
|
+
select option{background:var(--surface)}
|
|
95
|
+
input:focus,select:focus{border-color:#3b82f6}
|
|
96
|
+
input.input-err{border-color:#ef4444}
|
|
97
|
+
input.input-err:focus{border-color:#ef4444}
|
|
98
|
+
.field-error{font-size:.68rem;color:#f87171;margin-top:3px;display:none}
|
|
99
|
+
.required{color:#f87171;font-size:.75rem}
|
|
100
|
+
.url-status{font-size:.72rem;font-weight:600;padding:5px 10px;border-radius:0 0 6px 6px;margin-top:-1px;border-top:none}
|
|
101
|
+
.url-status.hint{background:var(--surface2);color:var(--text3);border:1px solid var(--border)}
|
|
102
|
+
.url-status.testing{background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
103
|
+
.url-status.ok{background:#052e16;color:#4ade80;border:1px solid #166534}
|
|
104
|
+
.url-status.warn{background:#1a1500;color:#ca8a04;border:1px solid #713f12}
|
|
105
|
+
.url-status.err{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
106
|
+
[data-theme=light] .url-status.ok{background:#dcfce7;color:#15803d;border-color:#86efac}
|
|
107
|
+
[data-theme=light] .url-status.warn{background:#fffbeb;color:#92400e;border-color:#fcd34d}
|
|
108
|
+
[data-theme=light] .url-status.err{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
|
109
|
+
.section-divider{display:flex;align-items:center;gap:10px;margin:20px 0 12px;font-size:.68rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text2)}.section-divider::before,.section-divider::after{content:'';flex:1;height:1px;background:var(--border)}.section-divider::before{display:none}
|
|
110
|
+
.toggle-section-label{font-size:.65rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text2);margin-top:14px;margin-bottom:4px}
|
|
111
|
+
.toggle-section-note{font-weight:400;letter-spacing:0;text-transform:none;color:var(--text2);font-size:.65rem}
|
|
112
|
+
.toggle-group{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
|
113
|
+
.toggle-item.disabled{opacity:.4;pointer-events:none}
|
|
114
|
+
.toggle-note{font-size:.7rem;color:#92400e;background:#fffbeb;border:1px solid #fcd34d;border-radius:6px;padding:8px 10px;margin-top:8px;line-height:1.5}
|
|
115
|
+
[data-theme=dark] .toggle-note{color:#ca8a04;background:#1a1500;border-color:#713f12}
|
|
116
|
+
.toggle-item{display:flex;align-items:center;justify-content:space-between;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:9px 12px}
|
|
117
|
+
.toggle-label{font-size:.75rem;color:var(--text2);display:flex;flex-direction:column;gap:2px}
|
|
118
|
+
.toggle-desc{font-size:.65rem;color:var(--text3);font-weight:400}
|
|
119
|
+
.toggle-item.danger .toggle-label{color:var(--text2)}
|
|
120
|
+
.toggle-item.danger input:checked+.slider{background:#7f1d1d;border-color:#ef4444}
|
|
121
|
+
.toggle-item.danger input:checked+.slider:before{background:#fca5a5}
|
|
122
|
+
.toggle-item.warning .toggle-label{color:var(--text2)}
|
|
123
|
+
.toggle-item.warning input:checked+.slider{background:#713f12;border-color:#ca8a04}
|
|
124
|
+
.toggle-item.warning input:checked+.slider:before{background:#fde68a}
|
|
125
|
+
.toggle{position:relative;width:34px;height:18px;flex-shrink:0}
|
|
126
|
+
.toggle input{opacity:0;width:0;height:0;position:absolute}
|
|
127
|
+
.slider{position:absolute;inset:0;background:var(--surface3);border:1px solid var(--border4);border-radius:9px;cursor:pointer;transition:.2s}
|
|
128
|
+
.slider:before{content:'';position:absolute;width:12px;height:12px;left:2px;top:2px;background:var(--text4);border-radius:50%;transition:.2s}
|
|
129
|
+
.toggle input:checked+.slider{background:#1d4ed8;border-color:#3b82f6}
|
|
130
|
+
.toggle input:checked+.slider:before{transform:translateX(16px);background:#fff}
|
|
131
|
+
.form-actions{display:flex;gap:8px;margin-top:20px}
|
|
132
|
+
.form-actions button{flex:1;padding:9px}
|
|
133
|
+
|
|
134
|
+
/* MCP log */
|
|
135
|
+
.log-dot{width:7px;height:7px;border-radius:50%;background:var(--border4);display:inline-block;flex-shrink:0}
|
|
136
|
+
.log-dot.connected{background:#4ade80;animation:pulse 2s infinite}
|
|
137
|
+
[data-theme=light] .log-dot.connected{background:#16a34a}
|
|
138
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
139
|
+
@keyframes btn-glow{0%,100%{box-shadow:0 0 0 0 rgba(59,130,246,.6),0 0 12px rgba(59,130,246,.3)}60%{box-shadow:0 0 0 8px rgba(59,130,246,0),0 0 20px rgba(59,130,246,.1)}}
|
|
140
|
+
.btn-pulse{animation:btn-glow 1.4s ease-in-out infinite;background:#2563eb}
|
|
141
|
+
|
|
142
|
+
.log-list{max-height:400px;overflow-y:auto;padding:6px 0}
|
|
143
|
+
.log-entry{border-bottom:1px solid var(--border3);font-size:.75rem}
|
|
144
|
+
.log-entry:last-child{border-bottom:none}
|
|
145
|
+
.log-summary{display:flex;align-items:center;gap:8px;padding:9px 16px;cursor:pointer;user-select:none;transition:background .1s}
|
|
146
|
+
.log-summary:hover{background:var(--surface2)}
|
|
147
|
+
.log-tool{font-weight:700;font-size:.65rem;padding:2px 7px;border-radius:4px;letter-spacing:.04em;flex-shrink:0}
|
|
148
|
+
.log-tool.flex{background:#0c1a2e;color:#7dd3fc;border:1px solid #1e3a5f}
|
|
149
|
+
.log-tool.impex{background:#1a0a2e;color:#c084fc;border:1px solid #4c1d95}
|
|
150
|
+
.log-tool.list{background:#0a1a10;color:#6ee7b7;border:1px solid #065f46}
|
|
151
|
+
.log-tool.err{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
152
|
+
[data-theme=light] .log-tool.err{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
|
153
|
+
.log-client{font-size:.65rem;font-weight:600;color:var(--text3);flex-shrink:0}
|
|
154
|
+
.log-env{color:var(--text3);flex-shrink:0}
|
|
155
|
+
.log-summary-system{opacity:.65}
|
|
156
|
+
.log-sys-dot{width:5px;height:5px;border-radius:50%;background:var(--border4);flex-shrink:0;margin-right:2px}
|
|
157
|
+
.log-preview{color:var(--text2);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
158
|
+
.log-time{color:var(--text3);font-size:.65rem;flex-shrink:0}
|
|
159
|
+
.log-chevron{color:var(--text3);font-size:.65rem;transition:transform .15s;flex-shrink:0}
|
|
160
|
+
.log-entry.open .log-chevron{transform:rotate(90deg)}
|
|
161
|
+
.log-detail{display:none;padding:10px 16px 12px 16px;background:var(--bg2);border-top:1px solid var(--border2)}
|
|
162
|
+
.log-entry.open .log-detail{display:block}
|
|
163
|
+
.log-detail pre{font-family:'JetBrains Mono','Fira Code',monospace;font-size:.72rem;color:var(--text2);white-space:pre-wrap;word-break:break-all;line-height:1.5}
|
|
164
|
+
.log-entry.running{background:var(--bg3)}
|
|
165
|
+
.log-entry.running .log-summary{opacity:.85}
|
|
166
|
+
.log-entry.running .log-chevron{animation:spin .8s linear infinite;display:inline-block}
|
|
167
|
+
@keyframes spin{to{transform:rotate(360deg)}}
|
|
168
|
+
|
|
169
|
+
/* HAC request log */
|
|
170
|
+
.hac-log-list{max-height:260px;overflow-y:auto;padding:6px 0;font-family:'JetBrains Mono','Fira Code',monospace;font-size:.72rem}
|
|
171
|
+
.hac-log-line{display:flex;gap:8px;padding:2px 14px;line-height:1.6}
|
|
172
|
+
.hac-log-line:hover{background:var(--surface2)}
|
|
173
|
+
.hac-ts{color:var(--text3);flex-shrink:0;min-width:52px}
|
|
174
|
+
.hac-badge{flex-shrink:0;font-weight:700;font-size:.62rem;width:36px;text-align:right}
|
|
175
|
+
.hac-log-line.http .hac-badge{color:#60a5fa}
|
|
176
|
+
.hac-log-line.info .hac-badge{color:var(--text3)}
|
|
177
|
+
.hac-log-line.ok .hac-badge{color:#4ade80}
|
|
178
|
+
.hac-log-line.error .hac-badge{color:#f87171}
|
|
179
|
+
[data-theme=light] .hac-log-line.http .hac-badge{color:#0369a1}
|
|
180
|
+
[data-theme=light] .hac-log-line.ok .hac-badge{color:#15803d}
|
|
181
|
+
[data-theme=light] .hac-log-line.error .hac-badge{color:#dc2626}
|
|
182
|
+
.hac-msg{flex:1;word-break:break-all}
|
|
183
|
+
.hac-log-line.http .hac-msg{color:#93c5fd}
|
|
184
|
+
.hac-log-line.info .hac-msg{color:var(--text2)}
|
|
185
|
+
.hac-log-line.ok .hac-msg{color:#86efac}
|
|
186
|
+
.hac-log-line.error .hac-msg{color:#fca5a5}
|
|
187
|
+
[data-theme=light] .hac-log-line.http .hac-msg{color:#0369a1}
|
|
188
|
+
[data-theme=light] .hac-log-line.ok .hac-msg{color:#15803d}
|
|
189
|
+
[data-theme=light] .hac-log-line.error .hac-msg{color:#dc2626}
|
|
190
|
+
|
|
191
|
+
.empty{padding:32px;text-align:center;color:var(--text3);font-size:.8rem;line-height:1.6}
|
|
192
|
+
|
|
193
|
+
/* info card */
|
|
194
|
+
.status-pill{display:inline-flex;align-items:center;gap:6px;font-size:.72rem;font-weight:600;padding:4px 10px;border-radius:6px;background:var(--bg);border:1px solid var(--border);color:var(--text3)}
|
|
195
|
+
.status-pill.active{background:#052e16;border-color:#166534;color:#4ade80}
|
|
196
|
+
[data-theme=light] .status-pill.active{background:#dcfce7;border-color:#86efac;color:#15803d}
|
|
197
|
+
.status-dot{width:6px;height:6px;border-radius:50%;background:var(--border4);flex-shrink:0}
|
|
198
|
+
.status-dot.active{background:#4ade80}
|
|
199
|
+
.info-label{font-size:.68rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--text2);margin-bottom:6px}
|
|
200
|
+
.copyable{position:relative}
|
|
201
|
+
.copyable .info-code,.copyable .info-pre{padding-right:56px}
|
|
202
|
+
.copy-btn{position:absolute;top:0;right:0;background:var(--surface3);border:0;border-left:1px solid var(--border);border-bottom:1px solid var(--border);color:var(--text2);border-radius:0 6px 0 4px;font-size:.65rem;font-weight:600;padding:4px 9px;cursor:pointer;transition:all .15s}
|
|
203
|
+
.copy-btn:hover{background:var(--border);color:var(--text)}
|
|
204
|
+
.info-code{display:block;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;font-size:.72rem;color:var(--accent);word-break:break-all;font-family:'JetBrains Mono','Fira Code',monospace}
|
|
205
|
+
.info-pre{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-size:.72rem;color:var(--text2);font-family:'JetBrains Mono','Fira Code',monospace;line-height:1.6;white-space:pre;overflow-x:auto}
|
|
206
|
+
|
|
207
|
+
/* onboarding modal */
|
|
208
|
+
.modal-overlay{position:fixed;inset:0;background:var(--modal-overlay);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:200;opacity:0;pointer-events:none;transition:opacity .2s}
|
|
209
|
+
.modal-overlay.visible{opacity:1;pointer-events:all}
|
|
210
|
+
.modal{background:var(--surface);border:1px solid var(--border);border-radius:14px;width:100%;max-width:540px;padding:28px;position:relative;max-height:90vh;overflow-y:auto;transform:translateY(12px);transition:transform .2s}
|
|
211
|
+
.modal.modal-form{padding:24px}
|
|
212
|
+
.modal-overlay.visible .modal{transform:translateY(0)}
|
|
213
|
+
.modal-close{position:absolute;top:16px;right:16px;background:transparent;border:none;color:var(--text3);font-size:.9rem;cursor:pointer;padding:4px 8px;border-radius:4px;transition:color .15s}
|
|
214
|
+
.modal-close:hover{color:var(--text)}
|
|
215
|
+
.modal-header{margin-bottom:28px}
|
|
216
|
+
.modal-logo{font-size:1.4rem;font-weight:800;color:var(--accent);margin-bottom:4px}
|
|
217
|
+
.modal-subtitle{font-size:.8rem;color:var(--text2)}
|
|
218
|
+
.modal-steps{display:flex;flex-direction:column;gap:0}
|
|
219
|
+
.modal-step{display:flex;gap:16px;padding:20px 0;border-bottom:1px solid var(--border)}
|
|
220
|
+
.modal-step:last-child{border-bottom:none}
|
|
221
|
+
.step-num{width:26px;height:26px;border-radius:50%;background:var(--bg);border:1px solid var(--border4);color:var(--text3);font-size:.72rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:1px}
|
|
222
|
+
.modal-step.done .step-num{background:#052e16;border-color:#166534;color:#4ade80}
|
|
223
|
+
[data-theme=light] .modal-step.done .step-num{background:#dcfce7;border-color:#86efac;color:#15803d}
|
|
224
|
+
.step-body{flex:1;min-width:0}
|
|
225
|
+
.step-title{font-size:.875rem;font-weight:700;color:var(--text);margin-bottom:4px}
|
|
226
|
+
.modal-step.done .step-title{color:#4ade80}
|
|
227
|
+
[data-theme=light] .modal-step.done .step-title{color:#15803d}
|
|
228
|
+
.step-desc{font-size:.78rem;color:var(--text2);line-height:1.5;margin-bottom:0}
|
|
229
|
+
.modal-code{display:block;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;font-size:.72rem;color:var(--accent);word-break:break-all;font-family:'JetBrains Mono','Fira Code',monospace;margin-top:8px}
|
|
230
|
+
.modal-pre{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px 12px;font-size:.72rem;color:var(--text2);font-family:'JetBrains Mono','Fira Code',monospace;line-height:1.6;white-space:pre;margin-top:8px}
|
|
231
|
+
.modal-dismiss{margin-top:24px;width:100%;padding:10px;background:transparent;border:1px solid var(--border);border-radius:8px;color:var(--text3);font-size:.78rem;cursor:pointer;transition:all .15s}
|
|
232
|
+
.modal-dismiss:hover{border-color:var(--text3);color:var(--text2)}
|
|
233
|
+
|
|
234
|
+
/* manifest button */
|
|
235
|
+
.btn-manifest{display:inline-flex;align-items:center;gap:7px;background:var(--surface);color:var(--text2);padding:8px 16px;border:1px solid var(--border);border-radius:8px;font-size:.8rem;font-weight:600;cursor:pointer;transition:all .2s}
|
|
236
|
+
.btn-manifest:hover{background:var(--surface2);color:var(--text);border-color:var(--text3)}
|
|
237
|
+
|
|
238
|
+
/* manifest modal */
|
|
239
|
+
.modal-manifest{max-width:640px}
|
|
240
|
+
.manifest-section{margin-bottom:24px}
|
|
241
|
+
.manifest-section:last-child{margin-bottom:0}
|
|
242
|
+
.manifest-section-title{font-size:.65rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--text3);margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--border2)}
|
|
243
|
+
.manifest-tool{padding:12px 14px;background:var(--bg);border:1px solid var(--border);border-radius:8px;margin-bottom:8px}
|
|
244
|
+
.manifest-tool:last-child{margin-bottom:0}
|
|
245
|
+
.manifest-tool-header{display:flex;align-items:center;gap:10px;margin-bottom:6px}
|
|
246
|
+
.manifest-tool-name{font-size:.78rem;font-weight:700;color:var(--accent);background:var(--accent-bg);border:1px solid var(--accent-border);padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono','Fira Code',monospace}
|
|
247
|
+
.manifest-cat{font-size:.6rem;font-weight:700;padding:2px 7px;border-radius:4px;letter-spacing:.04em;text-transform:uppercase}
|
|
248
|
+
.cat-read{background:#0c1a2e;color:#7dd3fc;border:1px solid #1e3a5f}
|
|
249
|
+
.cat-write{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
250
|
+
.cat-util{background:#0a1a10;color:#6ee7b7;border:1px solid #065f46}
|
|
251
|
+
[data-theme=light] .cat-read{background:#e0f2fe;color:#0369a1;border-color:#7dd3fc}
|
|
252
|
+
[data-theme=light] .cat-write{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
|
253
|
+
[data-theme=light] .cat-util{background:#dcfce7;color:#15803d;border-color:#86efac}
|
|
254
|
+
.manifest-tool-desc{font-size:.78rem;color:var(--text2);line-height:1.55}
|
|
255
|
+
.manifest-params{display:flex;flex-direction:column;gap:5px;margin-top:8px}
|
|
256
|
+
.manifest-param{display:flex;align-items:baseline;gap:10px;background:var(--bg2);border:1px solid var(--border);border-radius:6px;padding:5px 10px}
|
|
257
|
+
.manifest-param-name{font-size:.68rem;font-family:'JetBrains Mono','Fira Code',monospace;color:var(--accent);flex-shrink:0}
|
|
258
|
+
.manifest-param-desc{font-size:.7rem;color:var(--text3);line-height:1.4}
|
|
259
|
+
|
|
260
|
+
/* client cards */
|
|
261
|
+
.client-card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:5px}
|
|
262
|
+
.client-num{font-size:.6rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#4ade80}
|
|
263
|
+
[data-theme=light] .client-num{color:#15803d}
|
|
264
|
+
.client-card-header{display:flex;align-items:center;gap:8px}
|
|
265
|
+
.client-name{font-size:.8rem;font-weight:700;color:var(--text)}
|
|
266
|
+
.client-version{font-size:.65rem;font-weight:700;padding:2px 7px;border-radius:4px;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border);font-family:'JetBrains Mono','Fira Code',monospace}
|
|
267
|
+
.client-card-meta{display:flex;align-items:center;gap:8px}
|
|
268
|
+
.client-card-meta .client-since{margin-left:auto}
|
|
269
|
+
.client-calls{font-size:.6rem;font-weight:700;padding:2px 7px;border-radius:4px;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border);position:relative;cursor:help;user-select:none}
|
|
270
|
+
.client-calls-hint{margin-left:4px;font-size:.55rem;opacity:.7;cursor:default}
|
|
271
|
+
.global-tooltip{position:fixed;background:var(--surface);color:var(--text);border:1px solid var(--border4);border-radius:5px;padding:5px 8px;font-size:.72rem;font-weight:400;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity .15s;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,.3)}
|
|
272
|
+
.client-since{font-size:.65rem;color:var(--text3)}
|
|
273
|
+
.client-desc{font-size:.72rem;color:var(--text2);line-height:1.45}
|
|
274
|
+
.client-link{font-size:.68rem;color:var(--text3);text-decoration:none;transition:color .15s}
|
|
275
|
+
.client-link:hover{color:var(--accent)}
|
|
276
|
+
/* toast */
|
|
277
|
+
.toast{position:fixed;bottom:24px;right:24px;padding:10px 16px;border-radius:8px;font-size:.8rem;font-weight:600;opacity:0;transform:translateY(6px);transition:all .2s;pointer-events:none;z-index:100}
|
|
278
|
+
.toast.show{opacity:1;transform:translateY(0)}
|
|
279
|
+
.toast.ok{background:#052e16;color:#4ade80;border:1px solid #166534}
|
|
280
|
+
.toast.err{background:#2d0a0a;color:#f87171;border:1px solid #7f1d1d}
|
|
281
|
+
[data-theme=light] .toast.ok{background:#dcfce7;color:#15803d;border-color:#86efac}
|
|
282
|
+
[data-theme=light] .toast.err{background:#fef2f2;color:#dc2626;border-color:#fca5a5}
|
package/storage.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
|
|
7
|
+
const DATA_DIR = join(homedir(), '.hac-mcp');
|
|
8
|
+
const FILE = join(DATA_DIR, 'environments.json');
|
|
9
|
+
|
|
10
|
+
async function ensureDataDir() {
|
|
11
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function load() {
|
|
15
|
+
if (!existsSync(FILE)) return [];
|
|
16
|
+
return JSON.parse(await readFile(FILE, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function save(envs) {
|
|
20
|
+
await ensureDataDir();
|
|
21
|
+
await writeFile(FILE, JSON.stringify(envs, null, 2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function listEnvironments() {
|
|
25
|
+
return load();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getEnvironment(id) {
|
|
29
|
+
return (await load()).find(e => e.id === id) ?? null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createEnvironment(data) {
|
|
33
|
+
const envs = await load();
|
|
34
|
+
let id;
|
|
35
|
+
do { id = randomBytes(4).toString('hex'); } while (envs.some(e => e.id === id));
|
|
36
|
+
const env = { id, ...data };
|
|
37
|
+
envs.push(env);
|
|
38
|
+
await save(envs);
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function updateEnvironment(id, data) {
|
|
43
|
+
const envs = await load();
|
|
44
|
+
const i = envs.findIndex(e => e.id === id);
|
|
45
|
+
if (i === -1) throw new Error('Environment not found');
|
|
46
|
+
envs[i] = { ...envs[i], ...data };
|
|
47
|
+
await save(envs);
|
|
48
|
+
return envs[i];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function deleteEnvironment(id) {
|
|
52
|
+
const envs = await load();
|
|
53
|
+
await save(envs.filter(e => e.id !== id));
|
|
54
|
+
}
|