@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.
Files changed (28) hide show
  1. package/bin/alphaclaw.js +2 -32
  2. package/lib/public/css/theme.css +19 -0
  3. package/lib/public/js/app.js +1 -1
  4. package/lib/public/js/components/envars.js +0 -1
  5. package/lib/public/js/components/onboarding/welcome-config.js +39 -17
  6. package/lib/public/js/components/onboarding/welcome-form-step.js +142 -47
  7. package/lib/public/js/components/onboarding/welcome-import-step.js +306 -0
  8. package/lib/public/js/components/onboarding/welcome-placeholder-review-step.js +99 -0
  9. package/lib/public/js/components/onboarding/welcome-secret-review-step.js +191 -0
  10. package/lib/public/js/components/segmented-control.js +7 -1
  11. package/lib/public/js/components/welcome/index.js +112 -0
  12. package/lib/public/js/components/welcome/use-welcome.js +561 -0
  13. package/lib/public/js/lib/api.js +221 -161
  14. package/lib/server/commands.js +1 -0
  15. package/lib/server/constants.js +0 -1
  16. package/lib/server/gateway.js +15 -40
  17. package/lib/server/onboarding/github.js +120 -19
  18. package/lib/server/onboarding/import/import-applier.js +321 -0
  19. package/lib/server/onboarding/import/import-config.js +69 -0
  20. package/lib/server/onboarding/import/import-scanner.js +469 -0
  21. package/lib/server/onboarding/import/import-temp.js +63 -0
  22. package/lib/server/onboarding/import/secret-detector.js +289 -0
  23. package/lib/server/onboarding/index.js +256 -29
  24. package/lib/server/onboarding/workspace.js +38 -6
  25. package/lib/server/routes/onboarding.js +281 -12
  26. package/lib/server.js +11 -2
  27. package/package.json +1 -1
  28. 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 class=${`ac-segmented-control ${className}`}>
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