@chrysb/alphaclaw 0.4.6-beta.8 → 0.5.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.
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
package/bin/alphaclaw.js CHANGED
@@ -216,21 +216,7 @@ try {
216
216
  }
217
217
 
218
218
  // ---------------------------------------------------------------------------
219
- // 5. Symlink <rootDir>/.openclaw/.env -> <rootDir>/.env
220
- // ---------------------------------------------------------------------------
221
-
222
- const openclawEnvLink = path.join(openclawDir, ".env");
223
- try {
224
- if (!fs.existsSync(openclawEnvLink)) {
225
- fs.symlinkSync(envFilePath, openclawEnvLink);
226
- console.log(`[alphaclaw] Symlinked ${openclawEnvLink} -> ${envFilePath}`);
227
- }
228
- } catch (e) {
229
- console.log(`[alphaclaw] .env symlink skipped: ${e.message}`);
230
- }
231
-
232
- // ---------------------------------------------------------------------------
233
- // 6. Load .env into process.env
219
+ // 5. Load .env into process.env
234
220
  // ---------------------------------------------------------------------------
235
221
 
236
222
  if (fs.existsSync(envFilePath)) {
@@ -530,23 +516,7 @@ if (!gogInstalled) {
530
516
  }
531
517
 
532
518
  // ---------------------------------------------------------------------------
533
- // 7. Configure gog keyring (file backend for headless environments)
534
- // ---------------------------------------------------------------------------
535
-
536
- process.env.GOG_KEYRING_PASSWORD =
537
- process.env.GOG_KEYRING_PASSWORD || "alphaclaw";
538
- const gogConfigFile = path.join(openclawDir, "gogcli", "config.json");
539
-
540
- if (!fs.existsSync(gogConfigFile)) {
541
- fs.mkdirSync(path.join(openclawDir, "gogcli"), { recursive: true });
542
- try {
543
- execSync("gog auth keyring file", { stdio: "ignore" });
544
- console.log("[alphaclaw] gog keyring configured (file backend)");
545
- } catch {}
546
- }
547
-
548
- // ---------------------------------------------------------------------------
549
- // 8. Install/reconcile system cron entry
519
+ // 7. Install/reconcile system cron entry
550
520
  // ---------------------------------------------------------------------------
551
521
 
552
522
  const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
@@ -513,6 +513,11 @@ textarea:focus {
513
513
  height: 28px;
514
514
  }
515
515
 
516
+ .ac-segmented-control-full {
517
+ display: flex;
518
+ width: 100%;
519
+ }
520
+
516
521
  .ac-segmented-control-button {
517
522
  border: 0;
518
523
  background: transparent;
@@ -527,6 +532,20 @@ textarea:focus {
527
532
  transition: color 0.12s, background 0.12s;
528
533
  }
529
534
 
535
+ .ac-segmented-control-full .ac-segmented-control-button {
536
+ flex: 1 1 0%;
537
+ }
538
+
539
+ .ac-segmented-control-lg {
540
+ height: 36px;
541
+ border-radius: 12px;
542
+ }
543
+
544
+ .ac-segmented-control-lg .ac-segmented-control-button {
545
+ font-size: 14px;
546
+ padding: 0 16px;
547
+ }
548
+
530
549
  .ac-segmented-control-button:hover {
531
550
  color: var(--text);
532
551
  background: rgba(255, 255, 255, 0.03);
@@ -3,7 +3,7 @@ import { useState, useEffect } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { Router, Route, Switch, useLocation } from "https://esm.sh/wouter-preact";
5
5
  import { logout } from "./lib/api.js";
6
- import { Welcome } from "./components/welcome.js";
6
+ import { Welcome } from "./components/welcome/index.js";
7
7
  import { ToastContainer } from "./components/toast.js";
8
8
  import { GlobalRestartBanner } from "./components/global-restart-banner.js";
9
9
  import { LoadingSpinner } from "./components/loading-spinner.js";
@@ -86,7 +86,6 @@ const kHintByKey = {
86
86
  OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">platform.openai.com</a>`,
87
87
  GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">aistudio.google.com</a>`,
88
88
  ELEVENLABS_API_KEY: html`from <a href="https://elevenlabs.io" target="_blank" class="hover:underline" style="color: var(--accent-link)">elevenlabs.io</a> · <code class="text-xs bg-black/30 px-1 rounded">XI_API_KEY</code> also supported`,
89
- GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: var(--accent-link)">github settings</a>`,
90
89
  GITHUB_WORKSPACE_REPO: html`use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
91
90
  TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: var(--accent-link)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
92
91
  DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: var(--accent-link)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
@@ -4,6 +4,13 @@ import { kAllAiAuthFields } from "../../lib/model-config.js";
4
4
 
5
5
  const html = htm.bind(h);
6
6
 
7
+ export const kRepoModeNew = "new";
8
+ export const kRepoModeExisting = "existing";
9
+ export const kGithubFlowFresh = "fresh";
10
+ export const kGithubFlowImport = "import";
11
+ export const kGithubTargetRepoModeCreate = "create";
12
+ export const kGithubTargetRepoModeExistingEmpty = "existing-empty";
13
+
7
14
  export const normalizeGithubRepoInput = (repoInput) =>
8
15
  String(repoInput || "")
9
16
  .trim()
@@ -19,44 +26,59 @@ export const isValidGithubRepoInput = (repoInput) => {
19
26
  };
20
27
 
21
28
  export const kWelcomeGroups = [
22
- {
23
- id: "ai",
24
- title: "Primary Agent Model",
25
- description: "Choose your main model and authenticate its provider",
26
- fields: kAllAiAuthFields,
27
- validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
28
- },
29
29
  {
30
30
  id: "github",
31
31
  title: "GitHub",
32
32
  description: "Backs up your agent's config and workspace",
33
33
  fields: [
34
+ {
35
+ key: "_GITHUB_SOURCE_REPO",
36
+ label: "Source Repo",
37
+ placeholder: "username/existing-openclaw",
38
+ isText: true,
39
+ },
34
40
  {
35
41
  key: "GITHUB_WORKSPACE_REPO",
36
- label: "Workspace Repo",
37
- hint: "A new private repo will be created for you",
42
+ label: "New Workspace Repo",
38
43
  placeholder: "username/my-agent",
39
44
  isText: true,
40
45
  },
41
46
  {
42
47
  key: "GITHUB_TOKEN",
43
48
  label: "Personal Access Token",
44
- hint: html`Create a classic PAT on${" "}<a
49
+ hint: html`Create a${" "}<a
45
50
  href="https://github.com/settings/tokens"
46
51
  target="_blank"
47
52
  class="hover:underline"
48
53
  style="color: var(--accent-link)"
49
- >GitHub settings</a
54
+ >classic PAT</a
50
55
  >${" "}with${" "}<code class="text-xs bg-black/30 px-1 rounded"
51
56
  >repo</code
52
- >${" "}scope`,
53
- placeholder: "ghp_...",
57
+ >${" "}scope, or a${" "}<a
58
+ href="https://github.com/settings/personal-access-tokens/new"
59
+ target="_blank"
60
+ class="hover:underline"
61
+ style="color: var(--accent-link)"
62
+ >fine-grained token</a
63
+ >${" "}with Contents + Metadata access`,
64
+ placeholder: "ghp_... or github_pat_...",
54
65
  },
55
66
  ],
56
- validate: (vals) =>
57
- !!(
58
- vals.GITHUB_TOKEN && isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
59
- ),
67
+ validate: (vals) => {
68
+ const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
69
+ const hasTarget = isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO);
70
+ const hasSource =
71
+ githubFlow !== kGithubFlowImport ||
72
+ isValidGithubRepoInput(vals._GITHUB_SOURCE_REPO);
73
+ return !!(vals.GITHUB_TOKEN && hasTarget && hasSource);
74
+ },
75
+ },
76
+ {
77
+ id: "ai",
78
+ title: "Primary Agent Model",
79
+ description: "Choose your main model and authenticate its provider",
80
+ fields: kAllAiAuthFields,
81
+ validate: (vals, ctx = {}) => !!(vals.MODEL_KEY && ctx.hasAi),
60
82
  },
61
83
  {
62
84
  id: "channels",
@@ -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 { isValidGithubRepoInput } from "./welcome-config.js";
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.fields
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">${field.hint}</p>
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`<button
285
- onclick=${goBack}
286
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
287
- >
288
- Back
289
- </button>`
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
- <button
292
- onclick=${goNext}
293
- disabled=${!currentGroupValid || githubStepLoading}
294
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
295
- >
296
- ${activeGroup.id === "github" && githubStepLoading
297
- ? "Checking..."
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
- </button>
388
+ loadingLabel="Checking..."
389
+ className="w-full"
390
+ />
300
391
  `
301
392
  : html`
302
393
  ${step > 0
303
- ? html`<button
304
- onclick=${goBack}
305
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-secondary"
306
- >
307
- Back
308
- </button>`
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
- <button
311
- onclick=${handleSubmit}
312
- disabled=${!allValid || loading}
313
- class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
314
- >
315
- ${loading ? "Starting..." : "Next"}
316
- </button>
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
  `;