@starlight-ai/discord-waifus 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/dist/index.js ADDED
@@ -0,0 +1,1669 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import path8 from "path";
5
+ import process4 from "process";
6
+ import { spawnSync } from "child_process";
7
+ import { promises as fs7 } from "fs";
8
+ import { cac } from "cac";
9
+ import { parse as parseToml } from "smol-toml";
10
+ import pc2 from "picocolors";
11
+
12
+ // src/config-store.ts
13
+ import { promises as fs } from "fs";
14
+ import os from "os";
15
+ import path from "path";
16
+ import { z } from "zod";
17
+ var cliConfigSchema = z.object({
18
+ defaultProjectRoot: z.string().min(1).nullable().default(null)
19
+ });
20
+ function getCliConfigPath() {
21
+ const configHome = process.env.WAIFUS_CONFIG_HOME?.trim();
22
+ if (configHome) {
23
+ return path.resolve(configHome, "config.json");
24
+ }
25
+ return path.join(os.homedir(), ".config", "waifus", "config.json");
26
+ }
27
+ async function loadCliConfig() {
28
+ const filePath = getCliConfigPath();
29
+ try {
30
+ const raw = await fs.readFile(filePath, "utf8");
31
+ return cliConfigSchema.parse(JSON.parse(raw));
32
+ } catch (error) {
33
+ const code = error?.code;
34
+ if (code === "ENOENT") {
35
+ return cliConfigSchema.parse({});
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ async function saveCliConfig(nextConfig) {
41
+ const filePath = getCliConfigPath();
42
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
43
+ await fs.writeFile(filePath, `${JSON.stringify(cliConfigSchema.parse(nextConfig), null, 2)}
44
+ `, "utf8");
45
+ }
46
+
47
+ // src/project-root.ts
48
+ import { promises as fs2 } from "fs";
49
+ import path2 from "path";
50
+ var projectMarkers = [
51
+ "package.json",
52
+ "pnpm-workspace.yaml",
53
+ path2.join("packages", "backend"),
54
+ path2.join("packages", "dashboard"),
55
+ "defaults"
56
+ ];
57
+ async function isValidProjectRoot(projectRoot) {
58
+ const resolvedRoot = path2.resolve(projectRoot);
59
+ for (const marker of projectMarkers) {
60
+ try {
61
+ await fs2.access(path2.join(resolvedRoot, marker));
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+ return true;
67
+ }
68
+ async function resolveProjectRoot(options = {}) {
69
+ const cwd = path2.resolve(options.cwd ?? process.cwd());
70
+ const explicitProjectRoot = options.explicitProjectRoot ? path2.resolve(options.explicitProjectRoot) : null;
71
+ if (explicitProjectRoot && await isValidProjectRoot(explicitProjectRoot)) {
72
+ return explicitProjectRoot;
73
+ }
74
+ const config = await loadCliConfig();
75
+ if (config.defaultProjectRoot && await isValidProjectRoot(config.defaultProjectRoot)) {
76
+ return path2.resolve(config.defaultProjectRoot);
77
+ }
78
+ for (const candidate of walkParentDirectories(cwd)) {
79
+ if (await isValidProjectRoot(candidate)) {
80
+ return candidate;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ async function assertProjectRoot(projectRoot) {
86
+ const resolvedRoot = path2.resolve(projectRoot);
87
+ if (!await isValidProjectRoot(resolvedRoot)) {
88
+ throw new Error(
89
+ `Invalid Discord Waifus project root: ${resolvedRoot}
90
+ Expected: package.json, pnpm-workspace.yaml, packages/backend/, packages/dashboard/, and defaults/`
91
+ );
92
+ }
93
+ return resolvedRoot;
94
+ }
95
+ function* walkParentDirectories(start) {
96
+ let current = start;
97
+ while (true) {
98
+ yield current;
99
+ const parent = path2.dirname(current);
100
+ if (parent === current) {
101
+ return;
102
+ }
103
+ current = parent;
104
+ }
105
+ }
106
+
107
+ // src/command-utils.ts
108
+ import { promises as fs4 } from "fs";
109
+ import path4 from "path";
110
+ import process2 from "process";
111
+ import { spawn } from "child_process";
112
+ import pc from "picocolors";
113
+
114
+ // src/runtime-layout.ts
115
+ import { promises as fs3 } from "fs";
116
+ import path3 from "path";
117
+ import { stringify } from "smol-toml";
118
+ function getRuntimeLayoutPaths(projectRoot) {
119
+ const defaultsRoot = path3.join(projectRoot, "defaults");
120
+ const runtimeRoot = path3.join(projectRoot, ".waifus");
121
+ const runtimeStateRoot = path3.join(runtimeRoot, "state");
122
+ const legacyConfigRoot = path3.join(projectRoot, "config");
123
+ const legacyDataRoot = path3.join(projectRoot, "data");
124
+ return {
125
+ defaultsRoot,
126
+ runtimeRoot,
127
+ runtimeConfigFile: path3.join(runtimeRoot, "config.toml"),
128
+ runtimeProvidersFile: path3.join(runtimeRoot, "providers.toml"),
129
+ runtimeKeysFile: path3.join(runtimeRoot, "keys.toml"),
130
+ runtimeChannelsFile: path3.join(runtimeRoot, "channels.toml"),
131
+ runtimeOrchestratorFile: path3.join(runtimeRoot, "orchestrator.toml"),
132
+ runtimeStageManagerFile: path3.join(runtimeRoot, "stage-manager.toml"),
133
+ runtimeWaifusRoot: path3.join(runtimeRoot, "waifus"),
134
+ runtimeStageManagerDataRoot: path3.join(runtimeRoot, "stage-manager-data"),
135
+ runtimeAssetsWaifusRoot: path3.join(runtimeRoot, "assets", "waifus"),
136
+ runtimeStateRoot,
137
+ migrationStateFile: path3.join(runtimeStateRoot, "migration-state.json"),
138
+ migrationWarningsFile: path3.join(runtimeStateRoot, "migration-warnings.json"),
139
+ stageManagerCheckpointsFile: path3.join(runtimeStateRoot, "stage-manager-checkpoints.json"),
140
+ defaultsConfigFile: path3.join(defaultsRoot, "config.toml"),
141
+ defaultsChannelsFile: path3.join(defaultsRoot, "channels.toml"),
142
+ defaultsOrchestratorFile: path3.join(defaultsRoot, "orchestrator.toml"),
143
+ defaultsStageManagerFile: path3.join(defaultsRoot, "stage-manager.toml"),
144
+ defaultsProviderCatalogFile: path3.join(defaultsRoot, "providers.catalog.json"),
145
+ defaultsWaifuTemplateFile: path3.join(defaultsRoot, "waifus", "default-waifu.json"),
146
+ legacyConfigRoot,
147
+ legacyDataRoot,
148
+ legacyWaifusFile: path3.join(legacyConfigRoot, "waifus.json"),
149
+ legacyProvidersFile: path3.join(legacyConfigRoot, "providers.json"),
150
+ legacyChannelsFile: path3.join(legacyConfigRoot, "channels.json"),
151
+ legacyOrchestratorFile: path3.join(legacyConfigRoot, "orchestrator.json"),
152
+ legacyStageManagerFile: path3.join(legacyConfigRoot, "stage-manager.json"),
153
+ legacyStageManagerStateFile: path3.join(legacyDataRoot, "stage-manager-state.json")
154
+ };
155
+ }
156
+ async function inspectRuntimeState(projectRoot) {
157
+ const paths = getRuntimeLayoutPaths(projectRoot);
158
+ const runtimeRootExists = await fileExists(paths.runtimeRoot);
159
+ const legacyLiveExists = await hasLegacyLiveConfig(projectRoot);
160
+ const migrationState = await readMigrationState(paths.migrationStateFile);
161
+ const isCanonicalLocalRuntime = Boolean(runtimeRootExists) && (migrationState?.status === "import_completed" || migrationState?.status === "bootstrap_empty" && !legacyLiveExists || !migrationState && !legacyLiveExists);
162
+ return {
163
+ paths,
164
+ runtimeRootExists,
165
+ legacyLiveExists,
166
+ migrationState,
167
+ isCanonicalLocalRuntime,
168
+ isMigrationPending: Boolean(runtimeRootExists && legacyLiveExists && migrationState?.status !== "import_completed")
169
+ };
170
+ }
171
+ async function hasLegacyLiveConfig(projectRoot) {
172
+ const paths = getRuntimeLayoutPaths(projectRoot);
173
+ const candidates = [
174
+ paths.legacyWaifusFile,
175
+ paths.legacyProvidersFile,
176
+ paths.legacyChannelsFile,
177
+ paths.legacyOrchestratorFile,
178
+ paths.legacyStageManagerFile,
179
+ paths.legacyStageManagerStateFile
180
+ ];
181
+ for (const candidate of candidates) {
182
+ if (await fileExists(candidate)) {
183
+ return true;
184
+ }
185
+ }
186
+ return false;
187
+ }
188
+ async function bootstrapLocalRuntime(projectRoot) {
189
+ const inspection = await inspectRuntimeState(projectRoot);
190
+ if (inspection.legacyLiveExists && inspection.migrationState?.status !== "import_completed") {
191
+ throw new Error("Legacy live config still exists. Run: waifus migrate-local-config");
192
+ }
193
+ return seedLocalRuntimeFromDefaults(projectRoot, {
194
+ writeBootstrapMigrationState: !inspection.migrationState && !inspection.legacyLiveExists
195
+ });
196
+ }
197
+ async function seedLocalRuntimeFromDefaults(projectRoot, options) {
198
+ const inspection = await inspectRuntimeState(projectRoot);
199
+ const paths = inspection.paths;
200
+ const written = [];
201
+ await Promise.all([
202
+ fs3.mkdir(paths.runtimeWaifusRoot, { recursive: true }),
203
+ fs3.mkdir(paths.runtimeStageManagerDataRoot, { recursive: true }),
204
+ fs3.mkdir(paths.runtimeAssetsWaifusRoot, { recursive: true }),
205
+ fs3.mkdir(paths.runtimeStateRoot, { recursive: true })
206
+ ]);
207
+ written.push(
208
+ await ensureCopied(paths.defaultsConfigFile, paths.runtimeConfigFile),
209
+ await ensureCopied(paths.defaultsChannelsFile, paths.runtimeChannelsFile),
210
+ await ensureSeededProviders(paths.defaultsProviderCatalogFile, paths.runtimeProvidersFile),
211
+ await ensureFile(paths.runtimeKeysFile, `${stringify({ provider_keys: [] })}
212
+ `),
213
+ await ensureCopied(paths.defaultsOrchestratorFile, paths.runtimeOrchestratorFile),
214
+ await ensureCopied(paths.defaultsStageManagerFile, paths.runtimeStageManagerFile),
215
+ await ensureJsonFile(paths.stageManagerCheckpointsFile, { guilds: {} }),
216
+ await ensureJsonFile(paths.migrationWarningsFile, {
217
+ schemaVersion: 1,
218
+ globalWarnings: [],
219
+ waifuWarnings: {}
220
+ })
221
+ );
222
+ if (options.writeBootstrapMigrationState) {
223
+ written.push(
224
+ await ensureJsonFile(paths.migrationStateFile, {
225
+ schemaVersion: 1,
226
+ status: "bootstrap_empty",
227
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
228
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
229
+ })
230
+ );
231
+ }
232
+ return written.filter(Boolean);
233
+ }
234
+ function defaultSeedFiles(projectRoot) {
235
+ const paths = getRuntimeLayoutPaths(projectRoot);
236
+ return [
237
+ paths.defaultsConfigFile,
238
+ paths.defaultsChannelsFile,
239
+ paths.defaultsOrchestratorFile,
240
+ paths.defaultsStageManagerFile,
241
+ paths.defaultsProviderCatalogFile,
242
+ paths.defaultsWaifuTemplateFile
243
+ ];
244
+ }
245
+ function localRuntimeFiles(projectRoot) {
246
+ const paths = getRuntimeLayoutPaths(projectRoot);
247
+ return [
248
+ paths.runtimeConfigFile,
249
+ paths.runtimeProvidersFile,
250
+ paths.runtimeKeysFile,
251
+ paths.runtimeChannelsFile,
252
+ paths.runtimeOrchestratorFile,
253
+ paths.runtimeStageManagerFile,
254
+ paths.migrationStateFile,
255
+ paths.migrationWarningsFile,
256
+ paths.stageManagerCheckpointsFile
257
+ ];
258
+ }
259
+ async function ensureSeededProviders(catalogFile, providersFile) {
260
+ if (await fileExists(providersFile)) {
261
+ return "";
262
+ }
263
+ const catalog = JSON.parse(await fs3.readFile(catalogFile, "utf8"));
264
+ const tomlValue = {
265
+ providers: (catalog.providers ?? []).map((entry) => ({
266
+ id: entry.id,
267
+ origin: "built-in",
268
+ name: entry.name,
269
+ type: entry.type,
270
+ auth_mode: entry.authMode ?? "required",
271
+ enabled: entry.enabledByDefault ?? false,
272
+ base_url: entry.baseUrl,
273
+ models: entry.models ?? []
274
+ }))
275
+ };
276
+ await fs3.mkdir(path3.dirname(providersFile), { recursive: true });
277
+ await fs3.writeFile(providersFile, `${stringify(tomlValue)}
278
+ `, "utf8");
279
+ return providersFile;
280
+ }
281
+ async function ensureCopied(sourceFile, targetFile) {
282
+ if (await fileExists(targetFile)) {
283
+ return "";
284
+ }
285
+ const contents = await fs3.readFile(sourceFile, "utf8");
286
+ await fs3.mkdir(path3.dirname(targetFile), { recursive: true });
287
+ await fs3.writeFile(targetFile, contents, "utf8");
288
+ return targetFile;
289
+ }
290
+ async function ensureFile(targetFile, contents) {
291
+ if (await fileExists(targetFile)) {
292
+ return "";
293
+ }
294
+ await fs3.mkdir(path3.dirname(targetFile), { recursive: true });
295
+ await fs3.writeFile(targetFile, contents, "utf8");
296
+ return targetFile;
297
+ }
298
+ async function ensureJsonFile(targetFile, value) {
299
+ return ensureFile(targetFile, `${JSON.stringify(value, null, 2)}
300
+ `);
301
+ }
302
+ async function readMigrationState(filePath) {
303
+ try {
304
+ return JSON.parse(await fs3.readFile(filePath, "utf8"));
305
+ } catch (error) {
306
+ const code = error?.code;
307
+ if (code === "ENOENT") {
308
+ return null;
309
+ }
310
+ throw error;
311
+ }
312
+ }
313
+ async function fileExists(filePath) {
314
+ try {
315
+ await fs3.access(filePath);
316
+ return true;
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+
322
+ // src/command-utils.ts
323
+ async function requireProjectRoot(options) {
324
+ const projectRoot = await resolveProjectRoot({
325
+ cwd: process2.cwd(),
326
+ explicitProjectRoot: options.project ?? null
327
+ });
328
+ if (!projectRoot) {
329
+ throw new Error("No project root is configured.\nRun: waifus use /path/to/Discord-Waifus");
330
+ }
331
+ return projectRoot;
332
+ }
333
+ function runtimeConfigFiles(projectRoot) {
334
+ return [...defaultSeedFiles(projectRoot), ...localRuntimeFiles(projectRoot)];
335
+ }
336
+ function requiredBuildArtifacts(projectRoot) {
337
+ return [
338
+ path4.join(projectRoot, "packages", "backend", "dist", "index.js"),
339
+ path4.join(projectRoot, "packages", "dashboard", ".next", "BUILD_ID")
340
+ ];
341
+ }
342
+ async function fileExists2(filePath) {
343
+ try {
344
+ await fs4.access(filePath);
345
+ return true;
346
+ } catch {
347
+ return false;
348
+ }
349
+ }
350
+ async function readJsonFile(filePath) {
351
+ return JSON.parse(await fs4.readFile(filePath, "utf8"));
352
+ }
353
+ async function spawnPassthrough(command, args, cwd, envOverrides = {}) {
354
+ await new Promise((resolve, reject) => {
355
+ const child = spawn(command, args, {
356
+ cwd,
357
+ stdio: "inherit",
358
+ env: {
359
+ ...process2.env,
360
+ ...envOverrides
361
+ }
362
+ });
363
+ child.once("error", reject);
364
+ child.once("exit", (code, signal) => {
365
+ if (signal) {
366
+ reject(new Error(`${command} exited due to signal ${signal}`));
367
+ return;
368
+ }
369
+ if (code && code !== 0) {
370
+ reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
371
+ return;
372
+ }
373
+ resolve();
374
+ });
375
+ });
376
+ }
377
+ function info(message) {
378
+ console.log(pc.cyan(message));
379
+ }
380
+ function success(message) {
381
+ console.log(pc.green(message));
382
+ }
383
+ function warn(message) {
384
+ console.log(pc.yellow(message));
385
+ }
386
+
387
+ // src/open-url.ts
388
+ import process3 from "process";
389
+ import { spawn as spawn2 } from "child_process";
390
+ async function openUrl(url) {
391
+ const platform = process3.platform;
392
+ if (platform === "darwin") {
393
+ await spawnDetached("open", [url]);
394
+ return;
395
+ }
396
+ if (platform === "win32") {
397
+ await spawnDetached("cmd", ["/c", "start", "", url]);
398
+ return;
399
+ }
400
+ await spawnDetached("xdg-open", [url]);
401
+ }
402
+ async function spawnDetached(command, args) {
403
+ await new Promise((resolve, reject) => {
404
+ const child = spawn2(command, args, {
405
+ detached: true,
406
+ stdio: "ignore",
407
+ env: process3.env
408
+ });
409
+ child.once("error", reject);
410
+ child.once("spawn", () => {
411
+ child.unref();
412
+ resolve();
413
+ });
414
+ });
415
+ }
416
+
417
+ // src/local-config-migrator.ts
418
+ import { promises as fs5 } from "fs";
419
+ import path5 from "path";
420
+ import { stringify as stringify2 } from "smol-toml";
421
+ async function migrateLocalConfig(projectRoot) {
422
+ const runtimeState = await inspectRuntimeState(projectRoot);
423
+ if (!runtimeState.legacyLiveExists) {
424
+ throw new Error("No legacy runtime files found to import.");
425
+ }
426
+ if (runtimeState.migrationState?.status === "import_completed") {
427
+ throw new Error("Legacy import already completed for this project.");
428
+ }
429
+ const paths = runtimeState.paths;
430
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
431
+ const warnings = {
432
+ schemaVersion: 1,
433
+ globalWarnings: [],
434
+ waifuWarnings: {}
435
+ };
436
+ const [legacyWaifusFile, legacyProvidersFile, legacyChannelsFile, legacyOrchestratorFile, legacyStageManagerFile, legacyStageManagerStateFile, catalogFile] = await Promise.all([
437
+ readJson(paths.legacyWaifusFile, { waifus: [] }),
438
+ readJson(paths.legacyProvidersFile, { providers: [] }),
439
+ readJson(paths.legacyChannelsFile, { channels: [] }),
440
+ readJson(paths.legacyOrchestratorFile, {
441
+ orchestrator: {
442
+ providerId: "configure-me",
443
+ model: "configure-me",
444
+ temperature: 0.7,
445
+ maxTokens: 500
446
+ }
447
+ }),
448
+ readJson(paths.legacyStageManagerFile, {
449
+ stageManager: {
450
+ enabled: true,
451
+ providerId: null,
452
+ model: null,
453
+ temperature: 0.4,
454
+ maxTokens: 500,
455
+ quietPeriodSeconds: 300,
456
+ historyLimit: 60,
457
+ maxRelationshipsPerWaifu: 20,
458
+ maxMemoriesPerWaifu: 8
459
+ }
460
+ }),
461
+ readJson(paths.legacyStageManagerStateFile, {
462
+ waifus: {},
463
+ channels: {}
464
+ }),
465
+ readJson(paths.defaultsProviderCatalogFile, { providers: [] })
466
+ ]);
467
+ const idMap = buildWaifuIdMap(legacyWaifusFile.waifus);
468
+ const catalogById = new Map(catalogFile.providers.map((entry) => [entry.id, entry]));
469
+ const written = await seedLocalRuntimeFromDefaults(projectRoot, {
470
+ writeBootstrapMigrationState: false
471
+ });
472
+ const migratedWaifus = await Promise.all(
473
+ legacyWaifusFile.waifus.map(async (waifu) => {
474
+ const nextId = idMap[waifu.id];
475
+ const migratedAssets = await migrateWaifuAssets(
476
+ paths,
477
+ waifu,
478
+ nextId,
479
+ warnings,
480
+ timestamp
481
+ );
482
+ if (waifu.id !== nextId) {
483
+ addWaifuWarning(warnings, nextId, {
484
+ code: "legacy_id_sanitized",
485
+ field: "id",
486
+ message: `Legacy waifu id "${waifu.id}" was sanitized to "${nextId}".`,
487
+ legacyValue: waifu.id,
488
+ createdAt: timestamp
489
+ });
490
+ }
491
+ return {
492
+ schemaVersion: 1,
493
+ id: nextId,
494
+ name: waifu.name,
495
+ displayName: waifu.displayName,
496
+ botToken: waifu.botToken,
497
+ applicationId: waifu.applicationId,
498
+ enabled: waifu.enabled,
499
+ avatarPath: migratedAssets.avatarPath,
500
+ bannerPath: migratedAssets.bannerPath,
501
+ statusText: waifu.statusText,
502
+ statusType: waifu.statusType,
503
+ personality: {
504
+ ...waifu.personality,
505
+ relationshipsWithOtherWaifus: Object.fromEntries(
506
+ Object.entries(waifu.personality.relationshipsWithOtherWaifus).map(([relationshipId, value]) => [
507
+ idMap[relationshipId] ?? relationshipId,
508
+ value
509
+ ])
510
+ )
511
+ },
512
+ schedule: waifu.schedule,
513
+ ai: {
514
+ ...waifu.ai,
515
+ providerId: waifu.ai.providerId
516
+ }
517
+ };
518
+ })
519
+ );
520
+ const migratedChannels = legacyChannelsFile.channels.map((channel) => ({
521
+ guildId: channel.guildId,
522
+ channelId: channel.channelId,
523
+ channelName: channel.channelName,
524
+ enabled: channel.enabled,
525
+ activeWaifuIds: channel.activeWaifuIds.map((waifuId) => idMap[waifuId] ?? waifuId),
526
+ contextAnchorMessageId: channel.contextAnchorMessageId ?? "",
527
+ contextMessageCount: channel.contextMessageCount,
528
+ idleChatterEnabled: channel.idleChatterEnabled,
529
+ idleTimerMinSeconds: channel.idleTimerMinSeconds,
530
+ idleTimerMaxSeconds: channel.idleTimerMaxSeconds
531
+ }));
532
+ const migratedProviders = legacyProvidersFile.providers.map((provider) => {
533
+ const catalog = catalogById.get(provider.id);
534
+ return {
535
+ id: provider.id,
536
+ origin: catalog ? "built-in" : "custom",
537
+ name: provider.name,
538
+ type: provider.type,
539
+ authMode: catalog?.authMode ?? deriveAuthMode(provider.id),
540
+ enabled: provider.enabled,
541
+ baseUrl: provider.baseUrl,
542
+ models: provider.models
543
+ };
544
+ });
545
+ const migratedProviderKeys = legacyProvidersFile.providers.map((provider) => {
546
+ const authMode = catalogById.get(provider.id)?.authMode ?? deriveAuthMode(provider.id);
547
+ if (authMode === "none" || !provider.apiKey) {
548
+ return null;
549
+ }
550
+ return {
551
+ id: provider.id,
552
+ apiKey: provider.apiKey
553
+ };
554
+ }).filter((entry) => Boolean(entry));
555
+ const waifuGuilds = buildWaifuGuilds(migratedChannels);
556
+ const migratedStageManagerDocuments = Object.fromEntries(
557
+ migratedWaifus.map((waifu) => {
558
+ const legacyState = legacyStageManagerStateFile.waifus[findLegacyIdForNewId(idMap, waifu.id) ?? waifu.id];
559
+ const guildIds = [...waifuGuilds.get(waifu.id) ?? /* @__PURE__ */ new Set()];
560
+ const guilds = {};
561
+ if (legacyState) {
562
+ if (guildIds.length === 0) {
563
+ addWaifuWarning(warnings, waifu.id, {
564
+ code: "stage_manager_guild_missing",
565
+ field: "stageManager.guilds",
566
+ message: "Legacy stage-manager data could not be assigned because this waifu is not active in any migrated guild.",
567
+ createdAt: timestamp
568
+ });
569
+ }
570
+ if (guildIds.length > 1) {
571
+ addWaifuWarning(warnings, waifu.id, {
572
+ code: "stage_manager_state_duplicated",
573
+ field: "stageManager.guilds",
574
+ message: `Legacy stage-manager state was duplicated into ${guildIds.length} guild sections during import.`,
575
+ createdAt: timestamp
576
+ });
577
+ }
578
+ }
579
+ for (const guildId of guildIds) {
580
+ guilds[guildId] = {
581
+ relationshipsByParticipant: Object.fromEntries(
582
+ Object.entries(legacyState?.relationshipsByParticipant ?? {}).map(([participantKey, relationship]) => {
583
+ const targetWaifuId = relationship.targetWaifuId ? idMap[relationship.targetWaifuId] ?? relationship.targetWaifuId : null;
584
+ return [
585
+ remapParticipantKey(participantKey, idMap),
586
+ {
587
+ ...relationship,
588
+ targetWaifuId,
589
+ targetName: relationship.targetKind === "waifu" && targetWaifuId ? migratedWaifus.find((candidate) => candidate.id === targetWaifuId)?.name ?? relationship.targetName : relationship.targetName
590
+ }
591
+ ];
592
+ })
593
+ ),
594
+ memories: [...legacyState?.memories ?? []]
595
+ };
596
+ }
597
+ return [
598
+ waifu.id,
599
+ {
600
+ schemaVersion: 1,
601
+ waifuId: waifu.id,
602
+ guilds
603
+ }
604
+ ];
605
+ })
606
+ );
607
+ const migratedCheckpoints = collapseLegacyCheckpoints(
608
+ legacyStageManagerStateFile.channels,
609
+ migratedChannels,
610
+ warnings,
611
+ timestamp
612
+ );
613
+ await Promise.all([
614
+ atomicWriteFile(
615
+ paths.runtimeProvidersFile,
616
+ `${stringify2({
617
+ providers: migratedProviders.map((provider) => ({
618
+ id: provider.id,
619
+ origin: provider.origin,
620
+ name: provider.name,
621
+ type: provider.type,
622
+ auth_mode: provider.authMode,
623
+ enabled: provider.enabled,
624
+ base_url: provider.baseUrl,
625
+ models: provider.models
626
+ }))
627
+ })}
628
+ `
629
+ ),
630
+ atomicWriteFile(
631
+ paths.runtimeKeysFile,
632
+ `${stringify2({
633
+ provider_keys: migratedProviderKeys.map((entry) => ({
634
+ id: entry.id,
635
+ api_key: entry.apiKey
636
+ }))
637
+ })}
638
+ `
639
+ ),
640
+ atomicWriteFile(
641
+ paths.runtimeChannelsFile,
642
+ `${stringify2({
643
+ channels: migratedChannels.map((channel) => ({
644
+ guild_id: channel.guildId,
645
+ channel_id: channel.channelId,
646
+ channel_name: channel.channelName,
647
+ enabled: channel.enabled,
648
+ active_waifu_ids: channel.activeWaifuIds,
649
+ context_anchor_message_id: channel.contextAnchorMessageId,
650
+ context_message_count: channel.contextMessageCount,
651
+ idle_chatter_enabled: channel.idleChatterEnabled,
652
+ idle_timer_min_seconds: channel.idleTimerMinSeconds,
653
+ idle_timer_max_seconds: channel.idleTimerMaxSeconds
654
+ }))
655
+ })}
656
+ `
657
+ ),
658
+ atomicWriteFile(
659
+ paths.runtimeOrchestratorFile,
660
+ `${stringify2({
661
+ orchestrator: {
662
+ provider_id: asRecord(legacyOrchestratorFile).orchestrator?.providerId ?? "configure-me",
663
+ model: asRecord(legacyOrchestratorFile).orchestrator?.model ?? "configure-me",
664
+ temperature: asRecord(legacyOrchestratorFile).orchestrator?.temperature ?? 0.7,
665
+ max_tokens: asRecord(legacyOrchestratorFile).orchestrator?.maxTokens ?? 500
666
+ }
667
+ })}
668
+ `
669
+ ),
670
+ atomicWriteFile(
671
+ paths.runtimeStageManagerFile,
672
+ `${stringify2({
673
+ stage_manager: {
674
+ enabled: asRecord(legacyStageManagerFile).stageManager?.enabled ?? true,
675
+ provider_id: asRecord(legacyStageManagerFile).stageManager?.providerId ?? "",
676
+ model: asRecord(legacyStageManagerFile).stageManager?.model ?? "",
677
+ temperature: asRecord(legacyStageManagerFile).stageManager?.temperature ?? 0.4,
678
+ max_tokens: asRecord(legacyStageManagerFile).stageManager?.maxTokens ?? 500,
679
+ quiet_period_seconds: asRecord(legacyStageManagerFile).stageManager?.quietPeriodSeconds ?? 300,
680
+ history_limit: asRecord(legacyStageManagerFile).stageManager?.historyLimit ?? 60,
681
+ max_relationships_per_waifu: asRecord(legacyStageManagerFile).stageManager?.maxRelationshipsPerWaifu ?? 20,
682
+ max_memories_per_waifu: asRecord(legacyStageManagerFile).stageManager?.maxMemoriesPerWaifu ?? 8
683
+ }
684
+ })}
685
+ `
686
+ ),
687
+ atomicWriteFile(
688
+ paths.stageManagerCheckpointsFile,
689
+ `${JSON.stringify({ guilds: migratedCheckpoints }, null, 2)}
690
+ `
691
+ ),
692
+ atomicWriteFile(
693
+ paths.migrationWarningsFile,
694
+ `${JSON.stringify(warnings, null, 2)}
695
+ `
696
+ ),
697
+ atomicWriteFile(
698
+ paths.migrationStateFile,
699
+ `${JSON.stringify(
700
+ {
701
+ schemaVersion: 1,
702
+ status: "import_completed",
703
+ createdAt: timestamp,
704
+ completedAt: timestamp
705
+ },
706
+ null,
707
+ 2
708
+ )}
709
+ `
710
+ )
711
+ ]);
712
+ written.push(
713
+ paths.runtimeProvidersFile,
714
+ paths.runtimeKeysFile,
715
+ paths.runtimeChannelsFile,
716
+ paths.runtimeOrchestratorFile,
717
+ paths.runtimeStageManagerFile,
718
+ paths.stageManagerCheckpointsFile,
719
+ paths.migrationWarningsFile,
720
+ paths.migrationStateFile
721
+ );
722
+ for (const waifu of migratedWaifus) {
723
+ const waifuPath = path5.join(paths.runtimeWaifusRoot, `${waifu.id}.json`);
724
+ const stageManagerPath = path5.join(paths.runtimeStageManagerDataRoot, `${waifu.id}.json`);
725
+ await atomicWriteFile(waifuPath, `${JSON.stringify(waifu, null, 2)}
726
+ `);
727
+ await atomicWriteFile(
728
+ stageManagerPath,
729
+ `${JSON.stringify(migratedStageManagerDocuments[waifu.id], null, 2)}
730
+ `
731
+ );
732
+ written.push(waifuPath, stageManagerPath);
733
+ }
734
+ return {
735
+ written: [...new Set(written)],
736
+ idMap,
737
+ warningCount: warnings.globalWarnings.length + Object.values(warnings.waifuWarnings).reduce((count, entries) => count + entries.length, 0)
738
+ };
739
+ }
740
+ function buildWaifuIdMap(waifus) {
741
+ const used = /* @__PURE__ */ new Set();
742
+ const idMap = {};
743
+ for (const waifu of waifus) {
744
+ const baseId = sanitizeWaifuId(waifu.id);
745
+ let nextId = baseId;
746
+ let suffix = 2;
747
+ while (used.has(nextId)) {
748
+ nextId = `${baseId}_${suffix}`;
749
+ suffix += 1;
750
+ }
751
+ used.add(nextId);
752
+ idMap[waifu.id] = nextId;
753
+ }
754
+ return idMap;
755
+ }
756
+ function sanitizeWaifuId(value) {
757
+ const sanitized = value.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
758
+ return sanitized || "Waifu_1";
759
+ }
760
+ function buildWaifuGuilds(channels) {
761
+ const result = /* @__PURE__ */ new Map();
762
+ for (const channel of channels) {
763
+ for (const waifuId of channel.activeWaifuIds) {
764
+ const guilds = result.get(waifuId) ?? /* @__PURE__ */ new Set();
765
+ guilds.add(channel.guildId);
766
+ result.set(waifuId, guilds);
767
+ }
768
+ }
769
+ return result;
770
+ }
771
+ function collapseLegacyCheckpoints(checkpoints, channels, warnings, timestamp) {
772
+ const guildByChannelId = new Map(channels.map((channel) => [channel.channelId, channel.guildId]));
773
+ const grouped = /* @__PURE__ */ new Map();
774
+ for (const [channelId, checkpoint] of Object.entries(checkpoints)) {
775
+ const guildId = guildByChannelId.get(channelId);
776
+ if (!guildId) {
777
+ warnings.globalWarnings.push({
778
+ code: "legacy_checkpoint_orphaned",
779
+ field: "stageManager.checkpoints",
780
+ message: `Legacy checkpoint for channel "${channelId}" could not be mapped to a migrated guild.`,
781
+ legacyValue: channelId,
782
+ createdAt: timestamp
783
+ });
784
+ continue;
785
+ }
786
+ const entries = grouped.get(guildId) ?? [];
787
+ entries.push({ channelId, ...checkpoint });
788
+ grouped.set(guildId, entries);
789
+ }
790
+ return Object.fromEntries(
791
+ [...grouped.entries()].map(([guildId, entries]) => {
792
+ const withMessageId = entries.filter((entry) => entry.lastProcessedMessageId);
793
+ const highestMessage = withMessageId.sort(
794
+ (left, right) => compareMessageIds(left.lastProcessedMessageId, right.lastProcessedMessageId)
795
+ )[withMessageId.length - 1];
796
+ const latestRun = entries.filter((entry) => entry.lastRunAt).sort((left, right) => String(left.lastRunAt).localeCompare(String(right.lastRunAt))).at(-1);
797
+ if (entries.length > 1) {
798
+ warnings.globalWarnings.push({
799
+ code: "legacy_checkpoints_collapsed",
800
+ field: `stageManager.checkpoints.${guildId}`,
801
+ message: `Collapsed ${entries.length} channel checkpoints into one guild checkpoint for "${guildId}".`,
802
+ createdAt: timestamp
803
+ });
804
+ }
805
+ return [
806
+ guildId,
807
+ {
808
+ lastProcessedMessageId: highestMessage?.lastProcessedMessageId ?? null,
809
+ lastRunAt: latestRun?.lastRunAt ?? null
810
+ }
811
+ ];
812
+ })
813
+ );
814
+ }
815
+ function compareMessageIds(left, right) {
816
+ if (!left && !right) {
817
+ return 0;
818
+ }
819
+ if (!left) {
820
+ return -1;
821
+ }
822
+ if (!right) {
823
+ return 1;
824
+ }
825
+ try {
826
+ const leftValue = BigInt(left);
827
+ const rightValue = BigInt(right);
828
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
829
+ } catch {
830
+ return left.localeCompare(right);
831
+ }
832
+ }
833
+ async function migrateWaifuAssets(paths, waifu, waifuId, warnings, timestamp) {
834
+ return {
835
+ avatarPath: await migrateAssetField(paths, waifu, waifuId, "avatarPath", "avatar", warnings, timestamp),
836
+ bannerPath: await migrateAssetField(paths, waifu, waifuId, "bannerPath", "banner", warnings, timestamp)
837
+ };
838
+ }
839
+ async function migrateAssetField(paths, waifu, waifuId, key, stem, warnings, timestamp) {
840
+ const value = waifu[key];
841
+ if (!value || !value.trim()) {
842
+ return null;
843
+ }
844
+ const sourcePath = await resolveLegacyAssetPath(paths, value);
845
+ if (!sourcePath) {
846
+ addWaifuWarning(warnings, waifuId, {
847
+ code: "legacy_asset_unresolved",
848
+ field: key,
849
+ message: `Legacy ${stem} asset could not be resolved during import.`,
850
+ legacyValue: value,
851
+ createdAt: timestamp
852
+ });
853
+ return null;
854
+ }
855
+ const extension = path5.extname(sourcePath) || ".png";
856
+ const destinationDir = path5.join(paths.runtimeAssetsWaifusRoot, waifuId);
857
+ const destinationPath = path5.join(destinationDir, `${stem}${extension}`);
858
+ await fs5.mkdir(destinationDir, { recursive: true });
859
+ await fs5.copyFile(sourcePath, destinationPath);
860
+ return path5.posix.join("waifus", waifuId, `${stem}${extension}`);
861
+ }
862
+ async function resolveLegacyAssetPath(paths, value) {
863
+ const trimmed = value.trim();
864
+ const candidates = [
865
+ trimmed,
866
+ path5.join(path5.dirname(paths.legacyConfigRoot), trimmed.replace(/^\.\//, "")),
867
+ path5.join(paths.legacyConfigRoot, trimmed.replace(/^\.\/?config\/assets\//, "").replace(/^config\/assets\//, "").replace(/^\.\/assets\//, "").replace(/^assets\//, ""))
868
+ ];
869
+ for (const candidate of candidates) {
870
+ const resolved = path5.isAbsolute(candidate) ? candidate : path5.resolve(candidate);
871
+ if (await fileExists3(resolved)) {
872
+ return resolved;
873
+ }
874
+ }
875
+ return null;
876
+ }
877
+ function remapParticipantKey(value, idMap) {
878
+ const match = value.match(/^waifu:(.+)$/);
879
+ if (!match) {
880
+ return value;
881
+ }
882
+ return `waifu:${idMap[match[1]] ?? match[1]}`;
883
+ }
884
+ function findLegacyIdForNewId(idMap, nextId) {
885
+ return Object.entries(idMap).find(([, mapped]) => mapped === nextId)?.[0] ?? null;
886
+ }
887
+ function deriveAuthMode(providerId) {
888
+ return providerId === "ollama" || providerId === "lmstudio" ? "none" : "required";
889
+ }
890
+ function addWaifuWarning(warnings, waifuId, warning) {
891
+ warnings.waifuWarnings[waifuId] = warnings.waifuWarnings[waifuId] ?? [];
892
+ warnings.waifuWarnings[waifuId].push(warning);
893
+ }
894
+ function asRecord(value) {
895
+ return value && typeof value === "object" ? value : {};
896
+ }
897
+ async function readJson(filePath, fallback) {
898
+ try {
899
+ return JSON.parse(await fs5.readFile(filePath, "utf8"));
900
+ } catch (error) {
901
+ const code = error?.code;
902
+ if (code === "ENOENT") {
903
+ return fallback;
904
+ }
905
+ throw error;
906
+ }
907
+ }
908
+ async function atomicWriteFile(filePath, contents) {
909
+ await fs5.mkdir(path5.dirname(filePath), { recursive: true });
910
+ const tempPath = `${filePath}.tmp`;
911
+ await fs5.writeFile(tempPath, contents, "utf8");
912
+ await fs5.rename(tempPath, filePath);
913
+ }
914
+ async function fileExists3(filePath) {
915
+ try {
916
+ await fs5.access(filePath);
917
+ return true;
918
+ } catch {
919
+ return false;
920
+ }
921
+ }
922
+
923
+ // src/pm2-manager.ts
924
+ import { createRequire } from "module";
925
+ import path6 from "path";
926
+
927
+ // src/service-env.ts
928
+ function getServiceEnv(service) {
929
+ if (service === "backend") {
930
+ return {
931
+ NODE_ENV: "production"
932
+ };
933
+ }
934
+ return {
935
+ NODE_ENV: "production",
936
+ PORT: "3000"
937
+ };
938
+ }
939
+
940
+ // src/pm2-manager.ts
941
+ var require2 = createRequire(import.meta.url);
942
+ var backendProcessName = "waifus-backend";
943
+ var dashboardProcessName = "waifus-dashboard";
944
+ async function startServices(projectRoot) {
945
+ await withPm2(async () => {
946
+ await ensureStarted({
947
+ name: backendProcessName,
948
+ cwd: projectRoot,
949
+ script: "pnpm",
950
+ args: ["--filter", "backend", "start"],
951
+ env: getServiceEnv("backend")
952
+ });
953
+ await ensureStarted({
954
+ name: dashboardProcessName,
955
+ cwd: projectRoot,
956
+ script: "pnpm",
957
+ args: ["--filter", "dashboard", "start"],
958
+ env: getServiceEnv("dashboard")
959
+ });
960
+ });
961
+ }
962
+ async function stopServices() {
963
+ await withPm2(async () => {
964
+ await stopIfPresent(backendProcessName);
965
+ await stopIfPresent(dashboardProcessName);
966
+ });
967
+ }
968
+ async function restartServices(projectRoot) {
969
+ await withPm2(async () => {
970
+ await restartOrStart({
971
+ name: backendProcessName,
972
+ cwd: projectRoot,
973
+ script: "pnpm",
974
+ args: ["--filter", "backend", "start"],
975
+ env: getServiceEnv("backend")
976
+ });
977
+ await restartOrStart({
978
+ name: dashboardProcessName,
979
+ cwd: projectRoot,
980
+ script: "pnpm",
981
+ args: ["--filter", "dashboard", "start"],
982
+ env: getServiceEnv("dashboard")
983
+ });
984
+ });
985
+ }
986
+ async function listManagedServices() {
987
+ return withPm2(async () => {
988
+ const processes = await listProcesses();
989
+ return processes.filter(
990
+ (processDescription) => [backendProcessName, dashboardProcessName].includes(processDescription.name ?? "")
991
+ ).map((processDescription) => ({
992
+ name: processDescription.name ?? "unknown",
993
+ status: processDescription.pm2_env?.status ?? "unknown",
994
+ cwd: processDescription.pm2_env?.pm_cwd ?? null,
995
+ pid: typeof processDescription.pid === "number" ? processDescription.pid : null,
996
+ restartCount: processDescription.pm2_env?.restart_time ?? 0,
997
+ uptimeMs: typeof processDescription.pm2_env?.pm_uptime === "number" ? Date.now() - processDescription.pm2_env.pm_uptime : null
998
+ }));
999
+ });
1000
+ }
1001
+ function getPm2LogCommand(service, lines) {
1002
+ const pm2Bin = resolvePm2Bin();
1003
+ const args = ["logs"];
1004
+ if (service === "backend") {
1005
+ args.push(backendProcessName);
1006
+ } else if (service === "dashboard") {
1007
+ args.push(dashboardProcessName);
1008
+ } else {
1009
+ args.push(backendProcessName, dashboardProcessName);
1010
+ }
1011
+ args.push("--lines", String(lines));
1012
+ return {
1013
+ command: process.execPath,
1014
+ args: [pm2Bin, ...args]
1015
+ };
1016
+ }
1017
+ async function ensureStarted(options) {
1018
+ if (await hasManagedProcess(options.name ?? "")) {
1019
+ await restartProcess(options.name ?? "");
1020
+ return;
1021
+ }
1022
+ await startProcess(options);
1023
+ }
1024
+ async function restartOrStart(options) {
1025
+ if (await hasManagedProcess(options.name ?? "")) {
1026
+ await restartProcess(options.name ?? "");
1027
+ return;
1028
+ }
1029
+ await startProcess(options);
1030
+ }
1031
+ async function stopIfPresent(processName) {
1032
+ if (!await hasManagedProcess(processName)) {
1033
+ return;
1034
+ }
1035
+ await stopProcess(processName);
1036
+ }
1037
+ async function withPm2(callback) {
1038
+ const pm2 = await loadPm2();
1039
+ await connect(pm2);
1040
+ try {
1041
+ return await callback();
1042
+ } finally {
1043
+ pm2.disconnect();
1044
+ }
1045
+ }
1046
+ function connect(pm2) {
1047
+ return new Promise((resolve, reject) => {
1048
+ pm2.connect((error) => {
1049
+ if (error) {
1050
+ reject(error);
1051
+ return;
1052
+ }
1053
+ resolve();
1054
+ });
1055
+ });
1056
+ }
1057
+ async function startProcess(options) {
1058
+ const pm2 = await loadPm2();
1059
+ return new Promise((resolve, reject) => {
1060
+ pm2.start(options, (error) => {
1061
+ if (error) {
1062
+ reject(error);
1063
+ return;
1064
+ }
1065
+ resolve();
1066
+ });
1067
+ });
1068
+ }
1069
+ async function restartProcess(processName) {
1070
+ const pm2 = await loadPm2();
1071
+ return new Promise((resolve, reject) => {
1072
+ pm2.restart(processName, (error) => {
1073
+ if (error) {
1074
+ reject(error);
1075
+ return;
1076
+ }
1077
+ resolve();
1078
+ });
1079
+ });
1080
+ }
1081
+ async function stopProcess(processName) {
1082
+ const pm2 = await loadPm2();
1083
+ return new Promise((resolve, reject) => {
1084
+ pm2.stop(processName, (error) => {
1085
+ if (error) {
1086
+ reject(error);
1087
+ return;
1088
+ }
1089
+ resolve();
1090
+ });
1091
+ });
1092
+ }
1093
+ async function listProcesses() {
1094
+ const pm2 = await loadPm2();
1095
+ return new Promise((resolve, reject) => {
1096
+ pm2.list((error, processDescription) => {
1097
+ if (error) {
1098
+ reject(error);
1099
+ return;
1100
+ }
1101
+ resolve(processDescription);
1102
+ });
1103
+ });
1104
+ }
1105
+ async function hasManagedProcess(processName) {
1106
+ const processes = await listProcesses();
1107
+ return processes.some((processDescription) => processDescription.name === processName);
1108
+ }
1109
+ var pm2Promise = null;
1110
+ async function loadPm2() {
1111
+ if (!pm2Promise) {
1112
+ pm2Promise = Promise.resolve(require2("pm2"));
1113
+ }
1114
+ return pm2Promise;
1115
+ }
1116
+ function resolvePm2Bin() {
1117
+ return require2.resolve(path6.join("pm2", "bin", "pm2"));
1118
+ }
1119
+
1120
+ // src/repo-bootstrap.ts
1121
+ import { promises as fs6 } from "fs";
1122
+ import os2 from "os";
1123
+ import path7 from "path";
1124
+ import { spawn as spawn3 } from "child_process";
1125
+ async function bootstrapRepoFromGitHubArchive(targetDir, options = {}) {
1126
+ const projectRoot = path7.resolve(targetDir);
1127
+ await ensureTargetDirectoryIsEmpty(projectRoot);
1128
+ const repositoryUrl = options.repo?.trim() || await resolveRepositoryFromPackageMetadata();
1129
+ if (!repositoryUrl) {
1130
+ throw new Error(
1131
+ "No GitHub repository was provided.\nUse: waifus init <target-dir> --repo https://github.com/<owner>/<repo>"
1132
+ );
1133
+ }
1134
+ const githubRepo = parseGitHubRepository(repositoryUrl);
1135
+ if (!githubRepo) {
1136
+ throw new Error(
1137
+ `Unsupported repository: ${repositoryUrl}
1138
+ Only GitHub repositories are supported by waifus init.`
1139
+ );
1140
+ }
1141
+ const ref = options.ref?.trim() || null;
1142
+ const archiveUrl = buildGitHubArchiveUrl(githubRepo.owner, githubRepo.repo, ref);
1143
+ const tempRoot = await fs6.mkdtemp(path7.join(os2.tmpdir(), "waifus-init-"));
1144
+ const archivePath = path7.join(tempRoot, "repo.tar.gz");
1145
+ const extractRoot = path7.join(tempRoot, "extract");
1146
+ try {
1147
+ await fs6.mkdir(extractRoot, { recursive: true });
1148
+ await downloadFile(archiveUrl, archivePath);
1149
+ await extractTarGz(archivePath, extractRoot);
1150
+ const extractedRoot = await findSingleExtractedRoot(extractRoot);
1151
+ await fs6.mkdir(projectRoot, { recursive: true });
1152
+ await copyDirectoryContents(extractedRoot, projectRoot);
1153
+ return {
1154
+ projectRoot,
1155
+ sourceRepo: `https://github.com/${githubRepo.owner}/${githubRepo.repo}`,
1156
+ sourceRef: ref
1157
+ };
1158
+ } finally {
1159
+ await fs6.rm(tempRoot, { recursive: true, force: true });
1160
+ }
1161
+ }
1162
+ async function ensureTargetDirectoryIsEmpty(targetDir) {
1163
+ try {
1164
+ const stats = await fs6.stat(targetDir);
1165
+ if (!stats.isDirectory()) {
1166
+ throw new Error(`Target path exists and is not a directory: ${targetDir}`);
1167
+ }
1168
+ const entries = await fs6.readdir(targetDir);
1169
+ if (entries.length > 0) {
1170
+ throw new Error(`Target directory is not empty: ${targetDir}`);
1171
+ }
1172
+ } catch (error) {
1173
+ const code = error?.code;
1174
+ if (code === "ENOENT") {
1175
+ return;
1176
+ }
1177
+ throw error;
1178
+ }
1179
+ }
1180
+ async function resolveRepositoryFromPackageMetadata() {
1181
+ try {
1182
+ const packageJsonPath = new URL("../package.json", import.meta.url);
1183
+ const raw = JSON.parse(await fs6.readFile(packageJsonPath, "utf8"));
1184
+ if (typeof raw.repository === "string" && raw.repository.trim()) {
1185
+ return raw.repository.trim();
1186
+ }
1187
+ if (raw.repository && typeof raw.repository === "object" && typeof raw.repository.url === "string" && raw.repository.url.trim()) {
1188
+ return raw.repository.url.trim();
1189
+ }
1190
+ return null;
1191
+ } catch {
1192
+ return null;
1193
+ }
1194
+ }
1195
+ function parseGitHubRepository(value) {
1196
+ const trimmed = value.trim();
1197
+ const normalized = trimmed.replace(/^git\+/, "").replace(/^git@github\.com:/, "https://github.com/").replace(/\.git$/, "");
1198
+ const slugMatch = normalized.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
1199
+ if (slugMatch) {
1200
+ return { owner: slugMatch[1], repo: slugMatch[2] };
1201
+ }
1202
+ try {
1203
+ const url = new URL(normalized);
1204
+ if (url.hostname !== "github.com") {
1205
+ return null;
1206
+ }
1207
+ const segments = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
1208
+ if (segments.length < 2 || !segments[0] || !segments[1]) {
1209
+ return null;
1210
+ }
1211
+ return {
1212
+ owner: segments[0],
1213
+ repo: segments[1]
1214
+ };
1215
+ } catch {
1216
+ return null;
1217
+ }
1218
+ }
1219
+ function buildGitHubArchiveUrl(owner, repo, ref) {
1220
+ const encodedRef = ref ? `/${encodeURIComponent(ref)}` : "";
1221
+ return `https://api.github.com/repos/${owner}/${repo}/tarball${encodedRef}`;
1222
+ }
1223
+ async function downloadFile(url, destinationPath) {
1224
+ const response = await fetch(url, {
1225
+ headers: {
1226
+ "User-Agent": "@starlight-ai/discord-waifus"
1227
+ },
1228
+ redirect: "follow"
1229
+ });
1230
+ if (!response.ok) {
1231
+ throw new Error(`Failed to download repository archive: HTTP ${response.status}`);
1232
+ }
1233
+ const buffer = Buffer.from(await response.arrayBuffer());
1234
+ await fs6.writeFile(destinationPath, buffer);
1235
+ }
1236
+ async function extractTarGz(archivePath, extractRoot) {
1237
+ await spawnQuiet("tar", ["-xzf", archivePath, "-C", extractRoot]);
1238
+ }
1239
+ async function findSingleExtractedRoot(extractRoot) {
1240
+ const entries = await fs6.readdir(extractRoot, { withFileTypes: true });
1241
+ const directories = entries.filter((entry) => entry.isDirectory());
1242
+ if (directories.length !== 1) {
1243
+ throw new Error("Repository archive extraction did not produce a single root directory.");
1244
+ }
1245
+ return path7.join(extractRoot, directories[0].name);
1246
+ }
1247
+ async function copyDirectoryContents(sourceDir, targetDir) {
1248
+ const entries = await fs6.readdir(sourceDir);
1249
+ await Promise.all(
1250
+ entries.map(
1251
+ (entry) => fs6.cp(path7.join(sourceDir, entry), path7.join(targetDir, entry), {
1252
+ recursive: true
1253
+ })
1254
+ )
1255
+ );
1256
+ }
1257
+ async function spawnQuiet(command, args) {
1258
+ await new Promise((resolve, reject) => {
1259
+ const child = spawn3(command, args, {
1260
+ stdio: "ignore"
1261
+ });
1262
+ child.once("error", reject);
1263
+ child.once("exit", (code, signal) => {
1264
+ if (signal) {
1265
+ reject(new Error(`${command} exited due to signal ${signal}`));
1266
+ return;
1267
+ }
1268
+ if (code && code !== 0) {
1269
+ reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
1270
+ return;
1271
+ }
1272
+ resolve();
1273
+ });
1274
+ });
1275
+ }
1276
+
1277
+ // src/index.ts
1278
+ var cli = cac("waifus");
1279
+ cli.option("--project <path>", "Override the project root for this command");
1280
+ cli.command("use <projectPath>", "Store the default Discord Waifus project root for future commands").action(async (projectPath) => {
1281
+ try {
1282
+ const resolvedRoot = await assertProjectRoot(projectPath);
1283
+ await saveCliConfig({ defaultProjectRoot: resolvedRoot });
1284
+ console.log(pc2.green(`Default project root saved: ${resolvedRoot}`));
1285
+ } catch (error) {
1286
+ fail(error);
1287
+ }
1288
+ });
1289
+ cli.command("init <targetDir>", "Download the Discord Waifus repo into a target directory and register it").option("--repo <repo>", "GitHub repo URL or owner/repo slug. If omitted, uses the package repository when available.").option("--ref <ref>", "Git ref, branch, or tag to download. Defaults to the repo default branch.").option("--no-install", "Skip pnpm install after download").action(async (targetDir, options) => {
1290
+ try {
1291
+ const result = await bootstrapRepoFromGitHubArchive(targetDir, {
1292
+ repo: options.repo ?? null,
1293
+ ref: options.ref ?? null
1294
+ });
1295
+ await assertProjectRoot(result.projectRoot);
1296
+ success(`Downloaded project into ${result.projectRoot}`);
1297
+ info(`Source: ${result.sourceRepo}${result.sourceRef ? ` @ ${result.sourceRef}` : ""}`);
1298
+ await saveCliConfig({ defaultProjectRoot: result.projectRoot });
1299
+ success(`Default project root saved: ${result.projectRoot}`);
1300
+ if (options.install !== false) {
1301
+ info("Installing project dependencies with pnpm...");
1302
+ await spawnPassthrough("pnpm", ["install"], result.projectRoot);
1303
+ success("Dependencies installed.");
1304
+ } else {
1305
+ warn("Skipped pnpm install. Run `pnpm install` inside the project before building.");
1306
+ }
1307
+ info("Next steps:");
1308
+ info(`- waifus build`);
1309
+ info(`- waifus init-config`);
1310
+ info(`- waifus start`);
1311
+ } catch (error) {
1312
+ fail(error);
1313
+ }
1314
+ });
1315
+ cli.command("doctor", "Validate the local setup").action(async () => {
1316
+ try {
1317
+ const projectRoot = await requireProjectRoot(cli.options);
1318
+ const nodeMajor = Number.parseInt(process4.versions.node.split(".")[0] ?? "0", 10);
1319
+ const pnpmVersion = spawnSync("pnpm", ["--version"], { encoding: "utf8" });
1320
+ const configFiles = runtimeConfigFiles(projectRoot);
1321
+ const artifactFiles = requiredBuildArtifacts(projectRoot);
1322
+ const runtimeState = await inspectRuntimeState(projectRoot);
1323
+ console.log(pc2.bold("waifus doctor"));
1324
+ info(`Project root: ${projectRoot}`);
1325
+ if (Number.isFinite(nodeMajor) && nodeMajor >= 20) {
1326
+ success(`Node.js ${process4.versions.node}`);
1327
+ } else {
1328
+ warn(`Node.js ${process4.versions.node} detected. Recommended: Node.js 20+`);
1329
+ }
1330
+ if (pnpmVersion.status === 0) {
1331
+ success(`pnpm ${pnpmVersion.stdout.trim()}`);
1332
+ } else {
1333
+ warn("pnpm not found in PATH");
1334
+ }
1335
+ if (runtimeState.isCanonicalLocalRuntime) {
1336
+ success("Canonical local runtime detected: .waifus/");
1337
+ } else if (runtimeState.isMigrationPending) {
1338
+ warn("Migration pending: legacy config still takes precedence until import completes.");
1339
+ } else if (runtimeState.legacyLiveExists) {
1340
+ warn("Legacy runtime detected. Local .waifus/ bootstrap is blocked until migration.");
1341
+ } else {
1342
+ warn("Local runtime not initialized. Run: waifus init-config");
1343
+ }
1344
+ for (const configFile of configFiles) {
1345
+ if (await fileExists2(configFile)) {
1346
+ success(`Config present: ${path8.relative(projectRoot, configFile)}`);
1347
+ } else {
1348
+ warn(`Missing config: ${path8.relative(projectRoot, configFile)} (run: waifus init-config)`);
1349
+ }
1350
+ }
1351
+ for (const artifactFile of artifactFiles) {
1352
+ if (await fileExists2(artifactFile)) {
1353
+ success(`Build artifact present: ${path8.relative(projectRoot, artifactFile)}`);
1354
+ } else {
1355
+ warn(`Build artifact missing: ${path8.relative(projectRoot, artifactFile)}`);
1356
+ }
1357
+ }
1358
+ const filesToScan = runtimeState.isCanonicalLocalRuntime || runtimeState.runtimeRootExists && !runtimeState.legacyLiveExists ? [
1359
+ ...localRuntimeFiles(projectRoot),
1360
+ ...await listWaifuDocumentFiles(projectRoot)
1361
+ ] : [
1362
+ ...legacyConfigFiles(projectRoot)
1363
+ ];
1364
+ for (const configFile of filesToScan) {
1365
+ if (!await fileExists2(configFile)) {
1366
+ continue;
1367
+ }
1368
+ const configValue = await readConfigFile(configFile);
1369
+ if (configValue === null) {
1370
+ warn(`Could not parse config: ${path8.relative(projectRoot, configFile)}`);
1371
+ continue;
1372
+ }
1373
+ for (const envReference of findEnvReferences(configValue)) {
1374
+ if (process4.env[envReference.variableName]) {
1375
+ success(
1376
+ `Environment value resolved for ${envReference.variableName} in ${path8.relative(projectRoot, configFile)}`
1377
+ );
1378
+ } else {
1379
+ warn(
1380
+ `Unresolved ${envReference.raw} in ${path8.relative(projectRoot, configFile)}. Export ${envReference.variableName} before start.`
1381
+ );
1382
+ }
1383
+ }
1384
+ if (configFile.endsWith(path8.join(".waifus", "orchestrator.toml")) || configFile.endsWith(path8.join("config", "orchestrator.json"))) {
1385
+ const orchestrator = "orchestrator" in configValue ? configValue.orchestrator ?? {} : {};
1386
+ const providerValue = typeof orchestrator.providerId === "string" ? orchestrator.providerId : typeof orchestrator.provider_id === "string" ? orchestrator.provider_id : "";
1387
+ const modelValue = typeof orchestrator.model === "string" ? orchestrator.model : "";
1388
+ if (providerValue === "configure-me" || providerValue === "" || modelValue === "configure-me" || modelValue === "") {
1389
+ warn("Orchestrator config is still unconfigured. Update it in the dashboard before relying on orchestration.");
1390
+ }
1391
+ }
1392
+ }
1393
+ if (runtimeState.runtimeRootExists && runtimeState.migrationState) {
1394
+ info(`Migration state: ${runtimeState.migrationState.status}`);
1395
+ }
1396
+ info("If you edit config on disk, apply it with: waifus restart");
1397
+ } catch (error) {
1398
+ fail(error);
1399
+ }
1400
+ });
1401
+ cli.command("build", "Build backend, dashboard, and CLI").action(async () => {
1402
+ try {
1403
+ const projectRoot = await requireProjectRoot(cli.options);
1404
+ await spawnPassthrough("pnpm", ["build"], projectRoot);
1405
+ } catch (error) {
1406
+ fail(error);
1407
+ }
1408
+ });
1409
+ cli.command("init-config", "Create or repair the local .waifus runtime layout from defaults/").option("--force", "Reserved for future explicit empty-bootstrap overrides").action(async () => {
1410
+ try {
1411
+ const projectRoot = await requireProjectRoot(cli.options);
1412
+ const legacyLiveExists = await hasLegacyLiveConfig(projectRoot);
1413
+ const runtimeState = await inspectRuntimeState(projectRoot);
1414
+ if (legacyLiveExists && runtimeState.migrationState?.status !== "import_completed") {
1415
+ throw new Error("Legacy live config still exists. Run: waifus migrate-local-config");
1416
+ }
1417
+ const written = await bootstrapLocalRuntime(projectRoot);
1418
+ if (written.length === 0) {
1419
+ info("Local runtime layout already satisfied.");
1420
+ } else {
1421
+ for (const filePath of written) {
1422
+ success(`Wrote ${path8.relative(projectRoot, filePath)}`);
1423
+ }
1424
+ }
1425
+ info("Finish configuration in the dashboard after start.");
1426
+ info("If you later edit config on disk, apply it with: waifus restart");
1427
+ } catch (error) {
1428
+ fail(error);
1429
+ }
1430
+ });
1431
+ cli.command("migrate-local-config", "Import legacy config/*.json runtime data into the local .waifus layout").action(async () => {
1432
+ try {
1433
+ const projectRoot = await requireProjectRoot(cli.options);
1434
+ const result = await migrateLocalConfig(projectRoot);
1435
+ success(`Imported legacy runtime into .waifus/ (${result.written.length} files written).`);
1436
+ if (Object.keys(result.idMap).some((legacyId) => legacyId !== result.idMap[legacyId])) {
1437
+ info("Sanitized waifu IDs:");
1438
+ for (const [legacyId, nextId] of Object.entries(result.idMap)) {
1439
+ if (legacyId === nextId) {
1440
+ continue;
1441
+ }
1442
+ info(`- ${legacyId} -> ${nextId}`);
1443
+ }
1444
+ }
1445
+ if (result.warningCount > 0) {
1446
+ warn(
1447
+ `Migration completed with ${result.warningCount} warning(s). Review ${path8.relative(projectRoot, getRuntimeLayoutPaths(projectRoot).migrationWarningsFile)}`
1448
+ );
1449
+ } else {
1450
+ success("Migration warnings: none");
1451
+ }
1452
+ info("Legacy files were left untouched. After reviewing the import, apply it with: waifus restart");
1453
+ } catch (error) {
1454
+ fail(error);
1455
+ }
1456
+ });
1457
+ cli.command("start", "Start backend and dashboard under PM2").action(async () => {
1458
+ try {
1459
+ const projectRoot = await requireProjectRoot(cli.options);
1460
+ await assertNoPendingLegacyMigration(projectRoot);
1461
+ for (const artifactFile of requiredBuildArtifacts(projectRoot)) {
1462
+ if (!await fileExists2(artifactFile)) {
1463
+ throw new Error(
1464
+ `Missing build artifact: ${path8.relative(projectRoot, artifactFile)}
1465
+ Run: waifus build`
1466
+ );
1467
+ }
1468
+ }
1469
+ await startServices(projectRoot);
1470
+ success("Started waifus-backend and waifus-dashboard through PM2.");
1471
+ info("Local dashboard: http://localhost:3000");
1472
+ info("Local backend: http://127.0.0.1:4000");
1473
+ warn("These services are local to this machine.");
1474
+ info("If you edit config on disk, apply it with: waifus restart");
1475
+ } catch (error) {
1476
+ fail(error);
1477
+ }
1478
+ });
1479
+ cli.command("stop", "Stop PM2-managed backend and dashboard").action(async () => {
1480
+ try {
1481
+ await requireProjectRoot(cli.options);
1482
+ await stopServices();
1483
+ success("Stopped waifus-backend and waifus-dashboard.");
1484
+ } catch (error) {
1485
+ fail(error);
1486
+ }
1487
+ });
1488
+ cli.command("restart", "Restart PM2-managed backend and dashboard").action(async () => {
1489
+ try {
1490
+ const projectRoot = await requireProjectRoot(cli.options);
1491
+ await assertNoPendingLegacyMigration(projectRoot);
1492
+ for (const artifactFile of requiredBuildArtifacts(projectRoot)) {
1493
+ if (!await fileExists2(artifactFile)) {
1494
+ throw new Error(
1495
+ `Missing build artifact: ${path8.relative(projectRoot, artifactFile)}
1496
+ Run: waifus build`
1497
+ );
1498
+ }
1499
+ }
1500
+ await restartServices(projectRoot);
1501
+ success("Restarted waifus-backend and waifus-dashboard.");
1502
+ info("Local dashboard: http://localhost:3000");
1503
+ info("Local backend: http://127.0.0.1:4000");
1504
+ warn("These services are local to this machine.");
1505
+ info("Config changes on disk are now applied.");
1506
+ } catch (error) {
1507
+ fail(error);
1508
+ }
1509
+ });
1510
+ cli.command("status", "Show PM2 service status").action(async () => {
1511
+ try {
1512
+ await requireProjectRoot(cli.options);
1513
+ const services = await listManagedServices();
1514
+ console.log(pc2.bold("waifus status"));
1515
+ if (services.length === 0) {
1516
+ warn("No managed waifus PM2 services found.");
1517
+ } else {
1518
+ for (const service of services) {
1519
+ console.log(
1520
+ [
1521
+ `- ${service.name}`,
1522
+ ` status: ${service.status}`,
1523
+ ` cwd: ${service.cwd ?? "unknown"}`,
1524
+ ` pid: ${service.pid ?? "not running"}`,
1525
+ ` restarts: ${service.restartCount}`,
1526
+ ` uptimeMs: ${service.uptimeMs ?? "unknown"}`
1527
+ ].join("\n")
1528
+ );
1529
+ }
1530
+ }
1531
+ try {
1532
+ const response = await fetch("http://127.0.0.1:4000/api/status");
1533
+ if (response.ok) {
1534
+ const payload = await response.json();
1535
+ info(`Backend health: reachable on 127.0.0.1:4000 (uptimeSeconds=${payload.uptimeSeconds ?? "unknown"})`);
1536
+ } else {
1537
+ warn(`Backend health check returned HTTP ${response.status}`);
1538
+ }
1539
+ } catch {
1540
+ warn("Backend health: not reachable on 127.0.0.1:4000");
1541
+ }
1542
+ warn("These services are local to this machine.");
1543
+ } catch (error) {
1544
+ fail(error);
1545
+ }
1546
+ });
1547
+ cli.command("logs [service]", "Tail PM2 logs for backend, dashboard, or both").option("--lines <count>", "How many recent lines to include", { default: "100" }).action(async (service, options) => {
1548
+ try {
1549
+ await requireProjectRoot(cli.options);
1550
+ const normalizedService = service === "backend" || service === "dashboard" ? service : service ? null : null;
1551
+ if (service && !normalizedService) {
1552
+ throw new Error("Invalid service. Use: waifus logs | waifus logs backend | waifus logs dashboard");
1553
+ }
1554
+ const lineCount = Number.parseInt(options.lines, 10);
1555
+ if (!Number.isFinite(lineCount) || lineCount <= 0) {
1556
+ throw new Error("Invalid --lines value. Use a positive integer.");
1557
+ }
1558
+ const logCommand = getPm2LogCommand(normalizedService, lineCount);
1559
+ await spawnPassthrough(logCommand.command, logCommand.args, process4.cwd());
1560
+ } catch (error) {
1561
+ fail(error);
1562
+ }
1563
+ });
1564
+ cli.command("open", "Open the local dashboard in the browser").action(async () => {
1565
+ try {
1566
+ await requireProjectRoot(cli.options);
1567
+ await openUrl("http://localhost:3000");
1568
+ success("Opened http://localhost:3000");
1569
+ } catch (error) {
1570
+ fail(error);
1571
+ }
1572
+ });
1573
+ cli.command("run <service>", "Run backend or dashboard in the foreground").action(async (service) => {
1574
+ try {
1575
+ const projectRoot = await requireProjectRoot(cli.options);
1576
+ if (service !== "backend" && service !== "dashboard") {
1577
+ throw new Error("Invalid service. Use: waifus run backend | waifus run dashboard");
1578
+ }
1579
+ const filterTarget = service === "backend" ? "backend" : "dashboard";
1580
+ await spawnPassthrough("pnpm", ["--filter", filterTarget, "start"], projectRoot, getServiceEnv(service));
1581
+ } catch (error) {
1582
+ fail(error);
1583
+ }
1584
+ });
1585
+ cli.help();
1586
+ cli.version("0.1.0");
1587
+ cli.parse(process4.argv);
1588
+ function fail(error) {
1589
+ const message = error instanceof Error ? error.message : "Unknown CLI error";
1590
+ console.error(pc2.red(message));
1591
+ process4.exit(1);
1592
+ }
1593
+ async function listWaifuDocumentFiles(projectRoot) {
1594
+ const paths = getRuntimeLayoutPaths(projectRoot);
1595
+ try {
1596
+ const entries = await fs7.readdir(paths.runtimeWaifusRoot, { withFileTypes: true });
1597
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => path8.join(paths.runtimeWaifusRoot, entry.name));
1598
+ } catch {
1599
+ return [];
1600
+ }
1601
+ }
1602
+ function legacyConfigFiles(projectRoot) {
1603
+ const paths = getRuntimeLayoutPaths(projectRoot);
1604
+ return [
1605
+ paths.legacyWaifusFile,
1606
+ paths.legacyProvidersFile,
1607
+ paths.legacyChannelsFile,
1608
+ paths.legacyOrchestratorFile,
1609
+ paths.legacyStageManagerFile
1610
+ ];
1611
+ }
1612
+ async function readConfigFile(filePath) {
1613
+ try {
1614
+ if (filePath.endsWith(".json")) {
1615
+ return await readJsonFile(filePath);
1616
+ }
1617
+ if (filePath.endsWith(".toml")) {
1618
+ return parseToml(await fs7.readFile(filePath, "utf8"));
1619
+ }
1620
+ return null;
1621
+ } catch {
1622
+ return null;
1623
+ }
1624
+ }
1625
+ async function assertNoPendingLegacyMigration(projectRoot) {
1626
+ const runtimeState = await inspectRuntimeState(projectRoot);
1627
+ if (runtimeState.legacyLiveExists && runtimeState.migrationState?.status !== "import_completed") {
1628
+ throw new Error("Legacy live config still exists. Run: waifus migrate-local-config");
1629
+ }
1630
+ }
1631
+ function findEnvReferences(value) {
1632
+ const references = [];
1633
+ visit(value, (candidate) => {
1634
+ if (typeof candidate !== "string") {
1635
+ return;
1636
+ }
1637
+ const envPrefixMatch = candidate.match(/^env:([A-Z0-9_]+)$/i);
1638
+ if (envPrefixMatch) {
1639
+ references.push({
1640
+ raw: candidate,
1641
+ variableName: envPrefixMatch[1]
1642
+ });
1643
+ return;
1644
+ }
1645
+ const templateMatch = candidate.match(/^\$\{([A-Z0-9_]+)\}$/i);
1646
+ if (templateMatch) {
1647
+ references.push({
1648
+ raw: candidate,
1649
+ variableName: templateMatch[1]
1650
+ });
1651
+ }
1652
+ });
1653
+ return references;
1654
+ }
1655
+ function visit(value, callback) {
1656
+ callback(value);
1657
+ if (Array.isArray(value)) {
1658
+ for (const entry of value) {
1659
+ visit(entry, callback);
1660
+ }
1661
+ return;
1662
+ }
1663
+ if (value && typeof value === "object") {
1664
+ for (const entry of Object.values(value)) {
1665
+ visit(entry, callback);
1666
+ }
1667
+ }
1668
+ }
1669
+ //# sourceMappingURL=index.js.map