@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
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { kSystemVars } = require("../../constants");
|
|
3
|
+
const {
|
|
4
|
+
normalizeHookPath,
|
|
5
|
+
normalizeTransformModulePath,
|
|
6
|
+
resolveConfigIncludes,
|
|
7
|
+
} = require("./import-config");
|
|
8
|
+
|
|
9
|
+
const kWorkspaceFiles = [
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"SOUL.md",
|
|
12
|
+
"USER.md",
|
|
13
|
+
"TOOLS.md",
|
|
14
|
+
"MEMORY.md",
|
|
15
|
+
"IDENTITY.md",
|
|
16
|
+
"HEARTBEAT.md",
|
|
17
|
+
"BOOTSTRAP.md",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const kConfigLocations = ["openclaw.json"];
|
|
21
|
+
|
|
22
|
+
const kEnvFileLocations = [
|
|
23
|
+
".env",
|
|
24
|
+
".env.local",
|
|
25
|
+
".env.production",
|
|
26
|
+
".env.development",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const kUnsupportedNestedLocations = [
|
|
30
|
+
".openclaw/openclaw.json",
|
|
31
|
+
".openclaw/.env",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const kCredentialDirs = ["credentials", "identity", "devices", "gogcli"];
|
|
35
|
+
|
|
36
|
+
const kManagedFiles = [
|
|
37
|
+
"hooks/bootstrap/AGENTS.md",
|
|
38
|
+
"hooks/bootstrap/TOOLS.md",
|
|
39
|
+
"cron/system-sync.json",
|
|
40
|
+
".gitignore",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const kManagedDirs = [".alphaclaw"];
|
|
44
|
+
|
|
45
|
+
const fileExists = (fs, filePath) => {
|
|
46
|
+
try {
|
|
47
|
+
return fs.statSync(filePath).isFile();
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const dirExists = (fs, dirPath) => {
|
|
54
|
+
try {
|
|
55
|
+
return fs.statSync(dirPath).isDirectory();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const globDir = (fs, dirPath, pattern) => {
|
|
62
|
+
const results = [];
|
|
63
|
+
try {
|
|
64
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
const subPath = path.join(dirPath, entry.name);
|
|
68
|
+
const target = path.join(subPath, pattern);
|
|
69
|
+
if (fileExists(fs, target)) {
|
|
70
|
+
results.push(path.relative(dirPath, target));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
return results;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const listRootMarkdown = (fs, baseDir) => {
|
|
79
|
+
const files = [];
|
|
80
|
+
try {
|
|
81
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (
|
|
84
|
+
entry.isFile() &&
|
|
85
|
+
entry.name.endsWith(".md") &&
|
|
86
|
+
!kWorkspaceFiles.includes(entry.name)
|
|
87
|
+
) {
|
|
88
|
+
files.push(entry.name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {}
|
|
92
|
+
return files;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const scanCategory = (fs, baseDir, relativePaths) => {
|
|
96
|
+
const found = [];
|
|
97
|
+
for (const rel of relativePaths) {
|
|
98
|
+
if (fileExists(fs, path.join(baseDir, rel))) {
|
|
99
|
+
found.push(rel);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { found: found.length > 0, files: found };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const scanDirCategory = (fs, baseDir, relativeDirs) => {
|
|
106
|
+
const found = [];
|
|
107
|
+
for (const rel of relativeDirs) {
|
|
108
|
+
if (dirExists(fs, path.join(baseDir, rel))) {
|
|
109
|
+
found.push(rel);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { found: found.length > 0, dirs: found };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const parseCronJobs = (fs, baseDir, cronFile) => {
|
|
116
|
+
try {
|
|
117
|
+
const raw = fs.readFileSync(path.join(baseDir, cronFile), "utf8");
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
const jobs = Array.isArray(parsed) ? parsed : parsed?.jobs;
|
|
120
|
+
if (!Array.isArray(jobs)) return [];
|
|
121
|
+
return jobs
|
|
122
|
+
.filter((job) => job && typeof job === "object")
|
|
123
|
+
.map((job, index) => {
|
|
124
|
+
const name = String(job.name || "").trim();
|
|
125
|
+
const id = String(job.id || "").trim();
|
|
126
|
+
return name || id || `Job ${index + 1}`;
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const parseHookDefinitions = (fs, baseDir, configFiles) => {
|
|
134
|
+
const hookNames = [];
|
|
135
|
+
const transformWarnings = [];
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
|
|
138
|
+
const addHookName = (value) => {
|
|
139
|
+
const name = String(value || "").trim();
|
|
140
|
+
if (!name || seen.has(name)) return;
|
|
141
|
+
seen.add(name);
|
|
142
|
+
hookNames.push(name);
|
|
143
|
+
return name;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
for (const configFile of configFiles) {
|
|
147
|
+
try {
|
|
148
|
+
const raw = fs.readFileSync(path.join(baseDir, configFile), "utf8");
|
|
149
|
+
const cfg = JSON.parse(raw);
|
|
150
|
+
const mappings = Array.isArray(cfg?.hooks?.mappings)
|
|
151
|
+
? cfg.hooks.mappings
|
|
152
|
+
: [];
|
|
153
|
+
mappings.forEach((mapping, index) => {
|
|
154
|
+
const name = String(
|
|
155
|
+
mapping?.name ||
|
|
156
|
+
mapping?.id ||
|
|
157
|
+
mapping?.match?.path ||
|
|
158
|
+
`Hook ${index + 1}`,
|
|
159
|
+
).trim();
|
|
160
|
+
const matchPath = normalizeHookPath(mapping?.match?.path);
|
|
161
|
+
const hookLabel = addHookName(
|
|
162
|
+
matchPath ? `${name} (${matchPath})` : name,
|
|
163
|
+
);
|
|
164
|
+
if (matchPath) {
|
|
165
|
+
const actualModule = normalizeTransformModulePath(
|
|
166
|
+
mapping?.transform?.module,
|
|
167
|
+
);
|
|
168
|
+
const expectedModule = `${matchPath}/${matchPath}-transform.mjs`;
|
|
169
|
+
if (hookLabel && actualModule && actualModule !== expectedModule) {
|
|
170
|
+
transformWarnings.push({
|
|
171
|
+
hookLabel,
|
|
172
|
+
actualPath: `hooks/transforms/${actualModule}`,
|
|
173
|
+
expectedPath: `hooks/transforms/${expectedModule}`,
|
|
174
|
+
message: `Uses hooks/transforms/${actualModule}; expected hooks/transforms/${expectedModule}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const internalEntries = cfg?.hooks?.internal?.entries;
|
|
181
|
+
if (internalEntries && typeof internalEntries === "object") {
|
|
182
|
+
for (const [key, entry] of Object.entries(internalEntries)) {
|
|
183
|
+
const enabled = entry?.enabled;
|
|
184
|
+
addHookName(
|
|
185
|
+
enabled === false
|
|
186
|
+
? `internal:${key} (disabled)`
|
|
187
|
+
: `internal:${key}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { hookNames, transformWarnings };
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const kEnvRefPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
198
|
+
|
|
199
|
+
const collectManagedEnvRefs = (value, found) => {
|
|
200
|
+
if (typeof value === "string") {
|
|
201
|
+
for (const match of value.matchAll(kEnvRefPattern)) {
|
|
202
|
+
const envKey = match[1];
|
|
203
|
+
if (kSystemVars.has(envKey)) {
|
|
204
|
+
found.add(envKey);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(value)) {
|
|
210
|
+
value.forEach((entry) => collectManagedEnvRefs(entry, found));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (value && typeof value === "object") {
|
|
214
|
+
Object.values(value).forEach((entry) =>
|
|
215
|
+
collectManagedEnvRefs(entry, found),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const collectManagedEnvConflicts = (fs, baseDir, configFiles, envFiles) => {
|
|
221
|
+
const managedVars = new Set();
|
|
222
|
+
let gatewayAuthNormalized = false;
|
|
223
|
+
|
|
224
|
+
for (const configFile of configFiles) {
|
|
225
|
+
try {
|
|
226
|
+
const raw = fs.readFileSync(path.join(baseDir, configFile), "utf8");
|
|
227
|
+
const cfg = JSON.parse(raw);
|
|
228
|
+
collectManagedEnvRefs(cfg, managedVars);
|
|
229
|
+
const gatewayToken = String(cfg?.gateway?.auth?.token || "").trim();
|
|
230
|
+
if (gatewayToken && gatewayToken !== "${OPENCLAW_GATEWAY_TOKEN}") {
|
|
231
|
+
gatewayAuthNormalized = true;
|
|
232
|
+
managedVars.add("OPENCLAW_GATEWAY_TOKEN");
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const envFile of envFiles) {
|
|
238
|
+
try {
|
|
239
|
+
const raw = fs.readFileSync(path.join(baseDir, envFile), "utf8");
|
|
240
|
+
for (const line of String(raw || "").split("\n")) {
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
243
|
+
const eqIndex = trimmed.indexOf("=");
|
|
244
|
+
if (eqIndex === -1) continue;
|
|
245
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
246
|
+
if (kSystemVars.has(key)) {
|
|
247
|
+
managedVars.add(key);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
found: managedVars.size > 0 || gatewayAuthNormalized,
|
|
255
|
+
vars: [...managedVars].sort(),
|
|
256
|
+
gatewayAuthNormalized,
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const detectImportSourceLayout = ({
|
|
261
|
+
gatewayConfig,
|
|
262
|
+
workspaceFiles,
|
|
263
|
+
skills,
|
|
264
|
+
cronJobs,
|
|
265
|
+
webhooks,
|
|
266
|
+
memory,
|
|
267
|
+
credentials,
|
|
268
|
+
unsupportedNested,
|
|
269
|
+
}) => {
|
|
270
|
+
const configFiles = Array.isArray(gatewayConfig?.files)
|
|
271
|
+
? gatewayConfig.files
|
|
272
|
+
: [];
|
|
273
|
+
const hasRootConfig = configFiles.some(
|
|
274
|
+
(filePath) => filePath === "openclaw.json",
|
|
275
|
+
);
|
|
276
|
+
const hasUnsupportedNested = !!unsupportedNested?.found;
|
|
277
|
+
const hasWorkspaceOnlyContent = !!(
|
|
278
|
+
workspaceFiles?.found ||
|
|
279
|
+
skills?.found ||
|
|
280
|
+
cronJobs?.found ||
|
|
281
|
+
webhooks?.found ||
|
|
282
|
+
memory?.found ||
|
|
283
|
+
credentials?.found
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (hasUnsupportedNested) {
|
|
287
|
+
return {
|
|
288
|
+
kind: "unsupported-nested-openclaw",
|
|
289
|
+
supported: false,
|
|
290
|
+
error:
|
|
291
|
+
"This import source contains a nested .openclaw config. Point the source at the OpenClaw root itself, or at a workspace-only repo instead.",
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (hasRootConfig) {
|
|
295
|
+
return {
|
|
296
|
+
kind: "full-openclaw-root",
|
|
297
|
+
supported: true,
|
|
298
|
+
promoteSourceSubdir: "",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (hasWorkspaceOnlyContent) {
|
|
302
|
+
return {
|
|
303
|
+
kind: "workspace-only",
|
|
304
|
+
supported: true,
|
|
305
|
+
promoteSourceSubdir: "",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
kind: "empty",
|
|
310
|
+
supported: true,
|
|
311
|
+
promoteSourceSubdir: "",
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const scanWorkspace = ({ fs, baseDir }) => {
|
|
316
|
+
const gatewayConfig = scanCategory(fs, baseDir, kConfigLocations);
|
|
317
|
+
for (const cfgFile of gatewayConfig.files) {
|
|
318
|
+
const includes = resolveConfigIncludes({
|
|
319
|
+
fs,
|
|
320
|
+
absoluteConfigPath: path.join(baseDir, cfgFile),
|
|
321
|
+
});
|
|
322
|
+
for (const inc of includes) {
|
|
323
|
+
if (
|
|
324
|
+
fileExists(fs, path.join(baseDir, inc)) &&
|
|
325
|
+
!gatewayConfig.files.includes(inc)
|
|
326
|
+
) {
|
|
327
|
+
gatewayConfig.files.push(inc);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const loc of kConfigLocations) {
|
|
332
|
+
const bakPath = `${loc}.bak`;
|
|
333
|
+
if (fileExists(fs, path.join(baseDir, bakPath))) {
|
|
334
|
+
if (!gatewayConfig.backups) gatewayConfig.backups = [];
|
|
335
|
+
gatewayConfig.backups.push(bakPath);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const envFiles = scanCategory(fs, baseDir, kEnvFileLocations);
|
|
340
|
+
const unsupportedNested = scanCategory(
|
|
341
|
+
fs,
|
|
342
|
+
baseDir,
|
|
343
|
+
kUnsupportedNestedLocations,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const workspaceFilesScan = scanCategory(fs, baseDir, kWorkspaceFiles);
|
|
347
|
+
const extraMarkdown = listRootMarkdown(fs, baseDir);
|
|
348
|
+
const workspaceFiles = {
|
|
349
|
+
found: workspaceFilesScan.found || extraMarkdown.length > 0,
|
|
350
|
+
files: workspaceFilesScan.files,
|
|
351
|
+
extraMarkdown,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const workspaceDir = path.join(baseDir, "workspace");
|
|
355
|
+
const skillsBase = dirExists(fs, path.join(workspaceDir, "skills"))
|
|
356
|
+
? path.join(workspaceDir, "skills")
|
|
357
|
+
: dirExists(fs, path.join(baseDir, "skills"))
|
|
358
|
+
? path.join(baseDir, "skills")
|
|
359
|
+
: null;
|
|
360
|
+
const skillFiles = skillsBase ? globDir(fs, skillsBase, "SKILL.md") : [];
|
|
361
|
+
const skills = { found: skillFiles.length > 0, files: skillFiles };
|
|
362
|
+
|
|
363
|
+
const cronJobs = scanCategory(fs, baseDir, [
|
|
364
|
+
"cron/jobs.json",
|
|
365
|
+
"config/cron-definitions.json",
|
|
366
|
+
]);
|
|
367
|
+
const cronJobNames = [];
|
|
368
|
+
for (const cronFile of cronJobs.files) {
|
|
369
|
+
for (const jobName of parseCronJobs(fs, baseDir, cronFile)) {
|
|
370
|
+
cronJobNames.push(jobName);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
cronJobs.jobNames = cronJobNames;
|
|
374
|
+
cronJobs.jobCount = cronJobNames.length;
|
|
375
|
+
cronJobs.found = cronJobs.found || cronJobNames.length > 0;
|
|
376
|
+
if (fileExists(fs, path.join(baseDir, "cron/jobs.json.bak"))) {
|
|
377
|
+
cronJobs.backups = ["cron/jobs.json.bak"];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const hooksTransformsDir = path.join(baseDir, "hooks", "transforms");
|
|
381
|
+
const webhookDirs = [];
|
|
382
|
+
if (dirExists(fs, hooksTransformsDir)) {
|
|
383
|
+
try {
|
|
384
|
+
const entries = fs.readdirSync(hooksTransformsDir, {
|
|
385
|
+
withFileTypes: true,
|
|
386
|
+
});
|
|
387
|
+
for (const entry of entries) {
|
|
388
|
+
if (entry.isDirectory()) {
|
|
389
|
+
webhookDirs.push(`hooks/transforms/${entry.name}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch {}
|
|
393
|
+
}
|
|
394
|
+
const { hookNames, transformWarnings } = parseHookDefinitions(
|
|
395
|
+
fs,
|
|
396
|
+
baseDir,
|
|
397
|
+
gatewayConfig.files,
|
|
398
|
+
);
|
|
399
|
+
const webhooks = {
|
|
400
|
+
found: webhookDirs.length > 0 || hookNames.length > 0,
|
|
401
|
+
dirs: webhookDirs,
|
|
402
|
+
hookNames,
|
|
403
|
+
hookCount: hookNames.length,
|
|
404
|
+
transformWarnings,
|
|
405
|
+
warningCount: transformWarnings.length,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const memory = scanDirCategory(fs, baseDir, ["memory"]);
|
|
409
|
+
|
|
410
|
+
const credentials = scanDirCategory(fs, baseDir, kCredentialDirs);
|
|
411
|
+
|
|
412
|
+
const managedFileConflicts = scanCategory(fs, baseDir, kManagedFiles);
|
|
413
|
+
const managedDirConflicts = scanDirCategory(fs, baseDir, kManagedDirs);
|
|
414
|
+
const managedConflicts = {
|
|
415
|
+
found: managedFileConflicts.found || managedDirConflicts.found,
|
|
416
|
+
files: managedFileConflicts.files,
|
|
417
|
+
dirs: managedDirConflicts.dirs || [],
|
|
418
|
+
};
|
|
419
|
+
const managedEnvConflicts = collectManagedEnvConflicts(
|
|
420
|
+
fs,
|
|
421
|
+
baseDir,
|
|
422
|
+
gatewayConfig.files,
|
|
423
|
+
envFiles.files,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const hasOpenclawSetup = gatewayConfig.found;
|
|
427
|
+
const isEmpty =
|
|
428
|
+
!gatewayConfig.found &&
|
|
429
|
+
!envFiles.found &&
|
|
430
|
+
!workspaceFiles.found &&
|
|
431
|
+
!skills.found;
|
|
432
|
+
const sourceLayout = detectImportSourceLayout({
|
|
433
|
+
gatewayConfig,
|
|
434
|
+
workspaceFiles,
|
|
435
|
+
skills,
|
|
436
|
+
cronJobs,
|
|
437
|
+
webhooks,
|
|
438
|
+
memory,
|
|
439
|
+
credentials,
|
|
440
|
+
unsupportedNested,
|
|
441
|
+
});
|
|
442
|
+
if (sourceLayout.kind === "workspace-only") {
|
|
443
|
+
sourceLayout.promoteSourceSubdir = dirExists(
|
|
444
|
+
fs,
|
|
445
|
+
path.join(baseDir, "workspace"),
|
|
446
|
+
)
|
|
447
|
+
? "workspace"
|
|
448
|
+
: "";
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
hasOpenclawSetup,
|
|
453
|
+
isEmpty,
|
|
454
|
+
sourceLayout,
|
|
455
|
+
gatewayConfig,
|
|
456
|
+
envFiles,
|
|
457
|
+
unsupportedNested,
|
|
458
|
+
workspaceFiles,
|
|
459
|
+
skills,
|
|
460
|
+
cronJobs,
|
|
461
|
+
webhooks,
|
|
462
|
+
memory,
|
|
463
|
+
credentials,
|
|
464
|
+
managedConflicts,
|
|
465
|
+
managedEnvConflicts,
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
module.exports = { scanWorkspace, detectImportSourceLayout };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const kImportTempPrefix = "alphaclaw-import-";
|
|
6
|
+
const kImportTempTtlMs = 24 * 60 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
const resolveImportTempDir = (tempDir) => {
|
|
9
|
+
const value = String(tempDir || "").trim();
|
|
10
|
+
if (!value) return "";
|
|
11
|
+
return path.resolve(value);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isValidImportTempDir = (tempDir) => {
|
|
15
|
+
const resolved = resolveImportTempDir(tempDir);
|
|
16
|
+
if (!resolved) return false;
|
|
17
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
18
|
+
const relative = path.relative(tempRoot, resolved);
|
|
19
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return path.basename(resolved).startsWith(kImportTempPrefix);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const cleanupStaleImportTempDirs = ({
|
|
26
|
+
fsModule = fs,
|
|
27
|
+
maxAgeMs = kImportTempTtlMs,
|
|
28
|
+
nowMs = Date.now(),
|
|
29
|
+
} = {}) => {
|
|
30
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
31
|
+
let removedCount = 0;
|
|
32
|
+
|
|
33
|
+
let entries = [];
|
|
34
|
+
try {
|
|
35
|
+
entries = fsModule.readdirSync(tempRoot, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return { removedCount };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry?.isDirectory?.()) continue;
|
|
42
|
+
if (!String(entry.name || "").startsWith(kImportTempPrefix)) continue;
|
|
43
|
+
const candidate = path.join(tempRoot, entry.name);
|
|
44
|
+
if (!isValidImportTempDir(candidate)) continue;
|
|
45
|
+
try {
|
|
46
|
+
const stats = fsModule.statSync(candidate);
|
|
47
|
+
const ageMs = nowMs - Number(stats?.mtimeMs || 0);
|
|
48
|
+
if (ageMs < maxAgeMs) continue;
|
|
49
|
+
fsModule.rmSync(candidate, { recursive: true, force: true });
|
|
50
|
+
removedCount += 1;
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { removedCount };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
kImportTempPrefix,
|
|
59
|
+
kImportTempTtlMs,
|
|
60
|
+
resolveImportTempDir,
|
|
61
|
+
isValidImportTempDir,
|
|
62
|
+
cleanupStaleImportTempDirs,
|
|
63
|
+
};
|