@chrysb/alphaclaw 0.4.6-beta.8 → 0.4.6-beta.9
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/bin/alphaclaw.js +2 -32
- package/lib/public/css/theme.css +19 -0
- package/lib/public/js/app.js +1 -1
- package/lib/public/js/components/envars.js +0 -1
- package/lib/public/js/components/onboarding/welcome-config.js +39 -17
- package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
- package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
- package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
- package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
- package/lib/public/js/components/segmented-control.js +7 -1
- package/lib/public/js/components/welcome/index.js +112 -0
- package/lib/public/js/components/welcome/use-welcome.js +561 -0
- package/lib/public/js/lib/api.js +221 -161
- package/lib/server/commands.js +1 -0
- package/lib/server/constants.js +0 -1
- package/lib/server/gateway.js +15 -40
- package/lib/server/onboarding/github.js +120 -19
- package/lib/server/onboarding/import/import-applier.js +321 -0
- package/lib/server/onboarding/import/import-config.js +69 -0
- package/lib/server/onboarding/import/import-scanner.js +469 -0
- package/lib/server/onboarding/import/import-temp.js +63 -0
- package/lib/server/onboarding/import/secret-detector.js +289 -0
- package/lib/server/onboarding/index.js +256 -29
- package/lib/server/onboarding/workspace.js +38 -6
- package/lib/server/routes/onboarding.js +281 -12
- package/lib/server.js +11 -2
- package/package.json +1 -1
- package/lib/public/js/components/welcome.js +0 -318
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import { LoadingSpinner } from "../loading-spinner.js";
|
|
6
|
+
|
|
7
|
+
const html = htm.bind(h);
|
|
8
|
+
|
|
9
|
+
const kCategories = [
|
|
10
|
+
{
|
|
11
|
+
key: "gatewayConfig",
|
|
12
|
+
label: "Gateway Config",
|
|
13
|
+
icon: "⚙️",
|
|
14
|
+
description: "openclaw.json configuration",
|
|
15
|
+
showFiles: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "envFiles",
|
|
19
|
+
label: "Environment Files",
|
|
20
|
+
icon: "🔐",
|
|
21
|
+
description: ".env files with variables",
|
|
22
|
+
showFiles: true,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "workspaceFiles",
|
|
26
|
+
label: "Workspace Files",
|
|
27
|
+
icon: "📄",
|
|
28
|
+
description: "Prompt files (AGENTS.md, SOUL.md, etc.)",
|
|
29
|
+
showFiles: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "skills",
|
|
33
|
+
label: "Skills",
|
|
34
|
+
icon: "🛠",
|
|
35
|
+
description: "Custom skill definitions",
|
|
36
|
+
showFiles: true,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "cronJobs",
|
|
40
|
+
label: "Cron Jobs",
|
|
41
|
+
icon: "⏰",
|
|
42
|
+
description: "Scheduled tasks",
|
|
43
|
+
showFiles: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "webhooks",
|
|
47
|
+
label: "Hooks",
|
|
48
|
+
icon: "🔗",
|
|
49
|
+
description: "Webhook mappings and internal hooks",
|
|
50
|
+
showDirs: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
key: "memory",
|
|
54
|
+
label: "Memory",
|
|
55
|
+
icon: "🧠",
|
|
56
|
+
description: "Agent memory and embeddings",
|
|
57
|
+
showDirs: true,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const CategoryCard = ({ category, data }) => {
|
|
62
|
+
const [expanded, setExpanded] = useState(false);
|
|
63
|
+
if (!data?.found) return null;
|
|
64
|
+
const isHooksCategory = category.key === "webhooks";
|
|
65
|
+
const warningItems = Array.isArray(data.transformWarnings)
|
|
66
|
+
? data.transformWarnings
|
|
67
|
+
: [];
|
|
68
|
+
const warningPathPrefixes = new Set(
|
|
69
|
+
warningItems
|
|
70
|
+
.map((warning) => String(warning.actualPath || "").trim())
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.map((pathValue) => pathValue.split("/").slice(0, -2).join("/")),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const items = [
|
|
76
|
+
...(data.jobNames || []),
|
|
77
|
+
...(data.hookNames || []),
|
|
78
|
+
...(data.files || []),
|
|
79
|
+
...(data.dirs || []).filter((dir) => !warningPathPrefixes.has(dir)),
|
|
80
|
+
...(data.extraMarkdown || []),
|
|
81
|
+
];
|
|
82
|
+
const count =
|
|
83
|
+
typeof data.jobCount === "number" && data.jobCount > 0
|
|
84
|
+
? data.jobCount
|
|
85
|
+
: typeof data.hookCount === "number" && data.hookCount > 0
|
|
86
|
+
? data.hookCount
|
|
87
|
+
: items.length;
|
|
88
|
+
const warningCount =
|
|
89
|
+
typeof data.warningCount === "number"
|
|
90
|
+
? data.warningCount
|
|
91
|
+
: warningItems.length;
|
|
92
|
+
|
|
93
|
+
return html`
|
|
94
|
+
<div class="border border-border rounded-lg p-3">
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onclick=${() => setExpanded((p) => !p)}
|
|
98
|
+
class="w-full flex items-center justify-between text-left"
|
|
99
|
+
>
|
|
100
|
+
<div class="flex items-center gap-2">
|
|
101
|
+
<span class="text-sm">${category.icon}</span>
|
|
102
|
+
<span class="text-xs font-medium text-gray-200"
|
|
103
|
+
>${category.label}</span
|
|
104
|
+
>
|
|
105
|
+
<span
|
|
106
|
+
class="text-xs px-1.5 py-0.5 rounded-full bg-cyan-900/40 text-cyan-300"
|
|
107
|
+
>${count}</span
|
|
108
|
+
>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="flex items-center gap-2">
|
|
111
|
+
${warningCount > 0
|
|
112
|
+
? html`
|
|
113
|
+
<span
|
|
114
|
+
class="text-xs px-1.5 py-0.5 rounded-full bg-yellow-900/30 text-yellow-300"
|
|
115
|
+
>
|
|
116
|
+
⚠ ${warningCount}
|
|
117
|
+
</span>
|
|
118
|
+
`
|
|
119
|
+
: null}
|
|
120
|
+
<span class="text-xs text-gray-500">${expanded ? "▲" : "▼"}</span>
|
|
121
|
+
</div>
|
|
122
|
+
</button>
|
|
123
|
+
${expanded &&
|
|
124
|
+
items.length > 0 &&
|
|
125
|
+
html`
|
|
126
|
+
<div class="mt-2 space-y-1">
|
|
127
|
+
${items.map(
|
|
128
|
+
(item) => html`
|
|
129
|
+
<div
|
|
130
|
+
class="text-xs font-mono bg-black/20 rounded px-2 py-1 text-gray-500"
|
|
131
|
+
>
|
|
132
|
+
${item}
|
|
133
|
+
</div>
|
|
134
|
+
`,
|
|
135
|
+
)}
|
|
136
|
+
${isHooksCategory
|
|
137
|
+
? warningItems.map(
|
|
138
|
+
(warning) => html`
|
|
139
|
+
<div
|
|
140
|
+
class="text-xs font-mono bg-black/20 rounded px-2 py-1 text-yellow-300"
|
|
141
|
+
>
|
|
142
|
+
${warning.actualPath}
|
|
143
|
+
</div>
|
|
144
|
+
`,
|
|
145
|
+
)
|
|
146
|
+
: null}
|
|
147
|
+
</div>
|
|
148
|
+
`}
|
|
149
|
+
</div>
|
|
150
|
+
`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const WelcomeImportStep = ({
|
|
154
|
+
scanResult,
|
|
155
|
+
scanning,
|
|
156
|
+
error,
|
|
157
|
+
onApprove,
|
|
158
|
+
onShowSecretReview,
|
|
159
|
+
onBack,
|
|
160
|
+
}) => {
|
|
161
|
+
if (scanning) {
|
|
162
|
+
return html`
|
|
163
|
+
<div class="flex flex-col items-center justify-center py-8 gap-3">
|
|
164
|
+
<${LoadingSpinner} />
|
|
165
|
+
<p class="text-sm text-gray-400">Scanning repository...</p>
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (error) {
|
|
171
|
+
return html`
|
|
172
|
+
<div class="space-y-3">
|
|
173
|
+
<div
|
|
174
|
+
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
175
|
+
>
|
|
176
|
+
${error}
|
|
177
|
+
</div>
|
|
178
|
+
<button
|
|
179
|
+
onclick=${onBack}
|
|
180
|
+
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
|
|
181
|
+
>
|
|
182
|
+
Back
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!scanResult) return null;
|
|
189
|
+
|
|
190
|
+
const secretCount = (scanResult.secrets || []).length;
|
|
191
|
+
const hasConflicts = scanResult.managedConflicts?.found;
|
|
192
|
+
|
|
193
|
+
return html`
|
|
194
|
+
<div class="space-y-3">
|
|
195
|
+
<div>
|
|
196
|
+
<h2 class="text-sm font-medium text-gray-200">Import Summary</h2>
|
|
197
|
+
<p class="text-xs text-gray-500">
|
|
198
|
+
${scanResult.hasOpenclawSetup
|
|
199
|
+
? "Found an existing OpenClaw setup"
|
|
200
|
+
: "No OpenClaw config detected — we'll set up fresh after import"}
|
|
201
|
+
</p>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="space-y-2">
|
|
205
|
+
${kCategories.map(
|
|
206
|
+
(cat) => html`
|
|
207
|
+
<${CategoryCard}
|
|
208
|
+
key=${cat.key}
|
|
209
|
+
category=${cat}
|
|
210
|
+
data=${scanResult[cat.key]}
|
|
211
|
+
/>
|
|
212
|
+
`,
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
${scanResult.credentials?.found &&
|
|
217
|
+
html`
|
|
218
|
+
<div
|
|
219
|
+
class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
|
|
220
|
+
>
|
|
221
|
+
Deployment-specific files found (credentials, device identity) — these
|
|
222
|
+
will not be imported.
|
|
223
|
+
</div>
|
|
224
|
+
`}
|
|
225
|
+
${hasConflicts &&
|
|
226
|
+
html`
|
|
227
|
+
<div
|
|
228
|
+
class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
|
|
229
|
+
>
|
|
230
|
+
AlphaClaw-managed files detected
|
|
231
|
+
(${(scanResult.managedConflicts.files || []).join(", ")}). These will
|
|
232
|
+
be overwritten with AlphaClaw defaults.
|
|
233
|
+
</div>
|
|
234
|
+
`}
|
|
235
|
+
${scanResult.managedEnvConflicts?.found
|
|
236
|
+
? html`
|
|
237
|
+
<div
|
|
238
|
+
class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
|
|
239
|
+
>
|
|
240
|
+
AlphaClaw controls deployment env vars
|
|
241
|
+
(${(scanResult.managedEnvConflicts.vars || []).join(", ")}).
|
|
242
|
+
Imported values for these will be normalized during import.
|
|
243
|
+
</div>
|
|
244
|
+
`
|
|
245
|
+
: null}
|
|
246
|
+
${scanResult.webhooks?.warningCount > 0
|
|
247
|
+
? html`
|
|
248
|
+
<div
|
|
249
|
+
class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
|
|
250
|
+
>
|
|
251
|
+
AlphaClaw expects hook transforms at
|
|
252
|
+
<code class="text-xs bg-black/30 px-1 rounded"
|
|
253
|
+
>hooks/transforms/name/name-transform.mjs</code
|
|
254
|
+
>. We found some that do not match and will try to patch them
|
|
255
|
+
during import. The originals will be backed up under
|
|
256
|
+
<code class="text-xs bg-black/30 px-1 rounded"
|
|
257
|
+
>hooks/transforms/_backup</code
|
|
258
|
+
>.
|
|
259
|
+
</div>
|
|
260
|
+
`
|
|
261
|
+
: null}
|
|
262
|
+
${secretCount > 0 &&
|
|
263
|
+
html`
|
|
264
|
+
<div
|
|
265
|
+
class="bg-cyan-900/20 border border-cyan-800/50 rounded-lg p-3 flex items-center justify-between"
|
|
266
|
+
>
|
|
267
|
+
<div>
|
|
268
|
+
<span class="text-xs text-cyan-300 font-medium">
|
|
269
|
+
${secretCount} possible secret${secretCount === 1 ? "" : "s"}
|
|
270
|
+
detected
|
|
271
|
+
</span>
|
|
272
|
+
<p class="text-xs text-gray-500 mt-0.5">
|
|
273
|
+
Review and extract to environment variables
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
<${ActionButton}
|
|
277
|
+
onClick=${onShowSecretReview}
|
|
278
|
+
tone="primary"
|
|
279
|
+
size="sm"
|
|
280
|
+
idleLabel="Review"
|
|
281
|
+
className="font-medium"
|
|
282
|
+
/>
|
|
283
|
+
</div>
|
|
284
|
+
`}
|
|
285
|
+
|
|
286
|
+
<div class="grid grid-cols-2 gap-2 pt-1">
|
|
287
|
+
<${ActionButton}
|
|
288
|
+
onClick=${onBack}
|
|
289
|
+
tone="secondary"
|
|
290
|
+
size="md"
|
|
291
|
+
idleLabel="Back"
|
|
292
|
+
className="w-full"
|
|
293
|
+
/>
|
|
294
|
+
<${ActionButton}
|
|
295
|
+
onClick=${() => onApprove([])}
|
|
296
|
+
loading=${scanning}
|
|
297
|
+
tone="primary"
|
|
298
|
+
size="md"
|
|
299
|
+
idleLabel="Import"
|
|
300
|
+
loadingLabel="Importing..."
|
|
301
|
+
className="w-full"
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
`;
|
|
306
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useMemo } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import { SecretInput } from "../secret-input.js";
|
|
6
|
+
|
|
7
|
+
const html = htm.bind(h);
|
|
8
|
+
|
|
9
|
+
const isResolvedValue = (value) => {
|
|
10
|
+
const normalized = String(value || "").trim();
|
|
11
|
+
return !!normalized && normalized !== "placeholder";
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const PlaceholderRow = ({ item, value, onInput }) => {
|
|
15
|
+
return html`
|
|
16
|
+
<div class="border border-border rounded-lg p-3 space-y-2">
|
|
17
|
+
<div class="flex items-start justify-between gap-3">
|
|
18
|
+
<div class="min-w-0">
|
|
19
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
20
|
+
<code
|
|
21
|
+
class="text-xs text-gray-200 bg-black/30 px-1.5 py-0.5 rounded"
|
|
22
|
+
>${item.key}</code
|
|
23
|
+
>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<${SecretInput}
|
|
28
|
+
value=${value}
|
|
29
|
+
onInput=${(event) => onInput(event.target.value)}
|
|
30
|
+
placeholder="Enter value"
|
|
31
|
+
inputClass="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WelcomePlaceholderReviewStep = ({
|
|
38
|
+
placeholderReview,
|
|
39
|
+
vals,
|
|
40
|
+
setValue,
|
|
41
|
+
onContinue,
|
|
42
|
+
}) => {
|
|
43
|
+
const items = Array.isArray(placeholderReview?.vars)
|
|
44
|
+
? placeholderReview.vars
|
|
45
|
+
: [];
|
|
46
|
+
const unresolvedItems = useMemo(
|
|
47
|
+
() =>
|
|
48
|
+
items
|
|
49
|
+
.filter((item) => !isResolvedValue(vals[item.key]))
|
|
50
|
+
.map((item) => item.key),
|
|
51
|
+
[items, vals],
|
|
52
|
+
);
|
|
53
|
+
const unresolvedCount = unresolvedItems.length;
|
|
54
|
+
|
|
55
|
+
if (items.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
return html`
|
|
58
|
+
<div class="space-y-3">
|
|
59
|
+
<div>
|
|
60
|
+
<h2 class="text-sm font-medium text-gray-200">Add Missing Env Vars</h2>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="space-y-2 max-h-80 overflow-y-auto">
|
|
64
|
+
${items.map(
|
|
65
|
+
(item) => html`
|
|
66
|
+
<${PlaceholderRow}
|
|
67
|
+
key=${item.key}
|
|
68
|
+
item=${item}
|
|
69
|
+
value=${String(vals[item.key] || "") === "placeholder"
|
|
70
|
+
? ""
|
|
71
|
+
: vals[item.key] || ""}
|
|
72
|
+
onInput=${(nextValue) => setValue(item.key, nextValue)}
|
|
73
|
+
/>
|
|
74
|
+
`,
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div
|
|
79
|
+
class="bg-yellow-900/20 border border-yellow-800/50 rounded-lg p-3 text-xs text-yellow-300"
|
|
80
|
+
>
|
|
81
|
+
${unresolvedCount > 0
|
|
82
|
+
? `${unresolvedCount} detected env var${unresolvedCount === 1 ? "" : "s"} need values. You can continue without them, but the gateway might fail to start.`
|
|
83
|
+
: "All imported placeholder env vars have values now."}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="pt-1">
|
|
87
|
+
<${ActionButton}
|
|
88
|
+
onClick=${onContinue}
|
|
89
|
+
tone="primary"
|
|
90
|
+
size="md"
|
|
91
|
+
idleLabel=${unresolvedCount > 0
|
|
92
|
+
? `Continue with ${unresolvedCount} Unresolved`
|
|
93
|
+
: "Continue"}
|
|
94
|
+
className="w-full"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState, useCallback } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import { LoadingSpinner } from "../loading-spinner.js";
|
|
6
|
+
|
|
7
|
+
const html = htm.bind(h);
|
|
8
|
+
|
|
9
|
+
const SecretRow = ({ secret, selected, onToggle, envVarName, onEnvVarChange }) =>
|
|
10
|
+
html`
|
|
11
|
+
<div
|
|
12
|
+
class="border border-border rounded-lg p-3 space-y-2 ${selected
|
|
13
|
+
? "bg-cyan-900/10 border-cyan-800/40"
|
|
14
|
+
: ""}"
|
|
15
|
+
>
|
|
16
|
+
<div class="flex items-start gap-2">
|
|
17
|
+
<input
|
|
18
|
+
type="checkbox"
|
|
19
|
+
checked=${selected}
|
|
20
|
+
onChange=${onToggle}
|
|
21
|
+
class="mt-0.5 rounded"
|
|
22
|
+
/>
|
|
23
|
+
<div class="flex-1 min-w-0">
|
|
24
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
25
|
+
<span class="text-xs font-mono text-gray-300 truncate"
|
|
26
|
+
>${secret.maskedValue}</span
|
|
27
|
+
>
|
|
28
|
+
${secret.confidence === "high"
|
|
29
|
+
? html`<span
|
|
30
|
+
class="text-xs px-1.5 py-0.5 rounded-full bg-red-900/40 text-red-300"
|
|
31
|
+
>high confidence</span
|
|
32
|
+
>`
|
|
33
|
+
: html`<span
|
|
34
|
+
class="text-xs px-1.5 py-0.5 rounded-full bg-yellow-900/40 text-yellow-300"
|
|
35
|
+
>possible</span
|
|
36
|
+
>`}
|
|
37
|
+
</div>
|
|
38
|
+
<div class="text-xs text-gray-500 mt-1">
|
|
39
|
+
Found in${" "}
|
|
40
|
+
<span class="font-mono">${secret.file || "config"}</span>
|
|
41
|
+
${secret.configPath
|
|
42
|
+
? html` at <span class="font-mono">${secret.configPath}</span>`
|
|
43
|
+
: null}
|
|
44
|
+
</div>
|
|
45
|
+
${secret.duplicateIn &&
|
|
46
|
+
html`
|
|
47
|
+
<div class="text-xs text-yellow-400 mt-1">
|
|
48
|
+
Also found in${" "}<span class="font-mono"
|
|
49
|
+
>${secret.duplicateIn}</span
|
|
50
|
+
>
|
|
51
|
+
</div>
|
|
52
|
+
`}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
${selected &&
|
|
56
|
+
html`
|
|
57
|
+
<div class="pl-6">
|
|
58
|
+
<label class="text-xs text-gray-500">Extract as env var:</label>
|
|
59
|
+
<input
|
|
60
|
+
type="text"
|
|
61
|
+
value=${envVarName}
|
|
62
|
+
onInput=${(e) => onEnvVarChange(e.target.value)}
|
|
63
|
+
class="w-full mt-1 bg-black/30 border border-border rounded-lg px-3 py-1.5 text-xs text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
`}
|
|
67
|
+
</div>
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
export const WelcomeSecretReviewStep = ({
|
|
71
|
+
secrets = [],
|
|
72
|
+
onApprove,
|
|
73
|
+
onBack,
|
|
74
|
+
loading,
|
|
75
|
+
error,
|
|
76
|
+
}) => {
|
|
77
|
+
const [selections, setSelections] = useState(() => {
|
|
78
|
+
const initial = {};
|
|
79
|
+
for (const secret of secrets) {
|
|
80
|
+
initial[secret.configPath] = {
|
|
81
|
+
selected: secret.confidence === "high",
|
|
82
|
+
envVarName: secret.suggestedEnvVar || "",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return initial;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const toggleSecret = useCallback(
|
|
89
|
+
(configPath) => {
|
|
90
|
+
setSelections((prev) => ({
|
|
91
|
+
...prev,
|
|
92
|
+
[configPath]: {
|
|
93
|
+
...prev[configPath],
|
|
94
|
+
selected: !prev[configPath]?.selected,
|
|
95
|
+
},
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
98
|
+
[],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const updateEnvVarName = useCallback(
|
|
102
|
+
(configPath, name) => {
|
|
103
|
+
setSelections((prev) => ({
|
|
104
|
+
...prev,
|
|
105
|
+
[configPath]: {
|
|
106
|
+
...prev[configPath],
|
|
107
|
+
envVarName: name,
|
|
108
|
+
},
|
|
109
|
+
}));
|
|
110
|
+
},
|
|
111
|
+
[],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const selectedCount = Object.values(selections).filter(
|
|
115
|
+
(s) => s.selected,
|
|
116
|
+
).length;
|
|
117
|
+
|
|
118
|
+
const handleExtract = () => {
|
|
119
|
+
const approved = secrets
|
|
120
|
+
.filter((s) => selections[s.configPath]?.selected)
|
|
121
|
+
.map((s) => ({
|
|
122
|
+
...s,
|
|
123
|
+
suggestedEnvVar:
|
|
124
|
+
selections[s.configPath]?.envVarName || s.suggestedEnvVar,
|
|
125
|
+
}));
|
|
126
|
+
onApprove(approved);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (loading) {
|
|
130
|
+
return html`
|
|
131
|
+
<div class="flex flex-col items-center justify-center py-8 gap-3">
|
|
132
|
+
<${LoadingSpinner} />
|
|
133
|
+
<p class="text-sm text-gray-400">Applying import...</p>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return html`
|
|
139
|
+
<div class="space-y-3">
|
|
140
|
+
<div>
|
|
141
|
+
<h2 class="text-sm font-medium text-gray-200">Review Secrets</h2>
|
|
142
|
+
<p class="text-xs text-gray-500">
|
|
143
|
+
Select secrets to extract into environment variables. Inline values in
|
|
144
|
+
config will be replaced with ${"`"}${"${"}ENV_VAR_NAME${"}"}${"`"} references.
|
|
145
|
+
</p>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
${error &&
|
|
149
|
+
html`
|
|
150
|
+
<div
|
|
151
|
+
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
152
|
+
>
|
|
153
|
+
${error}
|
|
154
|
+
</div>
|
|
155
|
+
`}
|
|
156
|
+
|
|
157
|
+
<div class="space-y-2 max-h-80 overflow-y-auto">
|
|
158
|
+
${secrets.map(
|
|
159
|
+
(secret) => html`
|
|
160
|
+
<${SecretRow}
|
|
161
|
+
key=${secret.configPath}
|
|
162
|
+
secret=${secret}
|
|
163
|
+
selected=${selections[secret.configPath]?.selected || false}
|
|
164
|
+
envVarName=${selections[secret.configPath]?.envVarName || ""}
|
|
165
|
+
onToggle=${() => toggleSecret(secret.configPath)}
|
|
166
|
+
onEnvVarChange=${(name) =>
|
|
167
|
+
updateEnvVarName(secret.configPath, name)}
|
|
168
|
+
/>
|
|
169
|
+
`,
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div class="grid grid-cols-2 gap-2 pt-1">
|
|
174
|
+
<${ActionButton}
|
|
175
|
+
onClick=${onBack}
|
|
176
|
+
tone="secondary"
|
|
177
|
+
idleLabel="Back"
|
|
178
|
+
className="w-full"
|
|
179
|
+
/>
|
|
180
|
+
<${ActionButton}
|
|
181
|
+
onClick=${handleExtract}
|
|
182
|
+
tone="primary"
|
|
183
|
+
idleLabel=${selectedCount > 0
|
|
184
|
+
? `Extract ${selectedCount} Secret${selectedCount === 1 ? "" : "s"}`
|
|
185
|
+
: "Skip All"}
|
|
186
|
+
className="w-full"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
};
|
|
@@ -11,14 +11,20 @@ const html = htm.bind(h);
|
|
|
11
11
|
* @param {*} props.value Currently selected value.
|
|
12
12
|
* @param {Function} props.onChange Called with the new value on click.
|
|
13
13
|
* @param {string} [props.className] Extra classes on the wrapper.
|
|
14
|
+
* @param {"sm"|"lg"} [props.size] Visual size variant.
|
|
15
|
+
* @param {boolean} [props.fullWidth] Stretch wrapper and options to 100%.
|
|
14
16
|
*/
|
|
15
17
|
export const SegmentedControl = ({
|
|
16
18
|
options = [],
|
|
17
19
|
value,
|
|
18
20
|
onChange = () => {},
|
|
19
21
|
className = "",
|
|
22
|
+
size = "sm",
|
|
23
|
+
fullWidth = false,
|
|
20
24
|
}) => html`
|
|
21
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
class=${`ac-segmented-control ${size === "lg" ? "ac-segmented-control-lg" : ""} ${fullWidth ? "ac-segmented-control-full" : ""} ${className}`.trim()}
|
|
27
|
+
>
|
|
22
28
|
${options.map(
|
|
23
29
|
(option) => html`
|
|
24
30
|
<button
|