@aicopycoders/exodus 2026.6.2902 → 2026.7.200-beta.10
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/assets/docs/CLAUDE.md +3 -1
- package/assets/docs/PIPELINES.md +24 -0
- package/assets/references/cast-video.md +9 -0
- package/assets/skills/exodus/SKILL.md +3 -1
- package/assets/skills/exodus-brand/SKILL.md +2 -2
- package/assets/skills/exodus-genesis/SKILL.md +9 -0
- package/assets/skills/exodus-winners/SKILL.md +230 -0
- package/assets/skills/exodus-workflow/SKILL.md +173 -0
- package/dist/bin/exodus.js +4 -1
- package/dist/commands/brand.js +161 -1
- package/dist/commands/creative.js +5 -22
- package/dist/commands/doctor.js +22 -16
- package/dist/commands/genesis.js +19 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/keys.js +1 -0
- package/dist/commands/meme.js +5 -4
- package/dist/commands/migrate.js +7 -6
- package/dist/commands/primer.js +4 -2
- package/dist/commands/segment.d.ts +57 -0
- package/dist/commands/segment.js +274 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +20 -0
- package/dist/commands/winners.d.ts +10 -0
- package/dist/commands/winners.js +401 -0
- package/dist/commands/workflow.d.ts +298 -0
- package/dist/commands/workflow.js +763 -0
- package/dist/lib/channel.d.ts +5 -0
- package/dist/lib/channel.js +15 -0
- package/dist/lib/image-sniff.d.ts +6 -0
- package/dist/lib/image-sniff.js +18 -0
- package/dist/lib/keys-sync.js +4 -1
- package/dist/lib/scaffold.d.ts +5 -4
- package/dist/lib/scaffold.js +27 -6
- package/dist/lib/template-constants.d.ts +1 -1
- package/dist/lib/template-constants.js +102 -0
- package/package.json +1 -1
package/dist/commands/brand.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { apiGetDashboard, apiPostDashboard } from "../lib/client.js";
|
|
4
|
+
import { sniffImageType, BRAND_IMAGE_MAX_BYTES } from "../lib/image-sniff.js";
|
|
4
5
|
import { getActiveBrand, setActiveBrand, clearActiveBrand, findWorkspaceRoot, } from "../lib/state.js";
|
|
5
6
|
import { detectLayout, ensureBrandDir, resolveActiveBrand, brandStateDir, brandDirFor, } from "../lib/layout.js";
|
|
6
7
|
import { promptYesNo } from "../lib/prompts.js";
|
|
8
|
+
import { pkgRef } from "../lib/channel.js";
|
|
7
9
|
const AUTO_END_MARKER = "<!-- exodus:auto-section-end — manual brand notes below survive `brand use` refreshes -->";
|
|
8
10
|
export const helpText = `
|
|
9
11
|
exodus brand — list, switch, and inspect brands available to your CLI
|
|
@@ -15,6 +17,9 @@ Subcommands:
|
|
|
15
17
|
exodus brand current Print the active brand
|
|
16
18
|
exodus brand clear Stop overriding the brand (use the key's default)
|
|
17
19
|
exodus brand delete <slug> Delete a brand you own (asks before removing the local folder)
|
|
20
|
+
exodus brand images add <files...> --kind founder|product|reference
|
|
21
|
+
Upload brand-identity images (png/jpg/webp, max 15MB each)
|
|
22
|
+
exodus brand images list Show the active brand's uploaded identity images
|
|
18
23
|
|
|
19
24
|
Any user can create and own unlimited brands (\`brand create\`) — you do NOT
|
|
20
25
|
need to be an admin, and brands are scoped to you (two users can have the
|
|
@@ -176,7 +181,7 @@ async function runCreate(name) {
|
|
|
176
181
|
console.log(`✓ Wrote ${refresh.pathRel}.`);
|
|
177
182
|
}
|
|
178
183
|
console.log(`\nNext: set up the brand's primer to unlock the pipelines —\n` +
|
|
179
|
-
` say "exodus, set up my brand primer" (or run \`npx
|
|
184
|
+
` say "exodus, set up my brand primer" (or run \`npx ${pkgRef()} primer\`).`);
|
|
180
185
|
}
|
|
181
186
|
async function runDelete(slug, flags) {
|
|
182
187
|
if (!slug)
|
|
@@ -260,11 +265,166 @@ function runClear() {
|
|
|
260
265
|
console.log("from here still target it (the folder wins over the pointer).");
|
|
261
266
|
}
|
|
262
267
|
}
|
|
268
|
+
const BRAND_IMAGE_KINDS = ["founder", "product", "reference"];
|
|
269
|
+
async function runImagesAdd(files, flags) {
|
|
270
|
+
const kind = typeof flags["kind"] === "string" ? flags["kind"] : "";
|
|
271
|
+
if (!BRAND_IMAGE_KINDS.includes(kind)) {
|
|
272
|
+
console.error(`Error: brand images add requires --kind ${BRAND_IMAGE_KINDS.join("|")}. ` +
|
|
273
|
+
`(Logo stays a dashboard upload — it replaces rather than appends.)`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (files.length === 0) {
|
|
278
|
+
console.error("Error: brand images add requires at least one file.");
|
|
279
|
+
process.exit(1);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const valid = [];
|
|
283
|
+
const skipped = [];
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
let buf;
|
|
286
|
+
try {
|
|
287
|
+
buf = fs.readFileSync(file);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
skipped.push({ file, reason: "unreadable (does the path exist?)" });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (buf.length > BRAND_IMAGE_MAX_BYTES) {
|
|
294
|
+
skipped.push({
|
|
295
|
+
file,
|
|
296
|
+
reason: `too large (${(buf.length / 1024 / 1024).toFixed(1)}MB > 15MB)`,
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const sniffed = sniffImageType(buf);
|
|
301
|
+
if (!sniffed) {
|
|
302
|
+
skipped.push({ file, reason: "not a png/jpg/webp (content check failed)" });
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
valid.push({ file, buf, mime: sniffed.mime });
|
|
306
|
+
}
|
|
307
|
+
for (const s of skipped) {
|
|
308
|
+
console.error(` skipped ${path.basename(s.file)}: ${s.reason}`);
|
|
309
|
+
}
|
|
310
|
+
if (valid.length === 0) {
|
|
311
|
+
console.error("Error: no valid image files to upload.");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const mint = await apiPostDashboard("/api/brands/images/upload-url", { count: valid.length });
|
|
316
|
+
if (!mint.ok || !Array.isArray(mint.data.urls)) {
|
|
317
|
+
console.error(`Error: ${mint.data.error ?? `HTTP ${mint.status} minting upload URLs`}`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const storageIds = [];
|
|
322
|
+
const uploadedFiles = [];
|
|
323
|
+
for (let i = 0; i < valid.length; i++) {
|
|
324
|
+
const { file, buf, mime } = valid[i];
|
|
325
|
+
try {
|
|
326
|
+
const res = await fetch(mint.data.urls[i], {
|
|
327
|
+
method: "POST",
|
|
328
|
+
headers: { "Content-Type": mime },
|
|
329
|
+
body: new Uint8Array(buf),
|
|
330
|
+
});
|
|
331
|
+
const body = JSON.parse(await res.text());
|
|
332
|
+
if (!res.ok || !body.storageId)
|
|
333
|
+
throw new Error(`HTTP ${res.status}`);
|
|
334
|
+
storageIds.push(body.storageId);
|
|
335
|
+
uploadedFiles.push(file);
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
console.error(` skipped ${path.basename(file)}: storage upload failed (${err instanceof Error ? err.message : String(err)})`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (storageIds.length === 0) {
|
|
342
|
+
console.error("Error: every storage upload failed.");
|
|
343
|
+
process.exit(1);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const attach = await apiPostDashboard("/api/brands/images", {
|
|
347
|
+
kind,
|
|
348
|
+
storageIds,
|
|
349
|
+
});
|
|
350
|
+
if (!attach.ok) {
|
|
351
|
+
console.error(`Error: ${attach.data.error ?? `HTTP ${attach.status} attaching images`}`);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
for (const s of attach.data.skipped ?? []) {
|
|
356
|
+
console.error(` server skipped ${s.storageId}: ${s.reason}`);
|
|
357
|
+
}
|
|
358
|
+
for (const f of uploadedFiles)
|
|
359
|
+
console.log(` uploaded ${path.basename(f)}`);
|
|
360
|
+
console.log(`✓ ${kind}: ${attach.data.added} added (brand now has ${attach.data.total} ${kind} image${attach.data.total === 1 ? "" : "s"})`);
|
|
361
|
+
}
|
|
362
|
+
async function runImagesList() {
|
|
363
|
+
const res = await apiGetDashboard("/api/brands/images");
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
console.error(`Error: ${res.data.error ?? `HTTP ${res.status}`}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.log(`brand: ${res.data.brand}`);
|
|
370
|
+
for (const kind of BRAND_IMAGE_KINDS) {
|
|
371
|
+
const entries = res.data[kind] ?? [];
|
|
372
|
+
console.log(`${kind}: ${entries.length}`);
|
|
373
|
+
for (const e of entries) {
|
|
374
|
+
console.log(` ${e.url ?? e.storageId}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function imagesPositionals() {
|
|
379
|
+
const rest = process.argv.slice(3);
|
|
380
|
+
const out = [];
|
|
381
|
+
let i = 0;
|
|
382
|
+
while (i < rest.length) {
|
|
383
|
+
const arg = rest[i];
|
|
384
|
+
if (arg === "--help" || arg === "-h") {
|
|
385
|
+
i++;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (arg.startsWith("--")) {
|
|
389
|
+
const key = arg.slice(2);
|
|
390
|
+
const next = rest[i + 1];
|
|
391
|
+
if (key.startsWith("no-")) {
|
|
392
|
+
i++;
|
|
393
|
+
}
|
|
394
|
+
else if (next !== undefined && !next.startsWith("--")) {
|
|
395
|
+
i += 2;
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
out.push(arg);
|
|
403
|
+
i++;
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
async function runImages(args, flags) {
|
|
408
|
+
const sub = args[0];
|
|
409
|
+
if (sub === "add") {
|
|
410
|
+
await runImagesAdd(args.slice(1), flags);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (sub === "list" || sub === undefined) {
|
|
414
|
+
await runImagesList();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
console.error(`exodus brand images: unknown subcommand "${sub}" (use add | list)`);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
263
420
|
export async function run(flags) {
|
|
264
421
|
const positionals = process.argv.slice(3).filter((a) => !a.startsWith("--"));
|
|
265
422
|
const sub = positionals[0];
|
|
266
423
|
const arg = positionals[1];
|
|
267
424
|
switch (sub) {
|
|
425
|
+
case "images":
|
|
426
|
+
await runImages(imagesPositionals().slice(1), flags);
|
|
427
|
+
return;
|
|
268
428
|
case "list":
|
|
269
429
|
case undefined:
|
|
270
430
|
await runList();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { apiGet, apiPost, getDashboardUrl } from "../lib/client.js";
|
|
1
|
+
import { apiGet, apiGetDashboard, apiPost, getDashboardUrl, } from "../lib/client.js";
|
|
2
2
|
import { formatError } from "../lib/format.js";
|
|
3
3
|
export const helpText = `
|
|
4
4
|
exodus creative — Creative-suite engines (native, copy-derived, ref-match)
|
|
@@ -152,31 +152,14 @@ async function runStatus(flags) {
|
|
|
152
152
|
process.exit(1);
|
|
153
153
|
return;
|
|
154
154
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"";
|
|
159
|
-
const headers = { "Content-Type": "application/json" };
|
|
160
|
-
if (apiKey)
|
|
161
|
-
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
162
|
-
const fetchRes = await fetch(dashUrl, { method: "GET", headers });
|
|
163
|
-
const text = await fetchRes.text();
|
|
164
|
-
let data;
|
|
165
|
-
try {
|
|
166
|
-
data = JSON.parse(text);
|
|
167
|
-
}
|
|
168
|
-
catch {
|
|
169
|
-
console.error(`Non-JSON ${fetchRes.status} from /api/creative-suite/runs/${runId}: ${text.slice(0, 300)}`);
|
|
170
|
-
process.exit(1);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (!fetchRes.ok) {
|
|
174
|
-
const errMsg = data.error ?? `HTTP ${fetchRes.status}`;
|
|
155
|
+
const res = await apiGetDashboard(`/api/creative-suite/runs/${encodeURIComponent(runId)}`);
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const errMsg = res.data.error ?? `HTTP ${res.status}`;
|
|
175
158
|
console.error(`Error: ${errMsg}`);
|
|
176
159
|
process.exit(1);
|
|
177
160
|
return;
|
|
178
161
|
}
|
|
179
|
-
const d = data;
|
|
162
|
+
const d = res.data;
|
|
180
163
|
console.log(`runId: ${d._id}`);
|
|
181
164
|
if (d.name)
|
|
182
165
|
console.log(`name: ${d.name}`);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -7,6 +7,7 @@ import { findWorkspaceRoot } from "../lib/state.js";
|
|
|
7
7
|
import { detectLayout, listBrandDirs, resolveActiveBrand, brandStateDir, } from "../lib/layout.js";
|
|
8
8
|
import { auth401Hint, isPlaceholderApiUrl } from "../lib/backend-hint.js";
|
|
9
9
|
import { compareVersions, readPackageVersion, } from "../lib/release.js";
|
|
10
|
+
import { channelOf, pkgRef, stampChannel } from "../lib/channel.js";
|
|
10
11
|
export const helpText = `
|
|
11
12
|
exodus doctor — Preflight checks for your local Claude Code setup
|
|
12
13
|
|
|
@@ -58,7 +59,7 @@ export function checkEnvFile() {
|
|
|
58
59
|
return {
|
|
59
60
|
ok: false,
|
|
60
61
|
label: ".env",
|
|
61
|
-
fix:
|
|
62
|
+
fix: `run \`npx ${pkgRef()} init\`, then paste your dashboard .env block (Settings → Claude Code → Copy .env block)`,
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
const content = fs.readFileSync(envPath, "utf8");
|
|
@@ -80,7 +81,7 @@ export function checkEnvFile() {
|
|
|
80
81
|
ok: false,
|
|
81
82
|
label: ".env",
|
|
82
83
|
detail: "missing EXODUS_API_URL or EXODUS_API_KEY",
|
|
83
|
-
fix:
|
|
84
|
+
fix: `run \`npx ${pkgRef()} init\`, then paste your dashboard .env block (Settings → Claude Code → Copy .env block)`,
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
export async function checkWhoami() {
|
|
@@ -124,7 +125,7 @@ export async function checkWhoami() {
|
|
|
124
125
|
todo: true,
|
|
125
126
|
label: "Brand primer",
|
|
126
127
|
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
|
|
128
|
+
fix: `say "exodus, set up my brand primer" (or run \`npx ${pkgRef()} foundation\` from a source doc, or paste it at /settings?tab=brands&brand=${d.workspaceSlug})`,
|
|
128
129
|
};
|
|
129
130
|
}
|
|
130
131
|
return {
|
|
@@ -182,7 +183,7 @@ export async function checkActiveBrandMatch() {
|
|
|
182
183
|
ok: false,
|
|
183
184
|
label: "Active brand",
|
|
184
185
|
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
|
|
186
|
+
fix: `run \`npx ${pkgRef()} brand use <slug>\` with one of your brands, or \`npx ${pkgRef()} brand clear\` to fall back to the key default`,
|
|
186
187
|
};
|
|
187
188
|
}
|
|
188
189
|
const FRESHNESS_SUBTREES = ["commands", "lib", "bin"];
|
|
@@ -251,7 +252,9 @@ export function checkExodusDistFreshness(pkgRootOverride) {
|
|
|
251
252
|
}
|
|
252
253
|
return { ok: true, label: "Exodus CLI build", detail: "dist up-to-date" };
|
|
253
254
|
}
|
|
254
|
-
|
|
255
|
+
function npmRegistryUrl(channel) {
|
|
256
|
+
return `https://registry.npmjs.org/@aicopycoders/exodus/${channel}`;
|
|
257
|
+
}
|
|
255
258
|
export async function checkVersionCurrency(pkgRootOverride, fetchImpl = fetch) {
|
|
256
259
|
let pkgRoot;
|
|
257
260
|
if (pkgRootOverride) {
|
|
@@ -265,28 +268,29 @@ export async function checkVersionCurrency(pkgRootOverride, fetchImpl = fetch) {
|
|
|
265
268
|
}
|
|
266
269
|
}
|
|
267
270
|
const local = readPackageVersion(pkgRoot);
|
|
271
|
+
const channel = channelOf(local);
|
|
268
272
|
try {
|
|
269
273
|
const ctrl = new AbortController();
|
|
270
274
|
const t = setTimeout(() => ctrl.abort(), 8_000);
|
|
271
275
|
let res;
|
|
272
276
|
try {
|
|
273
|
-
res = await fetchImpl(
|
|
277
|
+
res = await fetchImpl(npmRegistryUrl(channel), { signal: ctrl.signal });
|
|
274
278
|
}
|
|
275
279
|
finally {
|
|
276
280
|
clearTimeout(t);
|
|
277
281
|
}
|
|
278
282
|
if (!res.ok)
|
|
279
283
|
throw new Error(`HTTP ${res.status}`);
|
|
280
|
-
const
|
|
281
|
-
if (compareVersions(local,
|
|
284
|
+
const published = (await res.json()).version;
|
|
285
|
+
if (compareVersions(local, published) >= 0) {
|
|
282
286
|
return { ok: true, label: "Exodus version", detail: `${local} — up to date` };
|
|
283
287
|
}
|
|
284
288
|
return {
|
|
285
289
|
ok: true,
|
|
286
290
|
warn: true,
|
|
287
291
|
label: "Exodus version",
|
|
288
|
-
detail: `${local} installed, ${
|
|
289
|
-
fix:
|
|
292
|
+
detail: `${local} installed, ${published} available`,
|
|
293
|
+
fix: `run \`npx ${pkgRef(channel)} init\` to update (or \`npm install -g ${pkgRef(channel)}\`)`,
|
|
290
294
|
};
|
|
291
295
|
}
|
|
292
296
|
catch (err) {
|
|
@@ -326,7 +330,7 @@ export async function checkApiAndDrive() {
|
|
|
326
330
|
ok: false,
|
|
327
331
|
label: "Dashboard API key",
|
|
328
332
|
detail: `HTTP ${res.status}`,
|
|
329
|
-
fix:
|
|
333
|
+
fix: `regenerate at Settings → API Keys, then paste the new .env block (or re-run \`npx ${pkgRef()} init\`)`,
|
|
330
334
|
},
|
|
331
335
|
{
|
|
332
336
|
ok: false,
|
|
@@ -445,7 +449,9 @@ export async function checkDashboardAuth() {
|
|
|
445
449
|
ok: false,
|
|
446
450
|
label: "Dashboard auth (dashboard-routed pipelines)",
|
|
447
451
|
detail: res.data?.error ?? "401 from /api/doctor/dashboard-ping",
|
|
448
|
-
fix: hint
|
|
452
|
+
fix: hint
|
|
453
|
+
? stampChannel(hint)
|
|
454
|
+
: `run \`npx ${pkgRef()} init\` to update the CLI — older releases skipped the Bearer header on dashboard routes`,
|
|
449
455
|
};
|
|
450
456
|
}
|
|
451
457
|
return {
|
|
@@ -476,7 +482,7 @@ export function checkLayout() {
|
|
|
476
482
|
return {
|
|
477
483
|
ok: true,
|
|
478
484
|
label: "Layout",
|
|
479
|
-
detail:
|
|
485
|
+
detail: `single-brand (legacy) — run \`npx ${pkgRef()} migrate\` to switch to the multi-brand layout`,
|
|
480
486
|
};
|
|
481
487
|
}
|
|
482
488
|
const brands = listBrandDirs(root);
|
|
@@ -490,7 +496,7 @@ export function checkLayout() {
|
|
|
490
496
|
warn: true,
|
|
491
497
|
label: "Layout",
|
|
492
498
|
detail: `multi-brand — no brand folders yet; ${active}`,
|
|
493
|
-
fix:
|
|
499
|
+
fix: `run \`npx ${pkgRef()} init\` (or \`npx ${pkgRef()} brand use <slug>\`) to create your brand folders`,
|
|
494
500
|
};
|
|
495
501
|
}
|
|
496
502
|
const missingProfiles = brands.filter((b) => !fs.existsSync(path.join(b.dir, "state", "brand-profile.md")));
|
|
@@ -500,7 +506,7 @@ export function checkLayout() {
|
|
|
500
506
|
warn: true,
|
|
501
507
|
label: "Layout",
|
|
502
508
|
detail: `multi-brand, ${brands.length} brand folder(s); ${active}; missing brand profile in: ${missingProfiles.map((b) => b.slug).join(", ")}`,
|
|
503
|
-
fix:
|
|
509
|
+
fix: `run \`npx ${pkgRef()} init\` to refresh every brand folder's profile`,
|
|
504
510
|
};
|
|
505
511
|
}
|
|
506
512
|
return {
|
|
@@ -518,7 +524,7 @@ export function checkBrandProfileGenesisDepth() {
|
|
|
518
524
|
ok: false,
|
|
519
525
|
label: "brand-profile",
|
|
520
526
|
detail: `${rel} is missing`,
|
|
521
|
-
fix:
|
|
527
|
+
fix: `run \`npx ${pkgRef()} brand use <slug>\` to generate it`,
|
|
522
528
|
};
|
|
523
529
|
}
|
|
524
530
|
const contents = fs.readFileSync(filePath, "utf-8");
|
package/dist/commands/genesis.js
CHANGED
|
@@ -22,7 +22,7 @@ Options:
|
|
|
22
22
|
--seeds <file|"text"> Per-run creative seeds. File: one per line OR a
|
|
23
23
|
JSON array. Inline: a single seed.
|
|
24
24
|
--steering "<text>" With regenerate: re-roll guidance for the hook pool.
|
|
25
|
-
--awareness <level> unaware | problem-aware (default) | solution-aware | product-aware
|
|
25
|
+
--awareness <level> unaware | problem-aware (default) | solution-aware | product-aware | most-aware
|
|
26
26
|
--passes <n> Writing passes per bot — Mario + Infeed (1-5, default 1 = 2 variants)
|
|
27
27
|
--variants <n> Advanced: raw total variant count (1-10); overrides --passes
|
|
28
28
|
--ad-account <id> Meta ad account ID for the top-ads-biased track
|
|
@@ -239,6 +239,8 @@ export async function run(flags) {
|
|
|
239
239
|
return runContinue(flags, cc);
|
|
240
240
|
if (sub === "regenerate")
|
|
241
241
|
return runRegenerate(flags, cc);
|
|
242
|
+
if (sub === "reject")
|
|
243
|
+
return runReject(flags, cc);
|
|
242
244
|
if (sub === "hooks")
|
|
243
245
|
return runHooks(flags);
|
|
244
246
|
const action = resolveGenesisAction(flags);
|
|
@@ -575,6 +577,22 @@ async function runRegenerate(flags, cc) {
|
|
|
575
577
|
: `Regenerating hooks for run ${runId}…`);
|
|
576
578
|
await waitForGenesisRun(runId);
|
|
577
579
|
}
|
|
580
|
+
async function runReject(flags, cc) {
|
|
581
|
+
const runId = typeof flags["id"] === "string" ? flags["id"].trim() : "";
|
|
582
|
+
if (!runId) {
|
|
583
|
+
console.error("Usage: exodus genesis reject --id <runId> [--superseded-by <newRunId>]");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const supersededBy = typeof flags["superseded-by"] === "string" ? flags["superseded-by"].trim() : "";
|
|
587
|
+
const res = await apiPost("/api/v2/genesis/reject", supersededBy ? { runId, supersededByRunId: supersededBy } : { runId }, { ccCommand: cc });
|
|
588
|
+
if (!res.ok) {
|
|
589
|
+
console.log(formatError(res));
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
console.log(`Run ${runId} rejected — marked superseded. Nothing further will run.`);
|
|
593
|
+
if (supersededBy)
|
|
594
|
+
console.log(`Linked replacement run: ${supersededBy}`);
|
|
595
|
+
}
|
|
578
596
|
function requireActiveBrand() {
|
|
579
597
|
const slug = resolveActiveBrand().slug;
|
|
580
598
|
if (!slug) {
|
package/dist/commands/init.js
CHANGED
|
@@ -3,12 +3,13 @@ import path from "node:path";
|
|
|
3
3
|
import { writeEnvScaffold, ensureGitignore, writeSkills, writeReferences, writeDocs, ensureBaseDirs, } from "../lib/scaffold.js";
|
|
4
4
|
import { loadWorkspaceEnv } from "../lib/load-env.js";
|
|
5
5
|
import { ensureBrandDir } from "../lib/layout.js";
|
|
6
|
+
import { pkgRef } from "../lib/channel.js";
|
|
6
7
|
export const helpText = `
|
|
7
8
|
exodus init — Set up (or refresh) an Exodus workspace in the current folder
|
|
8
9
|
|
|
9
10
|
Usage:
|
|
10
|
-
npx
|
|
11
|
-
npx
|
|
11
|
+
npx ${pkgRef()} init Scaffold the workspace + install skills
|
|
12
|
+
npx ${pkgRef()} init --root <dir> Target a specific folder
|
|
12
13
|
|
|
13
14
|
Run this in a fresh, empty folder. It creates the workspace layout, writes the
|
|
14
15
|
Exodus + Genesis skills into .claude/skills/, scaffolds a comment-only .env, and
|
package/dist/commands/keys.js
CHANGED
package/dist/commands/meme.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiPostDashboard, getDashboardUrl } from "../lib/client.js";
|
|
2
2
|
import { formatError } from "../lib/format.js";
|
|
3
|
+
import { pkgRef } from "../lib/channel.js";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
5
|
export const helpText = `
|
|
5
6
|
exodus meme — Meme generator: brief → recommended formats → one batched run
|
|
@@ -26,7 +27,7 @@ Flow:
|
|
|
26
27
|
|
|
27
28
|
Keys (strict BYOK — checked server-side before anything starts):
|
|
28
29
|
classic (layer 1) memes need your Imgflip login; AI (layer 2/3) memes need
|
|
29
|
-
your Kie.ai key; captions always need your LLM key. Run \`npx
|
|
30
|
+
your Kie.ai key; captions always need your LLM key. Run \`npx ${pkgRef()} doctor\`.
|
|
30
31
|
|
|
31
32
|
Examples:
|
|
32
33
|
exodus meme recommend --brief "grounding sheets reduce inflammation"
|
|
@@ -114,7 +115,7 @@ async function runRecommend(flags) {
|
|
|
114
115
|
if (!res.ok) {
|
|
115
116
|
console.error(formatError(res));
|
|
116
117
|
if (res.status === 400) {
|
|
117
|
-
console.error(
|
|
118
|
+
console.error(`Hint: run \`npx ${pkgRef()} doctor\` to check your keys.`);
|
|
118
119
|
}
|
|
119
120
|
process.exit(1);
|
|
120
121
|
return;
|
|
@@ -171,7 +172,7 @@ async function runRun(flags) {
|
|
|
171
172
|
if (!res.ok) {
|
|
172
173
|
console.error(formatError(res));
|
|
173
174
|
if (res.status === 400) {
|
|
174
|
-
console.error(
|
|
175
|
+
console.error(`Hint: run \`npx ${pkgRef()} doctor\` to check your keys.`);
|
|
175
176
|
}
|
|
176
177
|
process.exit(1);
|
|
177
178
|
return;
|
|
@@ -233,7 +234,7 @@ async function runRegenerate(flags) {
|
|
|
233
234
|
if (!res.ok) {
|
|
234
235
|
console.error(formatError(res));
|
|
235
236
|
if (res.status === 400) {
|
|
236
|
-
console.error(
|
|
237
|
+
console.error(`Hint: run \`npx ${pkgRef()} doctor\` to check your keys.`);
|
|
237
238
|
}
|
|
238
239
|
process.exit(1);
|
|
239
240
|
return;
|
package/dist/commands/migrate.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { apiGet, apiGetDashboard } from "../lib/client.js";
|
|
5
5
|
import { getActiveBrand, setLayoutVersion, findWorkspaceRoot } from "../lib/state.js";
|
|
6
6
|
import { detectLayout, brandDirFor, ensureBrandDir, BRAND_MARKER_FILE, } from "../lib/layout.js";
|
|
7
|
+
import { pkgRef } from "../lib/channel.js";
|
|
7
8
|
export const helpText = `
|
|
8
9
|
exodus migrate — convert a single-brand install to the multi-brand layout
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ What it does (one-time, opt-in — updates never force this):
|
|
|
15
16
|
falling back to the key's bound brand).
|
|
16
17
|
2. Creates a subfolder for it and MOVES your brand-specific files in:
|
|
17
18
|
state/ and output/ → <brand>/state/ and <brand>/output/
|
|
18
|
-
3. Marks the install as multi-brand. From then on, \`npx
|
|
19
|
+
3. Marks the install as multi-brand. From then on, \`npx ${pkgRef()} init\`
|
|
19
20
|
creates a subfolder for every brand you own, and running a command
|
|
20
21
|
from inside a brand's folder targets that brand automatically.
|
|
21
22
|
|
|
@@ -94,7 +95,7 @@ async function resolveBrand() {
|
|
|
94
95
|
}
|
|
95
96
|
if (!slug) {
|
|
96
97
|
fail("could not determine which brand this install belongs to.\n" +
|
|
97
|
-
|
|
98
|
+
` Run \`npx ${pkgRef()} brand use <slug>\` first (or check your network/.env), then re-run \`npx ${pkgRef()} migrate\`.`);
|
|
98
99
|
}
|
|
99
100
|
if (!name) {
|
|
100
101
|
try {
|
|
@@ -115,7 +116,7 @@ export async function run(flags) {
|
|
|
115
116
|
const root = findWorkspaceRoot();
|
|
116
117
|
if (detectLayout(root) === "v2") {
|
|
117
118
|
console.log("✓ Already on the multi-brand layout — nothing to migrate.");
|
|
118
|
-
console.log(
|
|
119
|
+
console.log(` Run \`npx ${pkgRef()} init\` to sync brand folders.`);
|
|
119
120
|
return;
|
|
120
121
|
}
|
|
121
122
|
const brand = await resolveBrand();
|
|
@@ -128,7 +129,7 @@ export async function run(flags) {
|
|
|
128
129
|
if (conflicts.length > 0) {
|
|
129
130
|
fail(`"${brandDirRel}/" already contains different versions of:\n` +
|
|
130
131
|
conflicts.map((f) => ` ${f}`).join("\n") +
|
|
131
|
-
|
|
132
|
+
`\n Reconcile or remove them, then re-run \`npx ${pkgRef()} migrate\`. Nothing was changed.`);
|
|
132
133
|
}
|
|
133
134
|
console.log(`Migrating this install to the multi-brand layout...`);
|
|
134
135
|
console.log(` brand: ${brand.slug} (${brand.name})`);
|
|
@@ -151,8 +152,8 @@ export async function run(flags) {
|
|
|
151
152
|
Shared files (.env, exodus/, .claude/skills/, references/) did not move.
|
|
152
153
|
|
|
153
154
|
Next steps:
|
|
154
|
-
• Run \`npx
|
|
155
|
+
• Run \`npx ${pkgRef()} init\` to pull folders for your other brands.
|
|
155
156
|
• Run commands from inside ${brandDirRel}/ to target it automatically,
|
|
156
|
-
or keep using \`npx
|
|
157
|
+
or keep using \`npx ${pkgRef()} brand use <slug>\` from anywhere.
|
|
157
158
|
`.trimEnd());
|
|
158
159
|
}
|
package/dist/commands/primer.js
CHANGED
|
@@ -36,8 +36,8 @@ Genesis writer for that primer honors it on every run.
|
|
|
36
36
|
|
|
37
37
|
<lane> is one of: bodyUnaware | bodyAware | hooks | headlines
|
|
38
38
|
(aliases: body-unaware/unaware/problem-aware, body-aware/solution-aware/
|
|
39
|
-
product-aware, hook, headline). Setting just one of --always /
|
|
40
|
-
leaves the other field as-is.
|
|
39
|
+
product-aware/most-aware, hook, headline). Setting just one of --always /
|
|
40
|
+
--never leaves the other field as-is.
|
|
41
41
|
|
|
42
42
|
Interactive build flags:
|
|
43
43
|
--file <path> Read the submission (winning ads + product info) from a file
|
|
@@ -397,6 +397,8 @@ const LANE_ALIASES = {
|
|
|
397
397
|
solutionaware: "bodyAware",
|
|
398
398
|
"product-aware": "bodyAware",
|
|
399
399
|
productaware: "bodyAware",
|
|
400
|
+
"most-aware": "bodyAware",
|
|
401
|
+
mostaware: "bodyAware",
|
|
400
402
|
hooks: "hooks",
|
|
401
403
|
hook: "hooks",
|
|
402
404
|
headlines: "headlines",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export declare const helpText: string;
|
|
2
|
+
interface NValue {
|
|
3
|
+
slug: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
interface MapOutcome {
|
|
7
|
+
slug: string;
|
|
8
|
+
name: string;
|
|
9
|
+
lens?: "Type" | "Location" | "Function" | "Moment" | "Severity";
|
|
10
|
+
subs: NValue[];
|
|
11
|
+
}
|
|
12
|
+
interface MapGroup {
|
|
13
|
+
name: string;
|
|
14
|
+
values: NValue[];
|
|
15
|
+
}
|
|
16
|
+
export interface SegmentMap {
|
|
17
|
+
productWord: string;
|
|
18
|
+
brandLabel: string;
|
|
19
|
+
outcomes: MapOutcome[];
|
|
20
|
+
demoGroups: MapGroup[];
|
|
21
|
+
facetFamilies: MapGroup[];
|
|
22
|
+
}
|
|
23
|
+
export interface ImportSummary {
|
|
24
|
+
added: string[];
|
|
25
|
+
keptRenamed: string[];
|
|
26
|
+
deleted: string[];
|
|
27
|
+
subsDeleted: string[];
|
|
28
|
+
demoValuesDeleted: string[];
|
|
29
|
+
facetValuesDeleted: string[];
|
|
30
|
+
personasArchived: string[];
|
|
31
|
+
personasDeleted: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function asSummary(data: unknown): ImportSummary;
|
|
34
|
+
export declare function isDestructive(summary: ImportSummary): boolean;
|
|
35
|
+
export declare function formatImportSummary(summary: ImportSummary): string;
|
|
36
|
+
export declare function formatMapSummary(map: SegmentMap): string;
|
|
37
|
+
export interface ImportDeps {
|
|
38
|
+
post: (path: string, body: unknown) => Promise<{
|
|
39
|
+
ok: boolean;
|
|
40
|
+
status: number;
|
|
41
|
+
data: unknown;
|
|
42
|
+
}>;
|
|
43
|
+
readFile: (path: string) => string;
|
|
44
|
+
}
|
|
45
|
+
export interface ImportFlowResult {
|
|
46
|
+
code: number;
|
|
47
|
+
lines: string[];
|
|
48
|
+
calls: Array<{
|
|
49
|
+
dryRun: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
export declare function importFlow(file: string, opts: {
|
|
53
|
+
yes: boolean;
|
|
54
|
+
json: boolean;
|
|
55
|
+
}, deps: ImportDeps): Promise<ImportFlowResult>;
|
|
56
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|
|
57
|
+
export {};
|