@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.
- 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,112 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { kWelcomeGroups } from "../onboarding/welcome-config.js";
|
|
4
|
+
import { WelcomeImportStep } from "../onboarding/welcome-import-step.js";
|
|
5
|
+
import { WelcomePlaceholderReviewStep } from "../onboarding/welcome-placeholder-review-step.js";
|
|
6
|
+
import { WelcomeSecretReviewStep } from "../onboarding/welcome-secret-review-step.js";
|
|
7
|
+
import { WelcomeHeader } from "../onboarding/welcome-header.js";
|
|
8
|
+
import { WelcomeSetupStep } from "../onboarding/welcome-setup-step.js";
|
|
9
|
+
import { WelcomeFormStep } from "../onboarding/welcome-form-step.js";
|
|
10
|
+
import { WelcomePairingStep } from "../onboarding/welcome-pairing-step.js";
|
|
11
|
+
import { useWelcome } from "./use-welcome.js";
|
|
12
|
+
|
|
13
|
+
const html = htm.bind(h);
|
|
14
|
+
|
|
15
|
+
export const Welcome = ({ onComplete }) => {
|
|
16
|
+
const { state, actions } = useWelcome({ onComplete });
|
|
17
|
+
|
|
18
|
+
return html`
|
|
19
|
+
<div class="max-w-lg w-full space-y-5">
|
|
20
|
+
<${WelcomeHeader}
|
|
21
|
+
groups=${kWelcomeGroups}
|
|
22
|
+
step=${state.step}
|
|
23
|
+
isSetupStep=${state.isSetupStep}
|
|
24
|
+
isPairingStep=${state.isPairingStep}
|
|
25
|
+
stepNumber=${state.stepNumber}
|
|
26
|
+
activeStepLabel=${state.activeStepLabel}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-3">
|
|
30
|
+
${state.isImportStep
|
|
31
|
+
? html`<${WelcomeImportStep}
|
|
32
|
+
scanResult=${state.importScanResult}
|
|
33
|
+
scanning=${state.importScanning}
|
|
34
|
+
error=${state.importError}
|
|
35
|
+
onApprove=${actions.handleImportApprove}
|
|
36
|
+
onShowSecretReview=${actions.handleShowSecretReview}
|
|
37
|
+
onBack=${actions.handleImportBack}
|
|
38
|
+
/>`
|
|
39
|
+
: state.isSecretReviewStep
|
|
40
|
+
? html`<${WelcomeSecretReviewStep}
|
|
41
|
+
secrets=${state.importScanResult?.secrets || []}
|
|
42
|
+
onApprove=${actions.handleImportApprove}
|
|
43
|
+
onBack=${actions.handleSecretReviewBack}
|
|
44
|
+
loading=${state.importScanning}
|
|
45
|
+
error=${state.importError}
|
|
46
|
+
/>`
|
|
47
|
+
: state.isPlaceholderReviewStep
|
|
48
|
+
? html`<${WelcomePlaceholderReviewStep}
|
|
49
|
+
placeholderReview=${state.placeholderReview}
|
|
50
|
+
vals=${state.vals}
|
|
51
|
+
setValue=${actions.setValue}
|
|
52
|
+
onContinue=${actions.handlePlaceholderReviewContinue}
|
|
53
|
+
/>`
|
|
54
|
+
: state.isSetupStep
|
|
55
|
+
? html`<${WelcomeSetupStep}
|
|
56
|
+
error=${state.setupError}
|
|
57
|
+
loading=${state.loading}
|
|
58
|
+
onRetry=${actions.handleSubmit}
|
|
59
|
+
onBack=${actions.goBackFromSetupError}
|
|
60
|
+
/>`
|
|
61
|
+
: state.isPairingStep
|
|
62
|
+
? html`<${WelcomePairingStep}
|
|
63
|
+
channel=${state.selectedPairingChannel}
|
|
64
|
+
pairings=${state.pairingRequestsPoll.data || []}
|
|
65
|
+
channels=${state.pairingChannels}
|
|
66
|
+
loading=${!state.pairingStatusPoll.data}
|
|
67
|
+
error=${state.pairingError}
|
|
68
|
+
onApprove=${actions.handlePairingApprove}
|
|
69
|
+
onReject=${actions.handlePairingReject}
|
|
70
|
+
canFinish=${state.pairingComplete || state.canFinishPairing}
|
|
71
|
+
onContinue=${actions.finishOnboarding}
|
|
72
|
+
/>`
|
|
73
|
+
: html`
|
|
74
|
+
<${WelcomeFormStep}
|
|
75
|
+
activeGroup=${state.activeGroup}
|
|
76
|
+
vals=${state.vals}
|
|
77
|
+
hasAi=${state.hasAi}
|
|
78
|
+
setValue=${actions.setValue}
|
|
79
|
+
modelOptions=${state.modelOptions}
|
|
80
|
+
modelsLoading=${state.modelsLoading}
|
|
81
|
+
modelsError=${state.modelsError}
|
|
82
|
+
canToggleFullCatalog=${state.canToggleFullCatalog}
|
|
83
|
+
showAllModels=${state.showAllModels}
|
|
84
|
+
setShowAllModels=${actions.setShowAllModels}
|
|
85
|
+
selectedProvider=${state.selectedProvider}
|
|
86
|
+
codexLoading=${state.codexLoading}
|
|
87
|
+
codexStatus=${state.codexStatus}
|
|
88
|
+
startCodexAuth=${actions.startCodexAuth}
|
|
89
|
+
handleCodexDisconnect=${actions.handleCodexDisconnect}
|
|
90
|
+
codexAuthStarted=${state.codexAuthStarted}
|
|
91
|
+
codexAuthWaiting=${state.codexAuthWaiting}
|
|
92
|
+
codexManualInput=${state.codexManualInput}
|
|
93
|
+
setCodexManualInput=${actions.setCodexManualInput}
|
|
94
|
+
completeCodexAuth=${actions.completeCodexAuth}
|
|
95
|
+
codexExchanging=${state.codexExchanging}
|
|
96
|
+
visibleAiFieldKeys=${state.visibleAiFieldKeys}
|
|
97
|
+
error=${state.formError}
|
|
98
|
+
step=${state.step}
|
|
99
|
+
totalGroups=${kWelcomeGroups.length}
|
|
100
|
+
currentGroupValid=${state.currentGroupValid}
|
|
101
|
+
goBack=${actions.goBack}
|
|
102
|
+
goNext=${actions.goNext}
|
|
103
|
+
loading=${state.loading}
|
|
104
|
+
githubStepLoading=${state.githubStepLoading}
|
|
105
|
+
allValid=${state.allValid}
|
|
106
|
+
handleSubmit=${actions.handleSubmit}
|
|
107
|
+
/>
|
|
108
|
+
`}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
`;
|
|
112
|
+
};
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import {
|
|
3
|
+
runOnboard,
|
|
4
|
+
verifyGithubOnboardingRepo,
|
|
5
|
+
scanImportRepo,
|
|
6
|
+
applyImport,
|
|
7
|
+
fetchModels,
|
|
8
|
+
} from "../../lib/api.js";
|
|
9
|
+
import {
|
|
10
|
+
getModelProvider,
|
|
11
|
+
getFeaturedModels,
|
|
12
|
+
getVisibleAiFieldKeys,
|
|
13
|
+
} from "../../lib/model-config.js";
|
|
14
|
+
import {
|
|
15
|
+
kWelcomeGroups,
|
|
16
|
+
isValidGithubRepoInput,
|
|
17
|
+
kGithubFlowFresh,
|
|
18
|
+
kGithubFlowImport,
|
|
19
|
+
kGithubTargetRepoModeCreate,
|
|
20
|
+
kGithubTargetRepoModeExistingEmpty,
|
|
21
|
+
kRepoModeNew,
|
|
22
|
+
kRepoModeExisting,
|
|
23
|
+
} from "../onboarding/welcome-config.js";
|
|
24
|
+
import { getPreferredPairingChannel } from "../onboarding/pairing-utils.js";
|
|
25
|
+
import {
|
|
26
|
+
kOnboardingStorageKey,
|
|
27
|
+
kPairingChannelKey,
|
|
28
|
+
useWelcomeStorage,
|
|
29
|
+
} from "../onboarding/use-welcome-storage.js";
|
|
30
|
+
import { useWelcomeCodex } from "../onboarding/use-welcome-codex.js";
|
|
31
|
+
import { useWelcomePairing } from "../onboarding/use-welcome-pairing.js";
|
|
32
|
+
|
|
33
|
+
const kMaxOnboardingVars = 64;
|
|
34
|
+
const kMaxEnvKeyLength = 128;
|
|
35
|
+
const kMaxEnvValueLength = 4096;
|
|
36
|
+
export const kImportStepId = "import";
|
|
37
|
+
export const kSecretReviewStepId = "secret-review";
|
|
38
|
+
export const kPlaceholderReviewStepId = "placeholder-review";
|
|
39
|
+
const kImportSubstepKey = "_IMPORT_SUBSTEP";
|
|
40
|
+
const kImportPlaceholderReviewKey = "_IMPORT_PLACEHOLDER_REVIEW";
|
|
41
|
+
const kImportPlaceholderSkipConfirmedKey = "_IMPORT_PLACEHOLDER_SKIP_CONFIRMED";
|
|
42
|
+
|
|
43
|
+
const normalizePlaceholderReview = (review) => {
|
|
44
|
+
if (!review || !Array.isArray(review.vars) || review.vars.length === 0) {
|
|
45
|
+
return { found: false, count: 0, vars: [] };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
found: true,
|
|
49
|
+
count:
|
|
50
|
+
typeof review.count === "number" ? review.count : review.vars.length,
|
|
51
|
+
vars: review.vars
|
|
52
|
+
.map((item) => ({
|
|
53
|
+
key: String(item?.key || "").trim(),
|
|
54
|
+
status: String(item?.status || "missing").trim() || "missing",
|
|
55
|
+
}))
|
|
56
|
+
.filter((item) => item.key),
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const useWelcome = ({ onComplete }) => {
|
|
61
|
+
const kSetupStepIndex = kWelcomeGroups.length;
|
|
62
|
+
const kPairingStepIndex = kSetupStepIndex + 1;
|
|
63
|
+
const { vals, setVals, setValue, step, setStep, setupError, setSetupError } =
|
|
64
|
+
useWelcomeStorage({
|
|
65
|
+
kSetupStepIndex,
|
|
66
|
+
kPairingStepIndex,
|
|
67
|
+
});
|
|
68
|
+
const [models, setModels] = useState([]);
|
|
69
|
+
const [modelsLoading, setModelsLoading] = useState(true);
|
|
70
|
+
const [modelsError, setModelsError] = useState(null);
|
|
71
|
+
const [showAllModels, setShowAllModels] = useState(false);
|
|
72
|
+
const [loading, setLoading] = useState(false);
|
|
73
|
+
const [githubStepLoading, setGithubStepLoading] = useState(false);
|
|
74
|
+
const [formError, setFormError] = useState(null);
|
|
75
|
+
const {
|
|
76
|
+
codexStatus,
|
|
77
|
+
codexLoading,
|
|
78
|
+
codexManualInput,
|
|
79
|
+
setCodexManualInput,
|
|
80
|
+
codexExchanging,
|
|
81
|
+
codexAuthStarted,
|
|
82
|
+
codexAuthWaiting,
|
|
83
|
+
startCodexAuth,
|
|
84
|
+
completeCodexAuth,
|
|
85
|
+
handleCodexDisconnect,
|
|
86
|
+
} = useWelcomeCodex({ setFormError });
|
|
87
|
+
const [importStep, setImportStepState] = useState(() => {
|
|
88
|
+
const storedStep = String(vals[kImportSubstepKey] || "").trim();
|
|
89
|
+
return storedStep === kPlaceholderReviewStepId
|
|
90
|
+
? storedStep
|
|
91
|
+
: null;
|
|
92
|
+
});
|
|
93
|
+
const [importTempDir, setImportTempDir] = useState(null);
|
|
94
|
+
const [importScanResult, setImportScanResult] = useState(null);
|
|
95
|
+
const [importScanning, setImportScanning] = useState(false);
|
|
96
|
+
const [importError, setImportError] = useState(null);
|
|
97
|
+
|
|
98
|
+
const setImportStep = (nextStep) => {
|
|
99
|
+
setImportStepState(nextStep);
|
|
100
|
+
setVals((prev) => ({
|
|
101
|
+
...prev,
|
|
102
|
+
[kImportSubstepKey]:
|
|
103
|
+
nextStep === kPlaceholderReviewStepId ? nextStep : "",
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const clearPlaceholderReview = () => {
|
|
108
|
+
setVals((prev) => ({
|
|
109
|
+
...prev,
|
|
110
|
+
[kImportPlaceholderReviewKey]: null,
|
|
111
|
+
[kImportPlaceholderSkipConfirmedKey]: false,
|
|
112
|
+
}));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
fetchModels()
|
|
117
|
+
.then((result) => {
|
|
118
|
+
const list = Array.isArray(result.models) ? result.models : [];
|
|
119
|
+
const featured = getFeaturedModels(list);
|
|
120
|
+
setModels(list);
|
|
121
|
+
if (!vals.MODEL_KEY && list.length > 0) {
|
|
122
|
+
const defaultModel = featured[0] || list[0];
|
|
123
|
+
setVals((prev) => ({ ...prev, MODEL_KEY: defaultModel.key }));
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
.catch(() => setModelsError("Failed to load models"))
|
|
127
|
+
.finally(() => setModelsLoading(false));
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
const selectedProvider = getModelProvider(vals.MODEL_KEY);
|
|
131
|
+
const placeholderReview = normalizePlaceholderReview(
|
|
132
|
+
vals[kImportPlaceholderReviewKey],
|
|
133
|
+
);
|
|
134
|
+
const featuredModels = getFeaturedModels(models);
|
|
135
|
+
const baseModelOptions = showAllModels
|
|
136
|
+
? models
|
|
137
|
+
: featuredModels.length > 0
|
|
138
|
+
? featuredModels
|
|
139
|
+
: models;
|
|
140
|
+
const selectedModelOption = models.find(
|
|
141
|
+
(model) => model.key === vals.MODEL_KEY,
|
|
142
|
+
);
|
|
143
|
+
const modelOptions =
|
|
144
|
+
selectedModelOption &&
|
|
145
|
+
!baseModelOptions.some((model) => model.key === selectedModelOption.key)
|
|
146
|
+
? [...baseModelOptions, selectedModelOption]
|
|
147
|
+
: baseModelOptions;
|
|
148
|
+
const canToggleFullCatalog =
|
|
149
|
+
featuredModels.length > 0 && models.length > featuredModels.length;
|
|
150
|
+
const visibleAiFieldKeys = getVisibleAiFieldKeys(selectedProvider);
|
|
151
|
+
const hasAi =
|
|
152
|
+
selectedProvider === "anthropic"
|
|
153
|
+
? !!(vals.ANTHROPIC_API_KEY || vals.ANTHROPIC_TOKEN)
|
|
154
|
+
: selectedProvider === "openai"
|
|
155
|
+
? !!vals.OPENAI_API_KEY
|
|
156
|
+
: selectedProvider === "google"
|
|
157
|
+
? !!vals.GEMINI_API_KEY
|
|
158
|
+
: selectedProvider === "openai-codex"
|
|
159
|
+
? !!codexStatus.connected
|
|
160
|
+
: false;
|
|
161
|
+
|
|
162
|
+
const allValid = kWelcomeGroups.every((group) => group.validate(vals, { hasAi }));
|
|
163
|
+
const isSetupStep = step === kSetupStepIndex;
|
|
164
|
+
const isPairingStep = step === kPairingStepIndex;
|
|
165
|
+
const activeGroup = step < kSetupStepIndex ? kWelcomeGroups[step] : null;
|
|
166
|
+
const currentGroupValid = activeGroup
|
|
167
|
+
? activeGroup.validate(vals, { hasAi })
|
|
168
|
+
: false;
|
|
169
|
+
const selectedPairingChannel = String(
|
|
170
|
+
vals[kPairingChannelKey] || getPreferredPairingChannel(vals),
|
|
171
|
+
);
|
|
172
|
+
const {
|
|
173
|
+
pairingStatusPoll,
|
|
174
|
+
pairingRequestsPoll,
|
|
175
|
+
pairingChannels,
|
|
176
|
+
canFinishPairing,
|
|
177
|
+
pairingError,
|
|
178
|
+
pairingComplete,
|
|
179
|
+
handlePairingApprove,
|
|
180
|
+
handlePairingReject,
|
|
181
|
+
resetPairingState,
|
|
182
|
+
} = useWelcomePairing({
|
|
183
|
+
isPairingStep,
|
|
184
|
+
selectedPairingChannel,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const handleSubmit = async () => {
|
|
188
|
+
if (!allValid || loading) return;
|
|
189
|
+
const vars = Object.entries(vals)
|
|
190
|
+
.filter(
|
|
191
|
+
([key]) => key !== "MODEL_KEY" && !String(key || "").startsWith("_"),
|
|
192
|
+
)
|
|
193
|
+
.filter(([, value]) => value)
|
|
194
|
+
.map(([key, value]) => ({ key, value }));
|
|
195
|
+
const preflightError = (() => {
|
|
196
|
+
if (!vals.MODEL_KEY || !String(vals.MODEL_KEY).includes("/")) {
|
|
197
|
+
return "A model selection is required";
|
|
198
|
+
}
|
|
199
|
+
if (vars.length > kMaxOnboardingVars) {
|
|
200
|
+
return `Too many environment variables (max ${kMaxOnboardingVars})`;
|
|
201
|
+
}
|
|
202
|
+
for (const entry of vars) {
|
|
203
|
+
const key = String(entry?.key || "");
|
|
204
|
+
const value = String(entry?.value || "");
|
|
205
|
+
if (!key) return "Each variable must include a key";
|
|
206
|
+
if (key.length > kMaxEnvKeyLength) {
|
|
207
|
+
return `Variable key is too long: ${key.slice(0, 32)}...`;
|
|
208
|
+
}
|
|
209
|
+
if (value.length > kMaxEnvValueLength) {
|
|
210
|
+
return `Value too long for ${key} (max ${kMaxEnvValueLength} chars)`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
!vals.GITHUB_TOKEN ||
|
|
215
|
+
!isValidGithubRepoInput(vals.GITHUB_WORKSPACE_REPO)
|
|
216
|
+
) {
|
|
217
|
+
return 'Target repo must be in "owner/repo" format.';
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
(vals._GITHUB_FLOW || kGithubFlowFresh) === kGithubFlowImport &&
|
|
221
|
+
!isValidGithubRepoInput(vals._GITHUB_SOURCE_REPO)
|
|
222
|
+
) {
|
|
223
|
+
return 'Source repo must be in "owner/repo" format.';
|
|
224
|
+
}
|
|
225
|
+
return "";
|
|
226
|
+
})();
|
|
227
|
+
if (preflightError) {
|
|
228
|
+
setFormError(preflightError);
|
|
229
|
+
setSetupError(null);
|
|
230
|
+
setStep(
|
|
231
|
+
Math.max(
|
|
232
|
+
0,
|
|
233
|
+
kWelcomeGroups.findIndex((group) => group.id === "github"),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
setStep(kSetupStepIndex);
|
|
239
|
+
setLoading(true);
|
|
240
|
+
setFormError(null);
|
|
241
|
+
setSetupError(null);
|
|
242
|
+
resetPairingState();
|
|
243
|
+
|
|
244
|
+
const wasImport =
|
|
245
|
+
(vals._GITHUB_FLOW || kGithubFlowFresh) === kGithubFlowImport;
|
|
246
|
+
try {
|
|
247
|
+
const result = await runOnboard(vars, vals.MODEL_KEY, {
|
|
248
|
+
importMode: wasImport,
|
|
249
|
+
});
|
|
250
|
+
if (!result.ok) throw new Error(result.error || "Onboarding failed");
|
|
251
|
+
const pairingChannel = getPreferredPairingChannel(vals);
|
|
252
|
+
if (!pairingChannel) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
"No Telegram or Discord bot token configured for pairing.",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
setVals((prev) => ({
|
|
258
|
+
...prev,
|
|
259
|
+
[kPairingChannelKey]: pairingChannel,
|
|
260
|
+
}));
|
|
261
|
+
setLoading(false);
|
|
262
|
+
setStep(kPairingStepIndex);
|
|
263
|
+
resetPairingState();
|
|
264
|
+
setSetupError(null);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error("Onboard error:", err);
|
|
267
|
+
setSetupError(err.message || "Onboarding failed");
|
|
268
|
+
setLoading(false);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const finishOnboarding = () => {
|
|
273
|
+
localStorage.removeItem(kOnboardingStorageKey);
|
|
274
|
+
onComplete();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const goBack = () => {
|
|
278
|
+
if (isSetupStep) return;
|
|
279
|
+
setFormError(null);
|
|
280
|
+
setStep((prev) => Math.max(0, prev - 1));
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const goBackFromSetupError = () => {
|
|
284
|
+
setLoading(false);
|
|
285
|
+
setSetupError(null);
|
|
286
|
+
setStep(kWelcomeGroups.length - 1);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const goNext = async () => {
|
|
290
|
+
if (!activeGroup || !currentGroupValid) return;
|
|
291
|
+
setFormError(null);
|
|
292
|
+
if (activeGroup.id === "github") {
|
|
293
|
+
const githubFlow = vals._GITHUB_FLOW || kGithubFlowFresh;
|
|
294
|
+
const targetRepoMode =
|
|
295
|
+
githubFlow === kGithubFlowImport
|
|
296
|
+
? kGithubTargetRepoModeCreate
|
|
297
|
+
: vals._GITHUB_TARGET_REPO_MODE || kGithubTargetRepoModeCreate;
|
|
298
|
+
const targetVerifyMode =
|
|
299
|
+
targetRepoMode === kGithubTargetRepoModeExistingEmpty
|
|
300
|
+
? kRepoModeExisting
|
|
301
|
+
: kRepoModeNew;
|
|
302
|
+
const sourceRepo =
|
|
303
|
+
githubFlow === kGithubFlowImport
|
|
304
|
+
? vals._GITHUB_SOURCE_REPO
|
|
305
|
+
: vals.GITHUB_WORKSPACE_REPO;
|
|
306
|
+
setGithubStepLoading(true);
|
|
307
|
+
clearPlaceholderReview();
|
|
308
|
+
try {
|
|
309
|
+
if (githubFlow === kGithubFlowImport) {
|
|
310
|
+
const sourceResult = await verifyGithubOnboardingRepo(
|
|
311
|
+
sourceRepo,
|
|
312
|
+
vals.GITHUB_TOKEN,
|
|
313
|
+
kRepoModeExisting,
|
|
314
|
+
);
|
|
315
|
+
if (!sourceResult?.ok) {
|
|
316
|
+
setFormError(sourceResult?.error || "GitHub source verification failed");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (sourceResult.repoIsEmpty) {
|
|
320
|
+
setFormError(
|
|
321
|
+
"That source repository is empty. Use Start fresh if you want AlphaClaw to bootstrap a new setup there.",
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const targetResult = await verifyGithubOnboardingRepo(
|
|
326
|
+
vals.GITHUB_WORKSPACE_REPO,
|
|
327
|
+
vals.GITHUB_TOKEN,
|
|
328
|
+
kRepoModeNew,
|
|
329
|
+
);
|
|
330
|
+
if (!targetResult?.ok) {
|
|
331
|
+
setFormError(targetResult?.error || "GitHub target verification failed");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
targetRepoMode === kGithubTargetRepoModeCreate &&
|
|
336
|
+
targetResult.repoExists
|
|
337
|
+
) {
|
|
338
|
+
setFormError(
|
|
339
|
+
"That target repository already exists. Choose Use existing empty repo or pick a new target repo name.",
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (
|
|
344
|
+
targetRepoMode === kGithubTargetRepoModeExistingEmpty &&
|
|
345
|
+
!targetResult.repoExists
|
|
346
|
+
) {
|
|
347
|
+
setFormError(
|
|
348
|
+
"That target repository does not exist yet. Choose Create new repo or enter an existing empty target repo.",
|
|
349
|
+
);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (sourceResult.tempDir && !sourceResult.repoIsEmpty) {
|
|
353
|
+
setImportTempDir(sourceResult.tempDir);
|
|
354
|
+
setImportStep(kImportStepId);
|
|
355
|
+
setImportScanning(true);
|
|
356
|
+
setImportError(null);
|
|
357
|
+
try {
|
|
358
|
+
const scanResult = await scanImportRepo(sourceResult.tempDir);
|
|
359
|
+
if (!scanResult?.ok) {
|
|
360
|
+
setImportError(scanResult?.error || "Import scan failed");
|
|
361
|
+
setImportScanning(false);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
setImportScanResult(scanResult);
|
|
365
|
+
} catch (scanErr) {
|
|
366
|
+
setImportError(scanErr?.message || "Import scan failed");
|
|
367
|
+
} finally {
|
|
368
|
+
setImportScanning(false);
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const targetResult = await verifyGithubOnboardingRepo(
|
|
374
|
+
vals.GITHUB_WORKSPACE_REPO,
|
|
375
|
+
vals.GITHUB_TOKEN,
|
|
376
|
+
targetVerifyMode,
|
|
377
|
+
);
|
|
378
|
+
if (!targetResult?.ok) {
|
|
379
|
+
setFormError(targetResult?.error || "GitHub verification failed");
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (
|
|
383
|
+
targetRepoMode === kGithubTargetRepoModeCreate &&
|
|
384
|
+
targetResult.repoExists
|
|
385
|
+
) {
|
|
386
|
+
setFormError(
|
|
387
|
+
"That target repository already exists. Choose Use existing empty repo or pick a new target repo name.",
|
|
388
|
+
);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
targetRepoMode === kGithubTargetRepoModeExistingEmpty &&
|
|
393
|
+
!targetResult.repoExists
|
|
394
|
+
) {
|
|
395
|
+
setFormError(
|
|
396
|
+
"That target repository does not exist yet. Choose Create new repo or enter an existing empty target repo.",
|
|
397
|
+
);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
} catch (err) {
|
|
401
|
+
setFormError(err?.message || "GitHub verification failed");
|
|
402
|
+
return;
|
|
403
|
+
} finally {
|
|
404
|
+
setGithubStepLoading(false);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const handleImportApprove = async (approvedSecrets = []) => {
|
|
411
|
+
setImportScanning(true);
|
|
412
|
+
setImportError(null);
|
|
413
|
+
try {
|
|
414
|
+
const skipSecretExtraction = approvedSecrets.length === 0;
|
|
415
|
+
const result = await applyImport({
|
|
416
|
+
tempDir: importTempDir,
|
|
417
|
+
approvedSecrets,
|
|
418
|
+
skipSecretExtraction,
|
|
419
|
+
githubRepo: vals.GITHUB_WORKSPACE_REPO,
|
|
420
|
+
githubToken: vals.GITHUB_TOKEN,
|
|
421
|
+
});
|
|
422
|
+
if (!result?.ok) {
|
|
423
|
+
setImportError(result?.error || "Import failed");
|
|
424
|
+
setImportScanning(false);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const nextPlaceholderReview = normalizePlaceholderReview(
|
|
428
|
+
result.placeholderReview,
|
|
429
|
+
);
|
|
430
|
+
setVals((prev) => ({
|
|
431
|
+
...prev,
|
|
432
|
+
...(result.preFill || {}),
|
|
433
|
+
[kImportPlaceholderReviewKey]: nextPlaceholderReview,
|
|
434
|
+
[kImportPlaceholderSkipConfirmedKey]: false,
|
|
435
|
+
}));
|
|
436
|
+
if (nextPlaceholderReview.found) {
|
|
437
|
+
setImportStep(kPlaceholderReviewStepId);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
clearPlaceholderReview();
|
|
441
|
+
setImportStep(null);
|
|
442
|
+
setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
|
|
443
|
+
} catch (err) {
|
|
444
|
+
setImportError(err?.message || "Import failed");
|
|
445
|
+
} finally {
|
|
446
|
+
setImportScanning(false);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const handleShowSecretReview = () => {
|
|
451
|
+
setImportStep(kSecretReviewStepId);
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const handleSecretReviewBack = () => {
|
|
455
|
+
setImportStep(kImportStepId);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const handleImportBack = () => {
|
|
459
|
+
setImportStep(null);
|
|
460
|
+
setImportTempDir(null);
|
|
461
|
+
setImportScanResult(null);
|
|
462
|
+
setImportError(null);
|
|
463
|
+
clearPlaceholderReview();
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const handlePlaceholderReviewContinue = () => {
|
|
467
|
+
clearPlaceholderReview();
|
|
468
|
+
setImportStep(null);
|
|
469
|
+
setStep((prev) => Math.min(kWelcomeGroups.length - 1, prev + 1));
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const isImportStep = importStep === kImportStepId;
|
|
473
|
+
const isSecretReviewStep = importStep === kSecretReviewStepId;
|
|
474
|
+
const isPlaceholderReviewStep = importStep === kPlaceholderReviewStepId;
|
|
475
|
+
const activeStepLabel = isImportStep
|
|
476
|
+
? "Import"
|
|
477
|
+
: isSecretReviewStep
|
|
478
|
+
? "Review Secrets"
|
|
479
|
+
: isPlaceholderReviewStep
|
|
480
|
+
? "Review Env Vars"
|
|
481
|
+
: isSetupStep
|
|
482
|
+
? "Initializing"
|
|
483
|
+
: isPairingStep
|
|
484
|
+
? "Pairing"
|
|
485
|
+
: activeGroup?.title || "Setup";
|
|
486
|
+
const stepNumber =
|
|
487
|
+
isImportStep || isSecretReviewStep || isPlaceholderReviewStep
|
|
488
|
+
? step + 1
|
|
489
|
+
: isSetupStep
|
|
490
|
+
? kWelcomeGroups.length + 1
|
|
491
|
+
: isPairingStep
|
|
492
|
+
? kWelcomeGroups.length + 2
|
|
493
|
+
: step + 1;
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
state: {
|
|
497
|
+
vals,
|
|
498
|
+
step,
|
|
499
|
+
setupError,
|
|
500
|
+
modelsLoading,
|
|
501
|
+
modelsError,
|
|
502
|
+
showAllModels,
|
|
503
|
+
loading,
|
|
504
|
+
githubStepLoading,
|
|
505
|
+
formError,
|
|
506
|
+
importScanResult,
|
|
507
|
+
importScanning,
|
|
508
|
+
importError,
|
|
509
|
+
selectedProvider,
|
|
510
|
+
modelOptions,
|
|
511
|
+
canToggleFullCatalog,
|
|
512
|
+
visibleAiFieldKeys,
|
|
513
|
+
hasAi,
|
|
514
|
+
allValid,
|
|
515
|
+
isSetupStep,
|
|
516
|
+
isPairingStep,
|
|
517
|
+
activeGroup,
|
|
518
|
+
currentGroupValid,
|
|
519
|
+
selectedPairingChannel,
|
|
520
|
+
placeholderReview,
|
|
521
|
+
isImportStep,
|
|
522
|
+
isSecretReviewStep,
|
|
523
|
+
isPlaceholderReviewStep,
|
|
524
|
+
activeStepLabel,
|
|
525
|
+
stepNumber,
|
|
526
|
+
codexStatus,
|
|
527
|
+
codexLoading,
|
|
528
|
+
codexManualInput,
|
|
529
|
+
codexExchanging,
|
|
530
|
+
codexAuthStarted,
|
|
531
|
+
codexAuthWaiting,
|
|
532
|
+
pairingStatusPoll,
|
|
533
|
+
pairingRequestsPoll,
|
|
534
|
+
pairingChannels,
|
|
535
|
+
canFinishPairing,
|
|
536
|
+
pairingError,
|
|
537
|
+
pairingComplete,
|
|
538
|
+
},
|
|
539
|
+
actions: {
|
|
540
|
+
setVals,
|
|
541
|
+
setValue,
|
|
542
|
+
setShowAllModels,
|
|
543
|
+
setCodexManualInput,
|
|
544
|
+
startCodexAuth,
|
|
545
|
+
completeCodexAuth,
|
|
546
|
+
handleCodexDisconnect,
|
|
547
|
+
handleSubmit,
|
|
548
|
+
finishOnboarding,
|
|
549
|
+
goBack,
|
|
550
|
+
goBackFromSetupError,
|
|
551
|
+
goNext,
|
|
552
|
+
handleImportApprove,
|
|
553
|
+
handleShowSecretReview,
|
|
554
|
+
handleSecretReviewBack,
|
|
555
|
+
handleImportBack,
|
|
556
|
+
handlePlaceholderReviewContinue,
|
|
557
|
+
handlePairingApprove,
|
|
558
|
+
handlePairingReject,
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
};
|