artbot 0.1.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/README.md +32 -0
- package/bin/artbot.cjs +11 -0
- package/dist/chunks/chunk-KKAF45ZB.js +667 -0
- package/dist/chunks/chunk-KKAF45ZB.js.map +7 -0
- package/dist/chunks/interactive-DQDPPJBS.js +994 -0
- package/dist/chunks/interactive-DQDPPJBS.js.map +7 -0
- package/dist/index.js +892 -0
- package/dist/index.js.map +7 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# artbot
|
|
2
|
+
|
|
3
|
+
Command-line client for ArtBot market research runs.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g artbot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
The CLI talks to an ArtBot API server. By default it uses `http://localhost:4000`.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
artbot runs list
|
|
17
|
+
artbot research artist --artist "Burhan Dogancay" --wait
|
|
18
|
+
artbot runs show --run-id <id>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
To point the CLI at a different backend:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export API_BASE_URL=https://your-artbot-api.example.com
|
|
25
|
+
artbot runs list
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For local development in this monorepo, start the API and worker first:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm run start:artbot
|
|
32
|
+
```
|
package/bin/artbot.cjs
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
// src/lib/file-system.ts
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
function pathExists(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
fs.accessSync(filePath, fs.constants.F_OK);
|
|
6
|
+
return true;
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function statFile(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
return fs.statSync(filePath);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/setup/env.ts
|
|
20
|
+
import fs2 from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { config as loadDotenv } from "dotenv";
|
|
24
|
+
var SETUP_PROFILE_BLUEPRINTS = [
|
|
25
|
+
{
|
|
26
|
+
id: "artsy-auth",
|
|
27
|
+
mode: "authorized",
|
|
28
|
+
sourceName: "Artsy",
|
|
29
|
+
sourcePatterns: ["artsy"]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "mutualart-auth",
|
|
33
|
+
mode: "authorized",
|
|
34
|
+
sourceName: "MutualArt",
|
|
35
|
+
sourcePatterns: ["mutualart"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "sanatfiyat-license",
|
|
39
|
+
mode: "licensed",
|
|
40
|
+
sourceName: "Sanatfiyat",
|
|
41
|
+
sourcePatterns: ["sanatfiyat"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "askart-license",
|
|
45
|
+
mode: "licensed",
|
|
46
|
+
sourceName: "askART",
|
|
47
|
+
sourcePatterns: ["askart"]
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
var CLI_MODULE_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
51
|
+
var loadedEnvPath = null;
|
|
52
|
+
function formatEnvValue(value) {
|
|
53
|
+
if (/^[A-Za-z0-9_./,:-]+$/.test(value)) {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
if (!value.includes("'") && !value.includes("\n") && !value.includes("\r")) {
|
|
57
|
+
return `'${value}'`;
|
|
58
|
+
}
|
|
59
|
+
return JSON.stringify(value);
|
|
60
|
+
}
|
|
61
|
+
function isWorkspaceRoot(directory) {
|
|
62
|
+
return fs2.existsSync(path.join(directory, "pnpm-workspace.yaml")) || fs2.existsSync(path.join(directory, "turbo.json"));
|
|
63
|
+
}
|
|
64
|
+
function coerceSearchDirectory(candidate) {
|
|
65
|
+
const resolved = path.resolve(candidate);
|
|
66
|
+
try {
|
|
67
|
+
return fs2.statSync(resolved).isDirectory() ? resolved : path.dirname(resolved);
|
|
68
|
+
} catch {
|
|
69
|
+
return path.extname(resolved) ? path.dirname(resolved) : resolved;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function findWorkspaceRoot(candidate) {
|
|
73
|
+
let current = coerceSearchDirectory(candidate);
|
|
74
|
+
while (true) {
|
|
75
|
+
if (isWorkspaceRoot(current)) {
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
const parent = path.dirname(current);
|
|
79
|
+
if (parent === current) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
current = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function detectWorkspaceRoot(cwd = process.cwd()) {
|
|
86
|
+
const candidates = [
|
|
87
|
+
process.env.INIT_CWD,
|
|
88
|
+
cwd,
|
|
89
|
+
process.env.ARTBOT_ROOT,
|
|
90
|
+
process.env.RUNS_ROOT,
|
|
91
|
+
process.env.DATABASE_PATH,
|
|
92
|
+
CLI_MODULE_DIR
|
|
93
|
+
].filter((candidate) => Boolean(candidate && candidate.trim().length > 0));
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
const workspaceRoot = findWorkspaceRoot(candidate);
|
|
96
|
+
if (workspaceRoot) {
|
|
97
|
+
return workspaceRoot;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function resolveWorkspaceRoot(cwd = process.cwd()) {
|
|
103
|
+
return detectWorkspaceRoot(cwd) ?? path.resolve(cwd);
|
|
104
|
+
}
|
|
105
|
+
function hasLocalBackendWorkspace(cwd = process.cwd()) {
|
|
106
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
107
|
+
if (!workspaceRoot) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return fs2.existsSync(path.join(workspaceRoot, "apps", "api", "package.json")) && fs2.existsSync(path.join(workspaceRoot, "apps", "worker", "package.json"));
|
|
111
|
+
}
|
|
112
|
+
function resolveEnvFilePath(cwd = process.cwd()) {
|
|
113
|
+
return path.resolve(resolveWorkspaceRoot(cwd), ".env");
|
|
114
|
+
}
|
|
115
|
+
function loadWorkspaceEnv(cwd = process.cwd()) {
|
|
116
|
+
const envPath = resolveEnvFilePath(cwd);
|
|
117
|
+
if (loadedEnvPath === envPath) {
|
|
118
|
+
return envPath;
|
|
119
|
+
}
|
|
120
|
+
loadDotenv({ path: envPath, override: false });
|
|
121
|
+
loadedEnvPath = envPath;
|
|
122
|
+
return envPath;
|
|
123
|
+
}
|
|
124
|
+
function parseBooleanEnv(value, fallback) {
|
|
125
|
+
if (value == null || value.trim() === "") return fallback;
|
|
126
|
+
return value.trim().toLowerCase() === "true";
|
|
127
|
+
}
|
|
128
|
+
function readEnvFile(envPath) {
|
|
129
|
+
try {
|
|
130
|
+
return fs2.readFileSync(envPath, "utf-8");
|
|
131
|
+
} catch {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function upsertEnvFile(envPath, updates) {
|
|
136
|
+
const existing = readEnvFile(envPath);
|
|
137
|
+
const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
|
|
138
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
139
|
+
const nextLines = lines.map((line) => {
|
|
140
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
141
|
+
if (!match) {
|
|
142
|
+
return line;
|
|
143
|
+
}
|
|
144
|
+
const key = match[1];
|
|
145
|
+
if (!(key in updates)) {
|
|
146
|
+
return line;
|
|
147
|
+
}
|
|
148
|
+
consumed.add(key);
|
|
149
|
+
return `${key}=${formatEnvValue(updates[key])}`;
|
|
150
|
+
});
|
|
151
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
152
|
+
if (consumed.has(key)) continue;
|
|
153
|
+
nextLines.push(`${key}=${formatEnvValue(value)}`);
|
|
154
|
+
}
|
|
155
|
+
fs2.writeFileSync(envPath, `${nextLines.filter(Boolean).join("\n")}
|
|
156
|
+
`, "utf-8");
|
|
157
|
+
}
|
|
158
|
+
function buildDefaultAuthProfiles(options = {}) {
|
|
159
|
+
const cwd = resolveWorkspaceRoot(options.cwd ?? process.cwd());
|
|
160
|
+
const enableOptionalProbes = options.enableOptionalProbes ?? false;
|
|
161
|
+
const enableLicensedIntegrations = options.enableLicensedIntegrations ?? false;
|
|
162
|
+
return SETUP_PROFILE_BLUEPRINTS.filter((profile) => {
|
|
163
|
+
if (profile.id === "artsy-auth" || profile.id === "mutualart-auth") {
|
|
164
|
+
return enableOptionalProbes;
|
|
165
|
+
}
|
|
166
|
+
return enableLicensedIntegrations;
|
|
167
|
+
}).map((profile) => ({
|
|
168
|
+
id: profile.id,
|
|
169
|
+
mode: profile.mode,
|
|
170
|
+
sourcePatterns: profile.sourcePatterns,
|
|
171
|
+
storageStatePath: path.resolve(cwd, "playwright", ".auth", `${profile.id}.json`)
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
function buildSetupEnvUpdates(values) {
|
|
175
|
+
return {
|
|
176
|
+
LLM_BASE_URL: values.llmBaseUrl,
|
|
177
|
+
API_BASE_URL: values.apiBaseUrl,
|
|
178
|
+
ENABLE_OPTIONAL_PROBE_ADAPTERS: String(values.enableOptionalProbes),
|
|
179
|
+
ENABLE_LICENSED_INTEGRATIONS: String(values.enableLicensedIntegrations),
|
|
180
|
+
DEFAULT_LICENSED_INTEGRATIONS: values.defaultLicensedIntegrations.join(","),
|
|
181
|
+
DEFAULT_AUTH_PROFILE: "",
|
|
182
|
+
AUTH_PROFILES_JSON: JSON.stringify(values.authProfiles)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function defaultSourceUrlForProfile(profileId) {
|
|
186
|
+
switch (profileId) {
|
|
187
|
+
case "artsy-auth":
|
|
188
|
+
return "https://www.artsy.net";
|
|
189
|
+
case "mutualart-auth":
|
|
190
|
+
return "https://www.mutualart.com";
|
|
191
|
+
case "sanatfiyat-license":
|
|
192
|
+
return "https://www.sanatfiyat.com";
|
|
193
|
+
case "askart-license":
|
|
194
|
+
return "https://www.askart.com";
|
|
195
|
+
default:
|
|
196
|
+
return "https://example.com";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/setup/auth.ts
|
|
201
|
+
import path2 from "node:path";
|
|
202
|
+
function normalizeSourcePattern(value) {
|
|
203
|
+
return value.trim().toLowerCase();
|
|
204
|
+
}
|
|
205
|
+
function buildAuthProfilesParseCandidates(rawValue) {
|
|
206
|
+
const trimmed = rawValue.trim();
|
|
207
|
+
const candidates = [trimmed];
|
|
208
|
+
const pushCandidate = (candidate) => {
|
|
209
|
+
if (candidate.length > 0 && !candidates.includes(candidate)) {
|
|
210
|
+
candidates.push(candidate);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
214
|
+
pushCandidate(trimmed.slice(1, -1));
|
|
215
|
+
}
|
|
216
|
+
for (const candidate of [...candidates]) {
|
|
217
|
+
const repaired = candidate.replace(/\\"/g, '"');
|
|
218
|
+
if (repaired !== candidate) {
|
|
219
|
+
pushCandidate(repaired);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return candidates;
|
|
223
|
+
}
|
|
224
|
+
function parseAuthProfilesJson(rawValue) {
|
|
225
|
+
if (!rawValue || rawValue.trim().length === 0) {
|
|
226
|
+
return { profiles: [], error: null };
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
let parsed;
|
|
230
|
+
let parsedSuccessfully = false;
|
|
231
|
+
let lastError;
|
|
232
|
+
for (const candidate of buildAuthProfilesParseCandidates(rawValue)) {
|
|
233
|
+
try {
|
|
234
|
+
parsed = candidate;
|
|
235
|
+
for (let depth = 0; depth < 3 && typeof parsed === "string"; depth += 1) {
|
|
236
|
+
parsed = JSON.parse(parsed);
|
|
237
|
+
}
|
|
238
|
+
parsedSuccessfully = true;
|
|
239
|
+
break;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
lastError = error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!parsedSuccessfully) {
|
|
245
|
+
throw lastError ?? new Error("AUTH_PROFILES_JSON is not valid JSON.");
|
|
246
|
+
}
|
|
247
|
+
if (!Array.isArray(parsed)) {
|
|
248
|
+
return {
|
|
249
|
+
profiles: [],
|
|
250
|
+
error: {
|
|
251
|
+
message: "AUTH_PROFILES_JSON must be a JSON array.",
|
|
252
|
+
rawValue
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const profiles = [];
|
|
257
|
+
for (const [index, entry] of parsed.entries()) {
|
|
258
|
+
if (!entry || typeof entry !== "object") {
|
|
259
|
+
return {
|
|
260
|
+
profiles: [],
|
|
261
|
+
error: {
|
|
262
|
+
message: `AUTH_PROFILES_JSON entry ${index} is not an object.`,
|
|
263
|
+
rawValue
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const candidate = entry;
|
|
268
|
+
if (typeof candidate.id !== "string" || candidate.id.trim().length === 0) {
|
|
269
|
+
return {
|
|
270
|
+
profiles: [],
|
|
271
|
+
error: {
|
|
272
|
+
message: `AUTH_PROFILES_JSON entry ${index} is missing a valid id.`,
|
|
273
|
+
rawValue
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (candidate.mode !== "authorized" && candidate.mode !== "licensed") {
|
|
278
|
+
return {
|
|
279
|
+
profiles: [],
|
|
280
|
+
error: {
|
|
281
|
+
message: `AUTH_PROFILES_JSON entry ${index} has invalid mode "${String(candidate.mode)}".`,
|
|
282
|
+
rawValue
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (!Array.isArray(candidate.sourcePatterns) || candidate.sourcePatterns.some((pattern) => typeof pattern !== "string")) {
|
|
287
|
+
return {
|
|
288
|
+
profiles: [],
|
|
289
|
+
error: {
|
|
290
|
+
message: `AUTH_PROFILES_JSON entry ${index} must define sourcePatterns as an array of strings.`,
|
|
291
|
+
rawValue
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
profiles.push({
|
|
296
|
+
id: candidate.id.trim(),
|
|
297
|
+
mode: candidate.mode,
|
|
298
|
+
sourcePatterns: candidate.sourcePatterns.map((pattern) => pattern.trim()).filter(Boolean),
|
|
299
|
+
cookieFile: candidate.cookieFile,
|
|
300
|
+
usernameEnv: candidate.usernameEnv,
|
|
301
|
+
passwordEnv: candidate.passwordEnv,
|
|
302
|
+
apiKeyEnv: candidate.apiKeyEnv,
|
|
303
|
+
storageStatePath: candidate.storageStatePath,
|
|
304
|
+
sessionTtlMinutes: candidate.sessionTtlMinutes
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return { profiles, error: null };
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
profiles: [],
|
|
311
|
+
error: {
|
|
312
|
+
message: "AUTH_PROFILES_JSON is not valid JSON.",
|
|
313
|
+
details: error instanceof Error ? error.message : String(error),
|
|
314
|
+
rawValue
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function resolveAuthProfilesFromEnv(env = process.env) {
|
|
320
|
+
if (env === process.env) {
|
|
321
|
+
loadWorkspaceEnv();
|
|
322
|
+
}
|
|
323
|
+
return parseAuthProfilesJson(env.AUTH_PROFILES_JSON);
|
|
324
|
+
}
|
|
325
|
+
function resolveStorageStatePath(profile, cwd = process.cwd()) {
|
|
326
|
+
return profile.storageStatePath ? path2.resolve(cwd, profile.storageStatePath) : path2.resolve(cwd, "playwright", ".auth", `${profile.id}.json`);
|
|
327
|
+
}
|
|
328
|
+
function inspectSessionState(profile, cwd = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
329
|
+
const storageStatePath = resolveStorageStatePath(profile, cwd);
|
|
330
|
+
const exists = pathExists(storageStatePath);
|
|
331
|
+
const stat = exists ? statFile(storageStatePath) : null;
|
|
332
|
+
const lastModifiedAtIso = stat?.mtime.toISOString() ?? null;
|
|
333
|
+
const ttlMinutes = profile.sessionTtlMinutes ?? 6 * 60;
|
|
334
|
+
let expired = true;
|
|
335
|
+
if (exists && stat) {
|
|
336
|
+
expired = now.getTime() - stat.mtime.getTime() > ttlMinutes * 60 * 1e3;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
profileId: profile.id,
|
|
340
|
+
storageStatePath,
|
|
341
|
+
exists,
|
|
342
|
+
lastModifiedAtIso,
|
|
343
|
+
expired
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function inspectSessionStates(profiles, cwd = process.cwd(), now = /* @__PURE__ */ new Date()) {
|
|
347
|
+
return profiles.map((profile) => inspectSessionState(profile, cwd, now));
|
|
348
|
+
}
|
|
349
|
+
function findAuthRelevantProfiles(profiles, sourceNames) {
|
|
350
|
+
const loweredSources = sourceNames.map(normalizeSourcePattern);
|
|
351
|
+
return profiles.map((profile) => {
|
|
352
|
+
const matchedSources = loweredSources.filter(
|
|
353
|
+
(source) => profile.sourcePatterns.some((pattern) => {
|
|
354
|
+
try {
|
|
355
|
+
return new RegExp(pattern, "i").test(source);
|
|
356
|
+
} catch {
|
|
357
|
+
return source.includes(normalizeSourcePattern(pattern));
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
return { profile, matchedSources };
|
|
362
|
+
}).filter((entry) => entry.matchedSources.length > 0);
|
|
363
|
+
}
|
|
364
|
+
function buildAuthCaptureCommand(profile, sourceUrl, storageStatePath = resolveStorageStatePath(profile)) {
|
|
365
|
+
const command = `artbot auth capture ${profile.id}`;
|
|
366
|
+
return {
|
|
367
|
+
profileId: profile.id,
|
|
368
|
+
sourceUrl,
|
|
369
|
+
storageStatePath,
|
|
370
|
+
command
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/setup/workflow.ts
|
|
375
|
+
import * as clack from "@clack/prompts";
|
|
376
|
+
import picocolors from "picocolors";
|
|
377
|
+
|
|
378
|
+
// src/setup/backend.ts
|
|
379
|
+
import fs3 from "node:fs";
|
|
380
|
+
import path3 from "node:path";
|
|
381
|
+
import { spawn } from "node:child_process";
|
|
382
|
+
function resolveBackendStartMetadata(cwd = process.cwd(), apiBaseUrl = "http://localhost:4000") {
|
|
383
|
+
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
384
|
+
return {
|
|
385
|
+
api: {
|
|
386
|
+
service: "api",
|
|
387
|
+
command: "pnpm --filter @artbot/api dev",
|
|
388
|
+
cwd: workspaceRoot,
|
|
389
|
+
displayName: "ArtBot API"
|
|
390
|
+
},
|
|
391
|
+
worker: {
|
|
392
|
+
service: "worker",
|
|
393
|
+
command: "pnpm --filter @artbot/worker dev",
|
|
394
|
+
cwd: workspaceRoot,
|
|
395
|
+
displayName: "ArtBot worker"
|
|
396
|
+
},
|
|
397
|
+
apiHealthPath: `${apiBaseUrl.replace(/\/$/, "")}/health`,
|
|
398
|
+
recommendedEntryCommand: "pnpm run start:artbot"
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function spawnDetachedProcess(command, cwd, logPath) {
|
|
402
|
+
const logFd = fs3.openSync(logPath, "a");
|
|
403
|
+
const child = spawn(command, {
|
|
404
|
+
cwd,
|
|
405
|
+
shell: true,
|
|
406
|
+
detached: true,
|
|
407
|
+
stdio: ["ignore", logFd, logFd]
|
|
408
|
+
});
|
|
409
|
+
child.unref();
|
|
410
|
+
fs3.closeSync(logFd);
|
|
411
|
+
return child.pid ?? -1;
|
|
412
|
+
}
|
|
413
|
+
function startLocalBackendServices(cwd = process.cwd()) {
|
|
414
|
+
if (!hasLocalBackendWorkspace(cwd)) {
|
|
415
|
+
throw new Error("Local backend auto-start is only available from an ArtBot workspace checkout.");
|
|
416
|
+
}
|
|
417
|
+
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
418
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
419
|
+
const logDir = path3.resolve(workspaceRoot, ".artbot-logs");
|
|
420
|
+
fs3.mkdirSync(logDir, { recursive: true });
|
|
421
|
+
const apiLogPath = path3.join(logDir, `api-${stamp}.log`);
|
|
422
|
+
const workerLogPath = path3.join(logDir, `worker-${stamp}.log`);
|
|
423
|
+
const metadata = resolveBackendStartMetadata(workspaceRoot);
|
|
424
|
+
const apiPid = spawnDetachedProcess(metadata.api.command, workspaceRoot, apiLogPath);
|
|
425
|
+
const workerPid = spawnDetachedProcess(metadata.worker.command, workspaceRoot, workerLogPath);
|
|
426
|
+
return {
|
|
427
|
+
logDir,
|
|
428
|
+
apiLogPath,
|
|
429
|
+
workerLogPath,
|
|
430
|
+
apiPid,
|
|
431
|
+
workerPid
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/setup/health.ts
|
|
436
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
437
|
+
const controller = new AbortController();
|
|
438
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
439
|
+
try {
|
|
440
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
441
|
+
} finally {
|
|
442
|
+
clearTimeout(timeout);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function checkLlmHealth(baseUrl, apiKey = "", timeoutMs = 1500) {
|
|
446
|
+
const headers = {};
|
|
447
|
+
if (apiKey) {
|
|
448
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const response = await fetchWithTimeout(`${baseUrl.replace(/\/$/, "")}/models`, { headers }, timeoutMs);
|
|
452
|
+
if (!response.ok) {
|
|
453
|
+
return {
|
|
454
|
+
ok: false,
|
|
455
|
+
baseUrl,
|
|
456
|
+
statusCode: response.status,
|
|
457
|
+
reason: `HTTP ${response.status}`
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const payload = await response.json();
|
|
461
|
+
return {
|
|
462
|
+
ok: true,
|
|
463
|
+
baseUrl,
|
|
464
|
+
modelId: payload.data?.[0]?.id
|
|
465
|
+
};
|
|
466
|
+
} catch (error) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
baseUrl,
|
|
470
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function checkApiHealth(apiBaseUrl, apiKey = "", timeoutMs = 1500) {
|
|
475
|
+
const headers = {};
|
|
476
|
+
if (apiKey) {
|
|
477
|
+
headers["x-api-key"] = apiKey;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const response = await fetchWithTimeout(`${apiBaseUrl.replace(/\/$/, "")}/health`, { headers }, timeoutMs);
|
|
481
|
+
if (!response.ok) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
apiBaseUrl,
|
|
485
|
+
statusCode: response.status,
|
|
486
|
+
reason: `HTTP ${response.status}`
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return { ok: true, apiBaseUrl };
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return {
|
|
492
|
+
ok: false,
|
|
493
|
+
apiBaseUrl,
|
|
494
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/setup/workflow.ts
|
|
500
|
+
function createIssue(severity, code, message, detail) {
|
|
501
|
+
return { severity, code, message, detail };
|
|
502
|
+
}
|
|
503
|
+
async function assessLocalSetup(env = process.env, cwd = process.cwd()) {
|
|
504
|
+
if (env === process.env) {
|
|
505
|
+
loadWorkspaceEnv(cwd);
|
|
506
|
+
}
|
|
507
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
508
|
+
const resolvedCwd = resolveWorkspaceRoot(cwd);
|
|
509
|
+
const envPath = resolveEnvFilePath(resolvedCwd);
|
|
510
|
+
const localBackendAvailable = hasLocalBackendWorkspace(cwd);
|
|
511
|
+
const llmBaseUrl = env.LLM_BASE_URL?.trim() || "http://127.0.0.1:1234/v1";
|
|
512
|
+
const apiBaseUrl = env.API_BASE_URL?.trim() || "http://localhost:4000";
|
|
513
|
+
const llmHealth = await checkLlmHealth(llmBaseUrl, env.LLM_API_KEY ?? env.OPENAI_API_KEY ?? "lm-studio");
|
|
514
|
+
const apiHealth = await checkApiHealth(apiBaseUrl, env.ARTBOT_API_KEY);
|
|
515
|
+
const parsedProfiles = parseAuthProfilesJson(env.AUTH_PROFILES_JSON);
|
|
516
|
+
const profiles = parsedProfiles.profiles;
|
|
517
|
+
const enableOptionalProbes = parseBooleanEnv(env.ENABLE_OPTIONAL_PROBE_ADAPTERS, false);
|
|
518
|
+
const enableLicensedIntegrations = parseBooleanEnv(env.ENABLE_LICENSED_INTEGRATIONS, false);
|
|
519
|
+
const enabledSourceNames = [
|
|
520
|
+
...enableOptionalProbes ? ["Artsy", "MutualArt", "askART"] : [],
|
|
521
|
+
...enableLicensedIntegrations ? ["Sanatfiyat"] : []
|
|
522
|
+
];
|
|
523
|
+
const relevantProfiles = findAuthRelevantProfiles(profiles, enabledSourceNames);
|
|
524
|
+
const sessionStates = inspectSessionStates(relevantProfiles.map((entry) => entry.profile), resolvedCwd);
|
|
525
|
+
const issues = [];
|
|
526
|
+
if (!llmHealth.ok) {
|
|
527
|
+
issues.push(createIssue("error", "llm_unreachable", "LM Studio is not reachable.", llmHealth.reason));
|
|
528
|
+
}
|
|
529
|
+
if (!apiHealth.ok) {
|
|
530
|
+
issues.push(createIssue("warning", "api_unreachable", "ArtBot API is not reachable.", apiHealth.reason));
|
|
531
|
+
if (!localBackendAvailable) {
|
|
532
|
+
issues.push(
|
|
533
|
+
createIssue(
|
|
534
|
+
"warning",
|
|
535
|
+
"local_backend_unavailable",
|
|
536
|
+
"Local backend auto-start is unavailable outside the ArtBot repo.",
|
|
537
|
+
"Set API_BASE_URL to a running ArtBot API."
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (parsedProfiles.error) {
|
|
543
|
+
issues.push(createIssue("error", "auth_profiles_invalid", parsedProfiles.error.message, parsedProfiles.error.details));
|
|
544
|
+
}
|
|
545
|
+
if (enabledSourceNames.length > 0 && profiles.length === 0) {
|
|
546
|
+
issues.push(createIssue("warning", "auth_profiles_missing", "Auth-capable sources are enabled but no auth profiles are configured."));
|
|
547
|
+
}
|
|
548
|
+
for (const session of sessionStates) {
|
|
549
|
+
if (!session.exists) {
|
|
550
|
+
issues.push(createIssue("warning", "auth_session_missing", `Missing browser session for ${session.profileId}.`, session.storageStatePath));
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (session.expired) {
|
|
554
|
+
issues.push(createIssue("warning", "auth_session_expired", `Saved browser session expired for ${session.profileId}.`, session.storageStatePath));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
cwd: resolvedCwd,
|
|
559
|
+
workspaceRoot,
|
|
560
|
+
envPath,
|
|
561
|
+
localBackendAvailable,
|
|
562
|
+
llmBaseUrl,
|
|
563
|
+
apiBaseUrl,
|
|
564
|
+
llmHealth,
|
|
565
|
+
apiHealth,
|
|
566
|
+
profiles,
|
|
567
|
+
authProfilesError: parsedProfiles.error,
|
|
568
|
+
relevantProfiles,
|
|
569
|
+
sessionStates,
|
|
570
|
+
issues
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function runSetupWizard(cwd = process.cwd()) {
|
|
574
|
+
const env = process.env;
|
|
575
|
+
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
|
576
|
+
const localBackendAvailable = hasLocalBackendWorkspace(cwd);
|
|
577
|
+
const llmBaseUrl = await clack.text({
|
|
578
|
+
message: "LM Studio base URL",
|
|
579
|
+
initialValue: env.LLM_BASE_URL?.trim() || "http://127.0.0.1:1234/v1",
|
|
580
|
+
validate(input) {
|
|
581
|
+
return input.trim().length === 0 ? "LM Studio URL is required." : void 0;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
if (clack.isCancel(llmBaseUrl)) throw new Error("Setup cancelled.");
|
|
585
|
+
const apiBaseUrl = await clack.text({
|
|
586
|
+
message: "ArtBot API base URL",
|
|
587
|
+
initialValue: env.API_BASE_URL?.trim() || "http://localhost:4000",
|
|
588
|
+
validate(input) {
|
|
589
|
+
return input.trim().length === 0 ? "API URL is required." : void 0;
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
if (clack.isCancel(apiBaseUrl)) throw new Error("Setup cancelled.");
|
|
593
|
+
const enableOptionalProbes = await clack.confirm({
|
|
594
|
+
message: "Enable optional probe sources (Artsy, MutualArt, askART)?",
|
|
595
|
+
initialValue: parseBooleanEnv(env.ENABLE_OPTIONAL_PROBE_ADAPTERS, false)
|
|
596
|
+
});
|
|
597
|
+
if (clack.isCancel(enableOptionalProbes)) throw new Error("Setup cancelled.");
|
|
598
|
+
const enableLicensedIntegrations = await clack.confirm({
|
|
599
|
+
message: "Enable licensed integrations?",
|
|
600
|
+
initialValue: parseBooleanEnv(env.ENABLE_LICENSED_INTEGRATIONS, true)
|
|
601
|
+
});
|
|
602
|
+
if (clack.isCancel(enableLicensedIntegrations)) throw new Error("Setup cancelled.");
|
|
603
|
+
const defaultLicensedIntegrations = enableLicensedIntegrations ? ["Sanatfiyat"] : [];
|
|
604
|
+
const authProfiles = buildDefaultAuthProfiles({
|
|
605
|
+
cwd: workspaceRoot,
|
|
606
|
+
enableOptionalProbes,
|
|
607
|
+
enableLicensedIntegrations
|
|
608
|
+
});
|
|
609
|
+
const values = {
|
|
610
|
+
llmBaseUrl: llmBaseUrl.trim(),
|
|
611
|
+
apiBaseUrl: apiBaseUrl.trim(),
|
|
612
|
+
enableOptionalProbes,
|
|
613
|
+
enableLicensedIntegrations,
|
|
614
|
+
defaultLicensedIntegrations,
|
|
615
|
+
authProfiles
|
|
616
|
+
};
|
|
617
|
+
const envPath = resolveEnvFilePath(workspaceRoot);
|
|
618
|
+
upsertEnvFile(envPath, buildSetupEnvUpdates(values));
|
|
619
|
+
clack.log.success(`Updated ${picocolors.bold(envPath)}`);
|
|
620
|
+
let backendStart = null;
|
|
621
|
+
const apiHealth = await checkApiHealth(values.apiBaseUrl, process.env.ARTBOT_API_KEY);
|
|
622
|
+
if (!apiHealth.ok && localBackendAvailable) {
|
|
623
|
+
const shouldStartBackend = await clack.confirm({
|
|
624
|
+
message: "ArtBot API is offline. Start local API and worker now?",
|
|
625
|
+
initialValue: true
|
|
626
|
+
});
|
|
627
|
+
if (clack.isCancel(shouldStartBackend)) throw new Error("Setup cancelled.");
|
|
628
|
+
if (shouldStartBackend) {
|
|
629
|
+
backendStart = startLocalBackendServices(workspaceRoot);
|
|
630
|
+
clack.log.info(`Started local backend. API log: ${backendStart.apiLogPath}`);
|
|
631
|
+
clack.log.info(`Worker log: ${backendStart.workerLogPath}`);
|
|
632
|
+
}
|
|
633
|
+
} else if (!apiHealth.ok) {
|
|
634
|
+
clack.log.info("Local backend auto-start is only available inside the ArtBot repo.");
|
|
635
|
+
clack.log.info("Set API_BASE_URL to a running ArtBot API or start the services manually.");
|
|
636
|
+
}
|
|
637
|
+
const captureNow = authProfiles.length > 0 ? await clack.confirm({
|
|
638
|
+
message: "Capture browser login sessions now?",
|
|
639
|
+
initialValue: false
|
|
640
|
+
}) : false;
|
|
641
|
+
if (clack.isCancel(captureNow)) throw new Error("Setup cancelled.");
|
|
642
|
+
if (captureNow) {
|
|
643
|
+
for (const profile of authProfiles) {
|
|
644
|
+
const shouldCaptureProfile = await clack.confirm({
|
|
645
|
+
message: `Capture session for ${profile.id}?`,
|
|
646
|
+
initialValue: !profile.id.startsWith("artsy") && !profile.id.startsWith("askart")
|
|
647
|
+
});
|
|
648
|
+
if (clack.isCancel(shouldCaptureProfile)) throw new Error("Setup cancelled.");
|
|
649
|
+
if (!shouldCaptureProfile) continue;
|
|
650
|
+
const command = buildAuthCaptureCommand(profile, defaultSourceUrlForProfile(profile.id));
|
|
651
|
+
clack.log.message(`${picocolors.cyan("Auth capture")}: ${command.command}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const assessment = await assessLocalSetup(process.env, workspaceRoot);
|
|
655
|
+
return { assessment, backendStart };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export {
|
|
659
|
+
pathExists,
|
|
660
|
+
loadWorkspaceEnv,
|
|
661
|
+
defaultSourceUrlForProfile,
|
|
662
|
+
resolveAuthProfilesFromEnv,
|
|
663
|
+
buildAuthCaptureCommand,
|
|
664
|
+
assessLocalSetup,
|
|
665
|
+
runSetupWizard
|
|
666
|
+
};
|
|
667
|
+
//# sourceMappingURL=chunk-KKAF45ZB.js.map
|