@aicopycoders/exodus 2026.6.2400

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/assets/references/awareness-framework.md +156 -0
  2. package/assets/references/cast-video.md +165 -0
  3. package/assets/references/copy-blocks.md +138 -0
  4. package/assets/references/creative-strategy.md +990 -0
  5. package/assets/references/editing-rules.md +81 -0
  6. package/assets/references/hook-quality-checklist.md +153 -0
  7. package/assets/references/iterations.md +73 -0
  8. package/assets/references/scrawls.md +483 -0
  9. package/assets/references/segments.md +79 -0
  10. package/assets/skills/exodus/SKILL.md +50 -0
  11. package/assets/skills/exodus-brand/SKILL.md +117 -0
  12. package/assets/skills/exodus-browse/SKILL.md +96 -0
  13. package/assets/skills/exodus-creative/SKILL.md +94 -0
  14. package/assets/skills/exodus-drive/SKILL.md +118 -0
  15. package/assets/skills/exodus-foundation/SKILL.md +170 -0
  16. package/assets/skills/exodus-genesis/SKILL.md +159 -0
  17. package/assets/skills/exodus-idea/SKILL.md +78 -0
  18. package/assets/skills/exodus-image/SKILL.md +224 -0
  19. package/assets/skills/exodus-meme/SKILL.md +183 -0
  20. package/assets/skills/exodus-primer/SKILL.md +145 -0
  21. package/assets/skills/exodus-strategist/SKILL.md +217 -0
  22. package/assets/skills/exodus-swipe/SKILL.md +85 -0
  23. package/assets/skills/exodus-template/SKILL.md +106 -0
  24. package/assets/skills/exodus-write/SKILL.md +157 -0
  25. package/assets/skills/genesis-bots/.env.example +12 -0
  26. package/assets/skills/genesis-bots/README.txt +29 -0
  27. package/assets/skills/genesis-bots/SKILL.md +219 -0
  28. package/assets/skills/genesis-bots/scripts/genesis-stream.mjs +307 -0
  29. package/assets/skills/genesis-bots/scripts/genesis-stream.py +220 -0
  30. package/dist/bin/exodus.d.ts +2 -0
  31. package/dist/bin/exodus.js +177 -0
  32. package/dist/commands/__tests__/genesis.hookgate.test.d.ts +1 -0
  33. package/dist/commands/__tests__/genesis.hookgate.test.js +12 -0
  34. package/dist/commands/brand.d.ts +24 -0
  35. package/dist/commands/brand.js +292 -0
  36. package/dist/commands/browse.d.ts +9 -0
  37. package/dist/commands/browse.js +132 -0
  38. package/dist/commands/creative.d.ts +2 -0
  39. package/dist/commands/creative.js +223 -0
  40. package/dist/commands/doctor.d.ts +21 -0
  41. package/dist/commands/doctor.js +590 -0
  42. package/dist/commands/drive.d.ts +2 -0
  43. package/dist/commands/drive.js +154 -0
  44. package/dist/commands/foundation.d.ts +2 -0
  45. package/dist/commands/foundation.js +468 -0
  46. package/dist/commands/genesis.d.ts +64 -0
  47. package/dist/commands/genesis.js +564 -0
  48. package/dist/commands/idea.d.ts +53 -0
  49. package/dist/commands/idea.js +333 -0
  50. package/dist/commands/image.d.ts +2 -0
  51. package/dist/commands/image.js +249 -0
  52. package/dist/commands/init.d.ts +13 -0
  53. package/dist/commands/init.js +70 -0
  54. package/dist/commands/keys.d.ts +2 -0
  55. package/dist/commands/keys.js +101 -0
  56. package/dist/commands/meme.d.ts +12 -0
  57. package/dist/commands/meme.js +272 -0
  58. package/dist/commands/migrate.d.ts +3 -0
  59. package/dist/commands/migrate.js +158 -0
  60. package/dist/commands/primer.d.ts +2 -0
  61. package/dist/commands/primer.js +540 -0
  62. package/dist/commands/read-doc.d.ts +3 -0
  63. package/dist/commands/read-doc.js +62 -0
  64. package/dist/commands/status.d.ts +6 -0
  65. package/dist/commands/status.js +205 -0
  66. package/dist/commands/swipe.d.ts +2 -0
  67. package/dist/commands/swipe.js +398 -0
  68. package/dist/commands/template.d.ts +2 -0
  69. package/dist/commands/template.js +225 -0
  70. package/dist/commands/whoami.d.ts +2 -0
  71. package/dist/commands/whoami.js +51 -0
  72. package/dist/lib/ad-library-write.d.ts +15 -0
  73. package/dist/lib/ad-library-write.js +67 -0
  74. package/dist/lib/assets.d.ts +3 -0
  75. package/dist/lib/assets.js +23 -0
  76. package/dist/lib/backend-hint.d.ts +2 -0
  77. package/dist/lib/backend-hint.js +22 -0
  78. package/dist/lib/cc-command.d.ts +1 -0
  79. package/dist/lib/cc-command.js +13 -0
  80. package/dist/lib/client.d.ts +23 -0
  81. package/dist/lib/client.js +213 -0
  82. package/dist/lib/format.d.ts +8 -0
  83. package/dist/lib/format.js +172 -0
  84. package/dist/lib/intel-format.d.ts +1 -0
  85. package/dist/lib/intel-format.js +38 -0
  86. package/dist/lib/keys-sync.d.ts +16 -0
  87. package/dist/lib/keys-sync.js +88 -0
  88. package/dist/lib/layout.d.ts +38 -0
  89. package/dist/lib/layout.js +153 -0
  90. package/dist/lib/load-env.d.ts +1 -0
  91. package/dist/lib/load-env.js +47 -0
  92. package/dist/lib/pixar-orchestrator-types.d.ts +54 -0
  93. package/dist/lib/pixar-orchestrator-types.js +1 -0
  94. package/dist/lib/poll.d.ts +14 -0
  95. package/dist/lib/poll.js +32 -0
  96. package/dist/lib/preflight.d.ts +18 -0
  97. package/dist/lib/preflight.js +56 -0
  98. package/dist/lib/prompts.d.ts +9 -0
  99. package/dist/lib/prompts.js +99 -0
  100. package/dist/lib/reel-write.d.ts +14 -0
  101. package/dist/lib/reel-write.js +59 -0
  102. package/dist/lib/release.d.ts +4 -0
  103. package/dist/lib/release.js +81 -0
  104. package/dist/lib/scaffold.d.ts +8 -0
  105. package/dist/lib/scaffold.js +67 -0
  106. package/dist/lib/scout-hydrate.d.ts +1 -0
  107. package/dist/lib/scout-hydrate.js +10 -0
  108. package/dist/lib/state.d.ts +6 -0
  109. package/dist/lib/state.js +49 -0
  110. package/dist/lib/template-constants.d.ts +4 -0
  111. package/dist/lib/template-constants.js +85 -0
  112. package/dist/lib/version.d.ts +1 -0
  113. package/dist/lib/version.js +19 -0
  114. package/package.json +30 -0
@@ -0,0 +1,590 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { execSync } from "node:child_process";
5
+ import { apiGet, apiGetDashboard, getApiUrl } from "../lib/client.js";
6
+ import { findWorkspaceRoot } from "../lib/state.js";
7
+ import { detectLayout, listBrandDirs, resolveActiveBrand, brandStateDir, } from "../lib/layout.js";
8
+ import { auth401Hint, isPlaceholderApiUrl } from "../lib/backend-hint.js";
9
+ import { compareVersions, readPackageVersion, } from "../lib/release.js";
10
+ export const helpText = `
11
+ exodus doctor — Preflight checks for your local Claude Code setup
12
+
13
+ Usage:
14
+ exodus doctor
15
+
16
+ Runs through every check (Node version, claude CLI, .env, API key, Google
17
+ Drive on the dashboard, dashboard auth). Install problems show ✅ / ❌;
18
+ expected brand setup (primer, profile depth) shows as cyan ▸ next-steps, not
19
+ errors. On any red, prints exactly what to do about it.
20
+
21
+ Exit code: 0 when the install is healthy (next-steps don't fail it), 1 on any red.
22
+ `.trim();
23
+ export function checkNodeVersion() {
24
+ const version = process.versions.node;
25
+ const major = parseInt(version.split(".")[0], 10);
26
+ if (major >= 20) {
27
+ return { ok: true, label: "Node.js", detail: `v${version} detected` };
28
+ }
29
+ return {
30
+ ok: false,
31
+ label: "Node.js",
32
+ detail: `v${version} is too old`,
33
+ fix: "install Node 20+ from https://nodejs.org (LTS) — see the “If Node isn't installed” section of the README",
34
+ };
35
+ }
36
+ export function checkClaudeCli() {
37
+ try {
38
+ const out = execSync("which claude", { stdio: ["ignore", "pipe", "ignore"] })
39
+ .toString()
40
+ .trim();
41
+ if (out)
42
+ return { ok: true, label: "Claude CLI", detail: `installed at ${out}` };
43
+ throw new Error("empty output");
44
+ }
45
+ catch {
46
+ return {
47
+ ok: true,
48
+ warn: true,
49
+ label: "Claude CLI",
50
+ detail: "not on PATH — expected if you're in the Claude Desktop app; only terminal users need the standalone CLI",
51
+ fix: "Terminal users: `curl -fsSL https://claude.ai/install.sh | bash` (Mac/Linux) or the Windows quickstart at https://code.claude.com/docs/en/quickstart",
52
+ };
53
+ }
54
+ }
55
+ export function checkEnvFile() {
56
+ const envPath = path.join(findWorkspaceRoot(), ".env");
57
+ if (!fs.existsSync(envPath)) {
58
+ return {
59
+ ok: false,
60
+ label: ".env",
61
+ fix: "run `npx @aicopycoders/exodus init`, then paste your dashboard .env block (Settings → Claude Code → Copy .env block)",
62
+ };
63
+ }
64
+ const content = fs.readFileSync(envPath, "utf8");
65
+ const hasUrl = /EXODUS_API_URL\s*=/.test(content);
66
+ const hasKey = /EXODUS_API_KEY\s*=/.test(content);
67
+ const urlValue = content.match(/EXODUS_API_URL\s*=\s*(\S+)/)?.[1] ?? "";
68
+ if (hasUrl && isPlaceholderApiUrl(urlValue)) {
69
+ return {
70
+ ok: false,
71
+ label: ".env",
72
+ detail: "EXODUS_API_URL is still the placeholder / not set",
73
+ fix: "set EXODUS_API_URL from your install instruction (xo.copycoders.ai → accomplished-tapir-106; dev.xo.copycoders.ai → good-cod-360)",
74
+ };
75
+ }
76
+ if (hasUrl && hasKey) {
77
+ return { ok: true, label: ".env", detail: "present with required keys" };
78
+ }
79
+ return {
80
+ ok: false,
81
+ label: ".env",
82
+ detail: "missing EXODUS_API_URL or EXODUS_API_KEY",
83
+ fix: "run `npx @aicopycoders/exodus init`, then paste your dashboard .env block (Settings → Claude Code → Copy .env block)",
84
+ };
85
+ }
86
+ export async function checkWhoami() {
87
+ try {
88
+ const { apiGet } = await import("../lib/client.js");
89
+ const res = await apiGet("/api/v2/whoami");
90
+ if (!res.ok) {
91
+ if (res.status === 401 || res.status === 403) {
92
+ let host = getApiUrl();
93
+ try {
94
+ host = new URL(getApiUrl()).host;
95
+ }
96
+ catch {
97
+ }
98
+ return {
99
+ ok: false,
100
+ label: "Brand resolution",
101
+ detail: `API key rejected by ${host} (HTTP ${res.status})`,
102
+ fix: auth401Hint(getApiUrl()),
103
+ };
104
+ }
105
+ return {
106
+ ok: false,
107
+ label: "Brand resolution",
108
+ detail: `whoami returned HTTP ${res.status}`,
109
+ fix: "verify EXODUS_API_KEY in .env is valid + workspace-bound",
110
+ };
111
+ }
112
+ const d = res.data;
113
+ if (!d.workspaceSlug) {
114
+ return {
115
+ ok: false,
116
+ label: "Brand resolution",
117
+ detail: "API key did not resolve to a workspace",
118
+ fix: "ask an admin to mint a brand-scoped API key in Settings → Brands → API keys",
119
+ };
120
+ }
121
+ if (!d.foundationReady) {
122
+ return {
123
+ ok: true,
124
+ todo: true,
125
+ label: "Brand primer",
126
+ detail: `${d.workspaceSlug} has no primer yet — set one up to unlock the pipelines`,
127
+ fix: `say "exodus, set up my brand primer" (or run \`npx @aicopycoders/exodus foundation\` from a source doc, or paste it at /settings?tab=brands&brand=${d.workspaceSlug})`,
128
+ };
129
+ }
130
+ return {
131
+ ok: true,
132
+ label: "Brand resolution",
133
+ detail: `${d.workspaceName} (${d.workspaceSlug}) — foundation ready`,
134
+ };
135
+ }
136
+ catch (err) {
137
+ return {
138
+ ok: false,
139
+ label: "Brand resolution",
140
+ detail: err instanceof Error ? err.message : String(err),
141
+ };
142
+ }
143
+ }
144
+ export async function checkActiveBrandMatch() {
145
+ const resolved = resolveActiveBrand();
146
+ if (!resolved.slug) {
147
+ return {
148
+ ok: true,
149
+ label: "Active brand",
150
+ detail: "no local override — using key's default brand",
151
+ };
152
+ }
153
+ const source = resolved.source === "folder" ? "from brand folder" : "from `brand use`";
154
+ let res;
155
+ try {
156
+ res = await apiGetDashboard("/api/brands/mine", { timeoutMs: 10_000 });
157
+ }
158
+ catch (err) {
159
+ return {
160
+ ok: true,
161
+ label: "Active brand",
162
+ detail: `${resolved.slug} (${source}) — access check skipped: ${err instanceof Error ? err.message : String(err)}`,
163
+ };
164
+ }
165
+ if (!res.ok || !Array.isArray(res.data?.brands)) {
166
+ return {
167
+ ok: true,
168
+ label: "Active brand",
169
+ detail: `${resolved.slug} (${source}) — access check skipped (HTTP ${res.status})`,
170
+ };
171
+ }
172
+ const match = res.data.brands.find((b) => b.slug === resolved.slug);
173
+ if (match) {
174
+ return {
175
+ ok: true,
176
+ label: "Active brand",
177
+ detail: `${resolved.slug} (${source}) — accessible`,
178
+ };
179
+ }
180
+ const available = res.data.brands.map((b) => b.slug).join(", ") || "(none)";
181
+ return {
182
+ ok: false,
183
+ label: "Active brand",
184
+ detail: `local active brand "${resolved.slug}" (${source}) is NOT in your accessible list — the server ignores it and every command silently targets the key's default brand. available: ${available}`,
185
+ fix: `run \`npx @aicopycoders/exodus brand use <slug>\` with one of your brands, or \`npx @aicopycoders/exodus brand clear\` to fall back to the key default`,
186
+ };
187
+ }
188
+ const FRESHNESS_SUBTREES = ["commands", "lib", "bin"];
189
+ function newestMtimeIn(dir) {
190
+ let max = 0;
191
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
192
+ if (entry.name === "node_modules" || entry.name.startsWith("."))
193
+ continue;
194
+ const full = path.join(dir, entry.name);
195
+ const stat = fs.statSync(full);
196
+ if (stat.isDirectory()) {
197
+ max = Math.max(max, newestMtimeIn(full));
198
+ }
199
+ else {
200
+ max = Math.max(max, stat.mtimeMs);
201
+ }
202
+ }
203
+ return max;
204
+ }
205
+ export function checkExodusDistFreshness(pkgRootOverride) {
206
+ let pkgRoot;
207
+ if (pkgRootOverride) {
208
+ pkgRoot = pkgRootOverride;
209
+ }
210
+ else {
211
+ const here = path.dirname(fileURLToPath(import.meta.url));
212
+ pkgRoot = here;
213
+ while (pkgRoot !== "/" && !fs.existsSync(path.join(pkgRoot, "package.json"))) {
214
+ pkgRoot = path.dirname(pkgRoot);
215
+ }
216
+ }
217
+ const distBase = path.join(pkgRoot, "dist");
218
+ if (!FRESHNESS_SUBTREES.some((s) => fs.existsSync(path.join(pkgRoot, s)))) {
219
+ return { ok: true, label: "Exodus CLI build", detail: "running from installed package" };
220
+ }
221
+ if (!fs.existsSync(distBase)) {
222
+ return {
223
+ ok: false,
224
+ label: "Exodus CLI build",
225
+ detail: "dist/ missing",
226
+ fix: "run `cd exodus && npm run build` (or `npm install` if in a fresh clone)",
227
+ };
228
+ }
229
+ const stale = [];
230
+ let worstLagMs = 0;
231
+ for (const sub of FRESHNESS_SUBTREES) {
232
+ const srcRoot = path.join(pkgRoot, sub);
233
+ const distRoot = path.join(distBase, sub);
234
+ if (!fs.existsSync(srcRoot) || !fs.existsSync(distRoot))
235
+ continue;
236
+ const srcMtime = newestMtimeIn(srcRoot);
237
+ const distMtime = newestMtimeIn(distRoot);
238
+ if (srcMtime > distMtime + 5000) {
239
+ stale.push(sub);
240
+ worstLagMs = Math.max(worstLagMs, srcMtime - distMtime);
241
+ }
242
+ }
243
+ if (stale.length > 0) {
244
+ const lagSec = Math.round(worstLagMs / 1000);
245
+ return {
246
+ ok: false,
247
+ label: "Exodus CLI build",
248
+ detail: `source newer than dist in ${stale.join(", ")} (lag ${lagSec}s) — CLI ships stale code`,
249
+ fix: "run `cd exodus && npm run build` to rebuild dist",
250
+ };
251
+ }
252
+ return { ok: true, label: "Exodus CLI build", detail: "dist up-to-date" };
253
+ }
254
+ const NPM_REGISTRY = "https://registry.npmjs.org/@aicopycoders/exodus/latest";
255
+ export async function checkVersionCurrency(pkgRootOverride, fetchImpl = fetch) {
256
+ let pkgRoot;
257
+ if (pkgRootOverride) {
258
+ pkgRoot = pkgRootOverride;
259
+ }
260
+ else {
261
+ const here = path.dirname(fileURLToPath(import.meta.url));
262
+ pkgRoot = here;
263
+ while (pkgRoot !== "/" && !fs.existsSync(path.join(pkgRoot, "package.json"))) {
264
+ pkgRoot = path.dirname(pkgRoot);
265
+ }
266
+ }
267
+ const local = readPackageVersion(pkgRoot);
268
+ try {
269
+ const ctrl = new AbortController();
270
+ const t = setTimeout(() => ctrl.abort(), 8_000);
271
+ let res;
272
+ try {
273
+ res = await fetchImpl(NPM_REGISTRY, { signal: ctrl.signal });
274
+ }
275
+ finally {
276
+ clearTimeout(t);
277
+ }
278
+ if (!res.ok)
279
+ throw new Error(`HTTP ${res.status}`);
280
+ const latest = (await res.json()).version;
281
+ if (compareVersions(local, latest) >= 0) {
282
+ return { ok: true, label: "Exodus version", detail: `${local} — up to date` };
283
+ }
284
+ return {
285
+ ok: true,
286
+ warn: true,
287
+ label: "Exodus version",
288
+ detail: `${local} installed, ${latest} available`,
289
+ fix: "run `npx @aicopycoders/exodus@latest init` to update (or `npm update -g @aicopycoders/exodus`)",
290
+ };
291
+ }
292
+ catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err);
294
+ return {
295
+ ok: true,
296
+ warn: true,
297
+ label: "Exodus version",
298
+ detail: `installed ${local} — couldn't reach npm (${message.slice(0, 80)})`,
299
+ };
300
+ }
301
+ }
302
+ export async function checkApiAndDrive() {
303
+ let res;
304
+ try {
305
+ res = await apiGet("/api/v2/doctor/preflight");
306
+ }
307
+ catch (err) {
308
+ const message = err instanceof Error ? err.message : String(err);
309
+ return [
310
+ {
311
+ ok: false,
312
+ label: "Dashboard API key",
313
+ detail: `request failed: ${message}`,
314
+ fix: "check your .env has EXODUS_API_URL and EXODUS_API_KEY, then re-run",
315
+ },
316
+ {
317
+ ok: false,
318
+ label: "Google Drive (dashboard)",
319
+ detail: "skipped — API key check failed",
320
+ },
321
+ ];
322
+ }
323
+ if (!res.ok) {
324
+ return [
325
+ {
326
+ ok: false,
327
+ label: "Dashboard API key",
328
+ detail: `HTTP ${res.status}`,
329
+ fix: "regenerate at Settings → API Keys, then paste the new .env block (or re-run `npx @aicopycoders/exodus init`)",
330
+ },
331
+ {
332
+ ok: false,
333
+ label: "Google Drive (dashboard)",
334
+ detail: "skipped — API key check failed",
335
+ },
336
+ ];
337
+ }
338
+ const { workspace, drive, providers } = res.data;
339
+ const apiResult = workspace
340
+ ? {
341
+ ok: true,
342
+ label: "Dashboard API key",
343
+ detail: `connected to workspace: ${workspace.name}`,
344
+ }
345
+ : {
346
+ ok: false,
347
+ label: "Dashboard API key",
348
+ detail: "no workspace found for this key",
349
+ fix: "contact support — your workspace may not be set up",
350
+ };
351
+ const driveResult = drive.connected
352
+ ? {
353
+ ok: true,
354
+ label: "Google Drive (dashboard)",
355
+ detail: `connected as ${drive.email ?? "unknown email"}`,
356
+ }
357
+ : {
358
+ ok: false,
359
+ label: "Google Drive (dashboard)",
360
+ fix: "go to Settings → Google Drive on the dashboard and click Connect",
361
+ };
362
+ const results = [apiResult, driveResult];
363
+ if (providers) {
364
+ results.push(providers.kie
365
+ ? {
366
+ ok: true,
367
+ label: "kie.ai key (image renders)",
368
+ detail: "configured for the active user",
369
+ }
370
+ : {
371
+ ok: false,
372
+ label: "kie.ai key (image renders)",
373
+ detail: "no kie.ai key found for the active user",
374
+ fix: "add it at Settings → Keys on the dashboard (image renders will fail without it)",
375
+ });
376
+ const hasLlm = providers.openrouter || providers.anthropic;
377
+ results.push(hasLlm
378
+ ? {
379
+ ok: true,
380
+ label: "LLM key (OpenRouter or Anthropic)",
381
+ detail: `configured: ${providers.openrouter ? "OpenRouter" : "Anthropic"}`,
382
+ }
383
+ : {
384
+ ok: false,
385
+ label: "LLM key (OpenRouter or Anthropic)",
386
+ detail: "no OpenRouter or Anthropic key found for the active user",
387
+ fix: "add an OpenRouter or Anthropic key at Settings → Keys (copy + image pipelines fail without one)",
388
+ });
389
+ if (providers.genesis !== undefined) {
390
+ results.push(providers.genesis
391
+ ? {
392
+ ok: true,
393
+ label: "Genesis key (copy writing)",
394
+ detail: "configured for the active user",
395
+ }
396
+ : {
397
+ ok: false,
398
+ label: "Genesis key (copy writing)",
399
+ detail: "no Genesis key linked for the active user",
400
+ fix: "re-run onboarding or contact support — Genesis auto-links on first sign-in",
401
+ });
402
+ }
403
+ if (providers.imgflip !== undefined) {
404
+ results.push(providers.imgflip
405
+ ? {
406
+ ok: true,
407
+ label: "Imgflip login (classic memes)",
408
+ detail: "configured for the active user",
409
+ }
410
+ : {
411
+ ok: true,
412
+ warn: true,
413
+ label: "Imgflip login (classic memes — optional)",
414
+ detail: "not set — classic meme formats need it; AI memes are unaffected",
415
+ fix: "add your Imgflip username/password at Settings → Keys if you want classic meme formats",
416
+ });
417
+ }
418
+ }
419
+ return results;
420
+ }
421
+ export async function checkDashboardAuth() {
422
+ let res;
423
+ try {
424
+ res = await apiGetDashboard("/api/doctor/dashboard-ping");
425
+ }
426
+ catch (err) {
427
+ const message = err instanceof Error ? err.message : String(err);
428
+ return {
429
+ ok: false,
430
+ label: "Dashboard auth (dashboard-routed pipelines)",
431
+ detail: `request failed: ${message}`,
432
+ fix: "check your network and EXODUS_DASHBOARD_URL, then re-run",
433
+ };
434
+ }
435
+ if (res.ok) {
436
+ return {
437
+ ok: true,
438
+ label: "Dashboard auth (dashboard-routed pipelines)",
439
+ detail: "Bearer token reaches dashboard route",
440
+ };
441
+ }
442
+ if (res.status === 401) {
443
+ const hint = res.data?.hint;
444
+ return {
445
+ ok: false,
446
+ label: "Dashboard auth (dashboard-routed pipelines)",
447
+ detail: res.data?.error ?? "401 from /api/doctor/dashboard-ping",
448
+ fix: hint ?? "run `npx @aicopycoders/exodus@latest init` to update the CLI — older releases skipped the Bearer header on dashboard routes",
449
+ };
450
+ }
451
+ return {
452
+ ok: false,
453
+ label: "Dashboard auth (image-ads path)",
454
+ detail: `HTTP ${res.status}`,
455
+ fix: "report to support — dashboard auth probe returned an unexpected status",
456
+ };
457
+ }
458
+ function printResult(r) {
459
+ const icon = r.todo
460
+ ? "\x1b[36m▸\x1b[0m"
461
+ : r.ok && r.warn
462
+ ? "\x1b[33m⚠️\x1b[0m"
463
+ : r.ok
464
+ ? "\x1b[32m✅\x1b[0m"
465
+ : "\x1b[31m❌\x1b[0m";
466
+ const detail = r.detail ? ` — ${r.detail}` : "";
467
+ console.log(`${icon} ${r.label}${detail}`);
468
+ if ((!r.ok || r.warn || r.todo) && r.fix) {
469
+ console.log(` → ${r.fix}`);
470
+ }
471
+ }
472
+ export function checkLayout() {
473
+ const root = findWorkspaceRoot();
474
+ const layout = detectLayout(root);
475
+ if (layout === "legacy") {
476
+ return {
477
+ ok: true,
478
+ label: "Layout",
479
+ detail: "single-brand (legacy) — run `npx @aicopycoders/exodus migrate` to switch to the multi-brand layout",
480
+ };
481
+ }
482
+ const brands = listBrandDirs(root);
483
+ const resolved = resolveActiveBrand();
484
+ const active = resolved.slug
485
+ ? `active: ${resolved.slug} (${resolved.source === "folder" ? "from brand folder" : "from \`brand use\`"})`
486
+ : "active: key default";
487
+ if (brands.length === 0) {
488
+ return {
489
+ ok: true,
490
+ warn: true,
491
+ label: "Layout",
492
+ detail: `multi-brand — no brand folders yet; ${active}`,
493
+ fix: "run `npx @aicopycoders/exodus@latest init` (or `npx @aicopycoders/exodus brand use <slug>`) to create your brand folders",
494
+ };
495
+ }
496
+ const missingProfiles = brands.filter((b) => !fs.existsSync(path.join(b.dir, "state", "brand-profile.md")));
497
+ if (missingProfiles.length > 0) {
498
+ return {
499
+ ok: true,
500
+ warn: true,
501
+ label: "Layout",
502
+ detail: `multi-brand, ${brands.length} brand folder(s); ${active}; missing brand profile in: ${missingProfiles.map((b) => b.slug).join(", ")}`,
503
+ fix: "run `npx @aicopycoders/exodus@latest init` to refresh every brand folder's profile",
504
+ };
505
+ }
506
+ return {
507
+ ok: true,
508
+ label: "Layout",
509
+ detail: `multi-brand, ${brands.length} brand folder(s) [${brands.map((b) => b.slug).join(", ")}]; ${active}`,
510
+ };
511
+ }
512
+ export function checkBrandProfileGenesisDepth() {
513
+ const stateDir = brandStateDir();
514
+ const filePath = path.join(stateDir, "brand-profile.md");
515
+ const rel = path.relative(findWorkspaceRoot(), filePath) || filePath;
516
+ if (!fs.existsSync(filePath)) {
517
+ return {
518
+ ok: false,
519
+ label: "brand-profile",
520
+ detail: `${rel} is missing`,
521
+ fix: "run `npx @aicopycoders/exodus brand use <slug>` to generate it",
522
+ };
523
+ }
524
+ const contents = fs.readFileSync(filePath, "utf-8");
525
+ if (contents.includes("exodus:genesis-depth-pending")) {
526
+ return {
527
+ ok: true,
528
+ todo: true,
529
+ label: "Brand-profile depth",
530
+ detail: "manual section is still on the placeholder — fill it in so Genesis output isn't generic",
531
+ fix: `open ${rel} and replace the TO-BE-FILLED-IN sections (Proven Angles, Segments, Key Differentiators, ICP Notes) with this brand's tested patterns`,
532
+ };
533
+ }
534
+ return {
535
+ ok: true,
536
+ label: "brand-profile (Genesis depth)",
537
+ detail: "manual section is filled in",
538
+ };
539
+ }
540
+ export async function run(_flags) {
541
+ const results = [];
542
+ results.push(checkNodeVersion());
543
+ results.push(checkClaudeCli());
544
+ results.push(checkEnvFile());
545
+ results.push(checkLayout());
546
+ results.push(checkExodusDistFreshness());
547
+ results.push(checkBrandProfileGenesisDepth());
548
+ const apiAndDrive = await checkApiAndDrive();
549
+ results.push(...apiAndDrive);
550
+ results.push(await checkDashboardAuth());
551
+ results.push(await checkWhoami());
552
+ results.push(await checkActiveBrandMatch());
553
+ results.push(await checkVersionCurrency());
554
+ for (const r of results)
555
+ printResult(r);
556
+ const failures = results.filter((r) => !r.ok).length;
557
+ const warnings = results.filter((r) => r.ok && r.warn && !r.todo).length;
558
+ const todos = results.filter((r) => r.ok && r.todo);
559
+ console.log("");
560
+ if (failures > 0) {
561
+ console.log(`${failures} ${failures === 1 ? "issue" : "issues"} found. Fix the item(s) above and run \`exodus doctor\` again.`);
562
+ process.exit(1);
563
+ }
564
+ const warnNote = warnings > 0
565
+ ? ` (${warnings} non-blocking ${warnings === 1 ? "warning" : "warnings"} above — safe to ignore)`
566
+ : "";
567
+ console.log(`✅ Install healthy — Exodus + Genesis are installed and connected.${warnNote}`);
568
+ if (todos.length > 0) {
569
+ const needsPrimer = todos.some((t) => /primer/i.test(t.label));
570
+ console.log("");
571
+ console.log("To start creating — in THIS folder:");
572
+ console.log(" 1. Restart Claude Code here (or run /clear) so the Exodus + Genesis skills load.");
573
+ if (needsPrimer) {
574
+ console.log(" 2. Set up your brand primer first — it unlocks every pipeline:");
575
+ console.log(' say "exodus, set up my brand primer"');
576
+ }
577
+ const rest = todos.filter((t) => !/primer/i.test(t.label));
578
+ if (rest.length > 0) {
579
+ console.log("");
580
+ console.log(" Then, when you're ready (optional):");
581
+ for (const t of rest) {
582
+ console.log(` • ${t.label}${t.detail ? ` — ${t.detail}` : ""}`);
583
+ }
584
+ }
585
+ }
586
+ else {
587
+ console.log("You're ready to run pipelines.");
588
+ }
589
+ process.exit(0);
590
+ }
@@ -0,0 +1,2 @@
1
+ export declare const helpText: string;
2
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;