@chrysb/alphaclaw 0.4.6-beta.7 → 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/doctor/helpers.js +71 -5
- package/lib/public/js/components/doctor/index.js +89 -28
- 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/doctor/bootstrap-context.js +191 -0
- package/lib/server/doctor/prompt.js +20 -4
- package/lib/server/doctor/service.js +18 -4
- 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 +12 -3
- package/package.json +1 -1
- package/lib/public/js/components/welcome.js +0 -318
|
@@ -4,7 +4,14 @@ import htm from "https://esm.sh/htm";
|
|
|
4
4
|
import { SecretInput } from "../secret-input.js";
|
|
5
5
|
import { ActionButton } from "../action-button.js";
|
|
6
6
|
import { Badge } from "../badge.js";
|
|
7
|
-
import {
|
|
7
|
+
import { SegmentedControl } from "../segmented-control.js";
|
|
8
|
+
import {
|
|
9
|
+
isValidGithubRepoInput,
|
|
10
|
+
kGithubFlowFresh,
|
|
11
|
+
kGithubFlowImport,
|
|
12
|
+
kGithubTargetRepoModeCreate,
|
|
13
|
+
kGithubTargetRepoModeExistingEmpty,
|
|
14
|
+
} from "./welcome-config.js";
|
|
8
15
|
|
|
9
16
|
const html = htm.bind(h);
|
|
10
17
|
|
|
@@ -42,14 +49,16 @@ export const WelcomeFormStep = ({
|
|
|
42
49
|
allValid,
|
|
43
50
|
handleSubmit,
|
|
44
51
|
}) => {
|
|
45
|
-
const [repoTouched, setRepoTouched] = useState(false);
|
|
46
52
|
const [showOptionalOpenai, setShowOptionalOpenai] = useState(false);
|
|
47
53
|
const [showOptionalGemini, setShowOptionalGemini] = useState(false);
|
|
54
|
+
const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
|
|
55
|
+
const freshRepoMode =
|
|
56
|
+
githubFlow === kGithubFlowImport
|
|
57
|
+
? kGithubTargetRepoModeCreate
|
|
58
|
+
: vals._GITHUB_TARGET_REPO_MODE || kGithubTargetRepoModeCreate;
|
|
48
59
|
|
|
49
60
|
useEffect(() => {
|
|
50
|
-
if (activeGroup.id !== "github")
|
|
51
|
-
setRepoTouched(false);
|
|
52
|
-
}
|
|
61
|
+
if (activeGroup.id !== "github") return;
|
|
53
62
|
}, [activeGroup.id]);
|
|
54
63
|
|
|
55
64
|
useEffect(() => {
|
|
@@ -183,9 +192,62 @@ export const WelcomeFormStep = ({
|
|
|
183
192
|
`}
|
|
184
193
|
</div>
|
|
185
194
|
`}
|
|
195
|
+
${activeGroup.id === "github" &&
|
|
196
|
+
html`
|
|
197
|
+
<div class="space-y-3">
|
|
198
|
+
<div class="space-y-1 pt-1">
|
|
199
|
+
<div>
|
|
200
|
+
<label class="text-xs font-medium text-gray-400">Setup mode</label>
|
|
201
|
+
</div>
|
|
202
|
+
<${SegmentedControl}
|
|
203
|
+
options=${[
|
|
204
|
+
{ label: "Start fresh", value: kGithubFlowFresh },
|
|
205
|
+
{ label: "Import existing setup", value: kGithubFlowImport },
|
|
206
|
+
]}
|
|
207
|
+
value=${githubFlow}
|
|
208
|
+
onChange=${(value) => setValue("_GITHUB_FLOW", value)}
|
|
209
|
+
size="md"
|
|
210
|
+
fullWidth=${true}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
${githubFlow === kGithubFlowFresh
|
|
214
|
+
? html`
|
|
215
|
+
<div class="space-y-1">
|
|
216
|
+
<div>
|
|
217
|
+
<label class="text-xs font-medium text-gray-400"
|
|
218
|
+
>Repository setup</label
|
|
219
|
+
>
|
|
220
|
+
</div>
|
|
221
|
+
<${SegmentedControl}
|
|
222
|
+
options=${[
|
|
223
|
+
{
|
|
224
|
+
label: "Create new repo",
|
|
225
|
+
value: kGithubTargetRepoModeCreate,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
label: "Use existing empty repo",
|
|
229
|
+
value: kGithubTargetRepoModeExistingEmpty,
|
|
230
|
+
},
|
|
231
|
+
]}
|
|
232
|
+
value=${freshRepoMode}
|
|
233
|
+
onChange=${(value) =>
|
|
234
|
+
setValue("_GITHUB_TARGET_REPO_MODE", value)}
|
|
235
|
+
fullWidth=${true}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
`
|
|
239
|
+
: null}
|
|
240
|
+
</div>
|
|
241
|
+
`}
|
|
186
242
|
${(activeGroup.id === "ai"
|
|
187
243
|
? activeGroup.fields.filter((field) => visibleAiFieldKeys.has(field.key))
|
|
188
|
-
: activeGroup.
|
|
244
|
+
: activeGroup.id === "github"
|
|
245
|
+
? activeGroup.fields.filter((field) =>
|
|
246
|
+
githubFlow === kGithubFlowImport
|
|
247
|
+
? true
|
|
248
|
+
: field.key !== "_GITHUB_SOURCE_REPO",
|
|
249
|
+
)
|
|
250
|
+
: activeGroup.fields
|
|
189
251
|
).map(
|
|
190
252
|
(field) => html`
|
|
191
253
|
<div class="space-y-1" key=${field.key}>
|
|
@@ -196,27 +258,51 @@ export const WelcomeFormStep = ({
|
|
|
196
258
|
key=${field.key}
|
|
197
259
|
value=${vals[field.key] || ""}
|
|
198
260
|
onInput=${(e) => setValue(field.key, e.target.value)}
|
|
199
|
-
onBlur=${field.key === "GITHUB_WORKSPACE_REPO"
|
|
200
|
-
? () => setRepoTouched(true)
|
|
201
|
-
: undefined}
|
|
202
261
|
placeholder=${field.placeholder || ""}
|
|
203
262
|
isSecret=${!field.isText}
|
|
204
263
|
inputClass="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 outline-none focus:border-gray-500 font-mono"
|
|
205
264
|
/>
|
|
206
|
-
<p class="text-xs text-gray-600"
|
|
265
|
+
<p class="text-xs text-gray-600">
|
|
266
|
+
${activeGroup.id === "github" &&
|
|
267
|
+
field.key === "GITHUB_WORKSPACE_REPO"
|
|
268
|
+
? githubFlow === kGithubFlowImport
|
|
269
|
+
? "Your new project will live here"
|
|
270
|
+
: freshRepoMode === kGithubTargetRepoModeExistingEmpty
|
|
271
|
+
? "Enter the owner/repo of an existing empty repository"
|
|
272
|
+
: "A new private repo will be created for you"
|
|
273
|
+
: activeGroup.id === "github" &&
|
|
274
|
+
field.key === "_GITHUB_SOURCE_REPO"
|
|
275
|
+
? "The repo to import from"
|
|
276
|
+
: activeGroup.id === "github" && field.key === "GITHUB_TOKEN"
|
|
277
|
+
? githubFlow === kGithubFlowImport
|
|
278
|
+
? freshRepoMode === kGithubTargetRepoModeCreate
|
|
279
|
+
? html`Use a classic PAT with${" "}<code
|
|
280
|
+
class="text-xs bg-black/30 px-1 rounded"
|
|
281
|
+
>repo</code
|
|
282
|
+
>${" "}scope to create the target repo. Fine-grained
|
|
283
|
+
works if the target already exists and can access both
|
|
284
|
+
repos.`
|
|
285
|
+
: html`Use a classic PAT with${" "}<code
|
|
286
|
+
class="text-xs bg-black/30 px-1 rounded"
|
|
287
|
+
>repo</code
|
|
288
|
+
>${" "}scope, or a fine-grained token with Contents +
|
|
289
|
+
Metadata access to both the source repo and target
|
|
290
|
+
repo`
|
|
291
|
+
: freshRepoMode === kGithubTargetRepoModeExistingEmpty
|
|
292
|
+
? html`Use a classic PAT with${" "}<code
|
|
293
|
+
class="text-xs bg-black/30 px-1 rounded"
|
|
294
|
+
>repo</code
|
|
295
|
+
>${" "}scope, or a fine-grained token with Contents +
|
|
296
|
+
Metadata access to this repo`
|
|
297
|
+
: html`Use a classic PAT with${" "}<code
|
|
298
|
+
class="text-xs bg-black/30 px-1 rounded"
|
|
299
|
+
>repo</code
|
|
300
|
+
>${" "}scope to create a new private repository`
|
|
301
|
+
: field.hint}
|
|
302
|
+
</p>
|
|
207
303
|
</div>
|
|
208
304
|
`,
|
|
209
305
|
)}
|
|
210
|
-
${activeGroup.id === "github" &&
|
|
211
|
-
repoTouched &&
|
|
212
|
-
vals.GITHUB_WORKSPACE_REPO &&
|
|
213
|
-
!isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
|
|
214
|
-
? html`<div class="text-xs text-red-300">
|
|
215
|
-
Workspace Repo must be in
|
|
216
|
-
<code class="text-xs bg-black/30 px-1 rounded">owner/repo</code>
|
|
217
|
-
format.
|
|
218
|
-
</div>`
|
|
219
|
-
: null}
|
|
220
306
|
${error
|
|
221
307
|
? html`<div
|
|
222
308
|
class="bg-red-900/30 border border-red-800 rounded-xl p-3 text-red-300 text-sm"
|
|
@@ -281,39 +367,48 @@ export const WelcomeFormStep = ({
|
|
|
281
367
|
${step < totalGroups - 1
|
|
282
368
|
? html`
|
|
283
369
|
${step > 0
|
|
284
|
-
? html
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
Back
|
|
289
|
-
|
|
370
|
+
? html`<${ActionButton}
|
|
371
|
+
onClick=${goBack}
|
|
372
|
+
tone="secondary"
|
|
373
|
+
size="md"
|
|
374
|
+
idleLabel="Back"
|
|
375
|
+
className="w-full"
|
|
376
|
+
/>`
|
|
290
377
|
: html`<div class="w-full"></div>`}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
disabled=${!currentGroupValid
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
378
|
+
<${ActionButton}
|
|
379
|
+
onClick=${goNext}
|
|
380
|
+
disabled=${!currentGroupValid}
|
|
381
|
+
loading=${activeGroup.id === "github" && githubStepLoading}
|
|
382
|
+
tone="primary"
|
|
383
|
+
size="md"
|
|
384
|
+
idleLabel=${activeGroup.id === "github" &&
|
|
385
|
+
githubFlow === kGithubFlowImport
|
|
386
|
+
? "Check compatibility"
|
|
298
387
|
: "Next"}
|
|
299
|
-
|
|
388
|
+
loadingLabel="Checking..."
|
|
389
|
+
className="w-full"
|
|
390
|
+
/>
|
|
300
391
|
`
|
|
301
392
|
: html`
|
|
302
393
|
${step > 0
|
|
303
|
-
? html
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
Back
|
|
308
|
-
|
|
394
|
+
? html`<${ActionButton}
|
|
395
|
+
onClick=${goBack}
|
|
396
|
+
tone="secondary"
|
|
397
|
+
size="md"
|
|
398
|
+
idleLabel="Back"
|
|
399
|
+
className="w-full"
|
|
400
|
+
/>`
|
|
309
401
|
: html`<div class="w-full"></div>`}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
disabled=${!allValid
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
402
|
+
<${ActionButton}
|
|
403
|
+
onClick=${handleSubmit}
|
|
404
|
+
disabled=${!allValid}
|
|
405
|
+
loading=${loading}
|
|
406
|
+
tone="primary"
|
|
407
|
+
size="md"
|
|
408
|
+
idleLabel="Next"
|
|
409
|
+
loadingLabel="Starting..."
|
|
410
|
+
className="w-full"
|
|
411
|
+
/>
|
|
317
412
|
`}
|
|
318
413
|
</div>
|
|
319
414
|
`;
|
|
@@ -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
|
+
};
|