@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.
- package/assets/references/awareness-framework.md +156 -0
- package/assets/references/cast-video.md +165 -0
- package/assets/references/copy-blocks.md +138 -0
- package/assets/references/creative-strategy.md +990 -0
- package/assets/references/editing-rules.md +81 -0
- package/assets/references/hook-quality-checklist.md +153 -0
- package/assets/references/iterations.md +73 -0
- package/assets/references/scrawls.md +483 -0
- package/assets/references/segments.md +79 -0
- package/assets/skills/exodus/SKILL.md +50 -0
- package/assets/skills/exodus-brand/SKILL.md +117 -0
- package/assets/skills/exodus-browse/SKILL.md +96 -0
- package/assets/skills/exodus-creative/SKILL.md +94 -0
- package/assets/skills/exodus-drive/SKILL.md +118 -0
- package/assets/skills/exodus-foundation/SKILL.md +170 -0
- package/assets/skills/exodus-genesis/SKILL.md +159 -0
- package/assets/skills/exodus-idea/SKILL.md +78 -0
- package/assets/skills/exodus-image/SKILL.md +224 -0
- package/assets/skills/exodus-meme/SKILL.md +183 -0
- package/assets/skills/exodus-primer/SKILL.md +145 -0
- package/assets/skills/exodus-strategist/SKILL.md +217 -0
- package/assets/skills/exodus-swipe/SKILL.md +85 -0
- package/assets/skills/exodus-template/SKILL.md +106 -0
- package/assets/skills/exodus-write/SKILL.md +157 -0
- package/assets/skills/genesis-bots/.env.example +12 -0
- package/assets/skills/genesis-bots/README.txt +29 -0
- package/assets/skills/genesis-bots/SKILL.md +219 -0
- package/assets/skills/genesis-bots/scripts/genesis-stream.mjs +307 -0
- package/assets/skills/genesis-bots/scripts/genesis-stream.py +220 -0
- package/dist/bin/exodus.d.ts +2 -0
- package/dist/bin/exodus.js +177 -0
- package/dist/commands/__tests__/genesis.hookgate.test.d.ts +1 -0
- package/dist/commands/__tests__/genesis.hookgate.test.js +12 -0
- package/dist/commands/brand.d.ts +24 -0
- package/dist/commands/brand.js +292 -0
- package/dist/commands/browse.d.ts +9 -0
- package/dist/commands/browse.js +132 -0
- package/dist/commands/creative.d.ts +2 -0
- package/dist/commands/creative.js +223 -0
- package/dist/commands/doctor.d.ts +21 -0
- package/dist/commands/doctor.js +590 -0
- package/dist/commands/drive.d.ts +2 -0
- package/dist/commands/drive.js +154 -0
- package/dist/commands/foundation.d.ts +2 -0
- package/dist/commands/foundation.js +468 -0
- package/dist/commands/genesis.d.ts +64 -0
- package/dist/commands/genesis.js +564 -0
- package/dist/commands/idea.d.ts +53 -0
- package/dist/commands/idea.js +333 -0
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +249 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/keys.d.ts +2 -0
- package/dist/commands/keys.js +101 -0
- package/dist/commands/meme.d.ts +12 -0
- package/dist/commands/meme.js +272 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.js +158 -0
- package/dist/commands/primer.d.ts +2 -0
- package/dist/commands/primer.js +540 -0
- package/dist/commands/read-doc.d.ts +3 -0
- package/dist/commands/read-doc.js +62 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +205 -0
- package/dist/commands/swipe.d.ts +2 -0
- package/dist/commands/swipe.js +398 -0
- package/dist/commands/template.d.ts +2 -0
- package/dist/commands/template.js +225 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +51 -0
- package/dist/lib/ad-library-write.d.ts +15 -0
- package/dist/lib/ad-library-write.js +67 -0
- package/dist/lib/assets.d.ts +3 -0
- package/dist/lib/assets.js +23 -0
- package/dist/lib/backend-hint.d.ts +2 -0
- package/dist/lib/backend-hint.js +22 -0
- package/dist/lib/cc-command.d.ts +1 -0
- package/dist/lib/cc-command.js +13 -0
- package/dist/lib/client.d.ts +23 -0
- package/dist/lib/client.js +213 -0
- package/dist/lib/format.d.ts +8 -0
- package/dist/lib/format.js +172 -0
- package/dist/lib/intel-format.d.ts +1 -0
- package/dist/lib/intel-format.js +38 -0
- package/dist/lib/keys-sync.d.ts +16 -0
- package/dist/lib/keys-sync.js +88 -0
- package/dist/lib/layout.d.ts +38 -0
- package/dist/lib/layout.js +153 -0
- package/dist/lib/load-env.d.ts +1 -0
- package/dist/lib/load-env.js +47 -0
- package/dist/lib/pixar-orchestrator-types.d.ts +54 -0
- package/dist/lib/pixar-orchestrator-types.js +1 -0
- package/dist/lib/poll.d.ts +14 -0
- package/dist/lib/poll.js +32 -0
- package/dist/lib/preflight.d.ts +18 -0
- package/dist/lib/preflight.js +56 -0
- package/dist/lib/prompts.d.ts +9 -0
- package/dist/lib/prompts.js +99 -0
- package/dist/lib/reel-write.d.ts +14 -0
- package/dist/lib/reel-write.js +59 -0
- package/dist/lib/release.d.ts +4 -0
- package/dist/lib/release.js +81 -0
- package/dist/lib/scaffold.d.ts +8 -0
- package/dist/lib/scaffold.js +67 -0
- package/dist/lib/scout-hydrate.d.ts +1 -0
- package/dist/lib/scout-hydrate.js +10 -0
- package/dist/lib/state.d.ts +6 -0
- package/dist/lib/state.js +49 -0
- package/dist/lib/template-constants.d.ts +4 -0
- package/dist/lib/template-constants.js +85 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +19 -0
- package/package.json +30 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { PROVIDER_ENV_MAP, fetchRemoteKeys, mapKeysToEnvVars, resolveEnvFilePath, upsertEnvVars, } from "../lib/keys-sync.js";
|
|
2
|
+
export const helpText = `
|
|
3
|
+
exodus keys — sync your provider API keys from the dashboard to local .env
|
|
4
|
+
|
|
5
|
+
You enter your keys once in the dashboard (Settings → Claude Code): your provider
|
|
6
|
+
LLM key (Anthropic or OpenRouter) plus Kie.ai. This command pulls them down
|
|
7
|
+
(decrypted, scoped to you) and writes them into the workspace .env so the local
|
|
8
|
+
manual-Genesis path (the genesis-bots skill calls bots with your provider key)
|
|
9
|
+
and other local tools have them — you never have to edit .env by hand.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
exodus keys status Show which keys are set in the dashboard vs locally
|
|
13
|
+
exodus keys pull Pull your dashboard keys into the workspace .env
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
• Uses your EXODUS_API_KEY (.env) to authenticate — run 'exodus whoami' first
|
|
17
|
+
if you're unsure which account you're on.
|
|
18
|
+
• Values are never printed. Only key names and statuses are shown.
|
|
19
|
+
• 'pull' preserves every other line in your .env and only upserts the keys.
|
|
20
|
+
`.trim();
|
|
21
|
+
const PROVIDER_LABEL = {
|
|
22
|
+
genesis: "Genesis",
|
|
23
|
+
anthropic: "Anthropic",
|
|
24
|
+
openrouter: "OpenRouter",
|
|
25
|
+
elevenlabs: "ElevenLabs",
|
|
26
|
+
kie: "Kie.ai",
|
|
27
|
+
};
|
|
28
|
+
async function status() {
|
|
29
|
+
let remote;
|
|
30
|
+
try {
|
|
31
|
+
remote = await fetchRemoteKeys();
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
console.error(`Could not reach the dashboard: ${e.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log("Pipeline keys\n");
|
|
39
|
+
const providers = Object.keys(PROVIDER_ENV_MAP);
|
|
40
|
+
for (const provider of providers) {
|
|
41
|
+
const inDashboard = provider in remote.keys;
|
|
42
|
+
const envName = PROVIDER_ENV_MAP[provider];
|
|
43
|
+
const localSet = !!process.env[envName];
|
|
44
|
+
const label = (PROVIDER_LABEL[provider] ?? provider).padEnd(11);
|
|
45
|
+
const dash = inDashboard ? "dashboard ✓" : "dashboard —";
|
|
46
|
+
const local = localSet ? "local ✓" : "local —";
|
|
47
|
+
console.log(` ${label} ${dash} ${local} (${envName})`);
|
|
48
|
+
}
|
|
49
|
+
if (remote.failed.length) {
|
|
50
|
+
console.log(`\n⚠ Could not decrypt: ${remote.failed.join(", ")} — re-save in the dashboard.`);
|
|
51
|
+
}
|
|
52
|
+
console.log(`\nRun 'exodus keys pull' to write dashboard keys into your .env.`);
|
|
53
|
+
}
|
|
54
|
+
async function pull() {
|
|
55
|
+
let remote;
|
|
56
|
+
try {
|
|
57
|
+
remote = await fetchRemoteKeys();
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
console.error(`Could not reach the dashboard: ${e.message}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const count = Object.keys(remote.keys).length;
|
|
65
|
+
if (count === 0) {
|
|
66
|
+
console.log("No keys are set in the dashboard yet. Add them at Settings → Pipeline Keys, then run this again.");
|
|
67
|
+
if (remote.failed.length) {
|
|
68
|
+
console.log(`(Could not decrypt: ${remote.failed.join(", ")}.)`);
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { vars, skipped } = mapKeysToEnvVars(remote.keys);
|
|
73
|
+
const envPath = resolveEnvFilePath();
|
|
74
|
+
const results = upsertEnvVars(envPath, vars);
|
|
75
|
+
const added = results.filter((r) => r.action === "added").map((r) => r.name);
|
|
76
|
+
const updated = results.filter((r) => r.action === "updated").map((r) => r.name);
|
|
77
|
+
const unchanged = results.filter((r) => r.action === "unchanged").map((r) => r.name);
|
|
78
|
+
console.log(`Synced ${results.length} key(s) → ${envPath}\n`);
|
|
79
|
+
if (added.length)
|
|
80
|
+
console.log(` added: ${added.join(", ")}`);
|
|
81
|
+
if (updated.length)
|
|
82
|
+
console.log(` updated: ${updated.join(", ")}`);
|
|
83
|
+
if (unchanged.length)
|
|
84
|
+
console.log(` unchanged: ${unchanged.join(", ")}`);
|
|
85
|
+
if (skipped.length) {
|
|
86
|
+
console.log(` skipped: ${skipped.join(", ")} (no known env var mapping)`);
|
|
87
|
+
}
|
|
88
|
+
if (remote.failed.length) {
|
|
89
|
+
console.log(`\n⚠ Could not decrypt: ${remote.failed.join(", ")} — re-save in the dashboard.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export async function run() {
|
|
93
|
+
const sub = process.argv.slice(2)[1] ?? "status";
|
|
94
|
+
if (sub === "pull")
|
|
95
|
+
return pull();
|
|
96
|
+
if (sub === "status")
|
|
97
|
+
return status();
|
|
98
|
+
console.error(`Unknown 'keys' sub-action: "${sub}"`);
|
|
99
|
+
console.error("Usage: exodus keys status | exodus keys pull");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const helpText: string;
|
|
2
|
+
export interface RunFormat {
|
|
3
|
+
layer: 1 | 2 | 3;
|
|
4
|
+
name: string;
|
|
5
|
+
template_id?: string;
|
|
6
|
+
template_name?: string;
|
|
7
|
+
box_count?: number;
|
|
8
|
+
format_id?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function normalizeFormat(entry: unknown, index: number): RunFormat;
|
|
11
|
+
export declare function normalizeFormats(raw: unknown): RunFormat[];
|
|
12
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { apiPostDashboard, getDashboardUrl } from "../lib/client.js";
|
|
2
|
+
import { formatError } from "../lib/format.js";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
export const helpText = `
|
|
5
|
+
exodus meme — Meme generator: brief → recommended formats → one batched run
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
exodus meme recommend --brief "<text>" [--avatar "<text>"]
|
|
9
|
+
exodus meme run --brief "<text>" --formats '<json>' [--avatar "<text>"] [--name "<label>"]
|
|
10
|
+
exodus meme run --brief "<text>" --formats-file <path> [--avatar "<text>"] [--name "<label>"]
|
|
11
|
+
exodus meme regenerate --brief "<text>" --layer 1 --template-id <id> --template-name "<name>" --boxes <N> [--id <runId>] [--avatar "<text>"]
|
|
12
|
+
exodus meme regenerate --brief "<text>" --layer 2|3 --format <formatId> [--hint "<text>"] [--id <runId>] [--avatar "<text>"]
|
|
13
|
+
|
|
14
|
+
Flow:
|
|
15
|
+
1. recommend → { recommendations: [...] } — 15 picks (5 per layer), each with
|
|
16
|
+
name, layer, reasoning, ad_angle. Layer 1 = classic Imgflip
|
|
17
|
+
templates; layers 2/3 = AI-image formats.
|
|
18
|
+
2. run → pass the picked recommendation objects straight into --formats
|
|
19
|
+
(a JSON array; both recommendation-shape and run-shape entries
|
|
20
|
+
are accepted, 1–50). Returns { runId } — the whole batch renders
|
|
21
|
+
server-side via Trigger.dev; closing your session won't kill it.
|
|
22
|
+
3. poll → exodus status --id <runId> --type creative
|
|
23
|
+
(terminal: complete | partial-error | error)
|
|
24
|
+
4. regenerate → re-render a single miss using the format fields from the
|
|
25
|
+
recommend output. Synchronous; returns the new image URL.
|
|
26
|
+
|
|
27
|
+
Keys (strict BYOK — checked server-side before anything starts):
|
|
28
|
+
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 @aicopycoders/exodus doctor\`.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
exodus meme recommend --brief "grounding sheets reduce inflammation"
|
|
33
|
+
exodus meme run --brief "grounding sheets reduce inflammation" --formats '[{"layer":2,"name":"Group Chat","format_id":"group-chat"}]'
|
|
34
|
+
exodus meme regenerate --brief "grounding sheets reduce inflammation" --layer 2 --format group-chat --hint "make the last message land the product" --id <runId>
|
|
35
|
+
`.trim();
|
|
36
|
+
function dashboardUrlForRun(runId) {
|
|
37
|
+
return `${getDashboardUrl()}/creative-suite/runs/${runId}`;
|
|
38
|
+
}
|
|
39
|
+
function flagString(flags, name) {
|
|
40
|
+
const v = flags[name];
|
|
41
|
+
return typeof v === "string" ? v : undefined;
|
|
42
|
+
}
|
|
43
|
+
function flagInt(flags, name) {
|
|
44
|
+
const v = flags[name];
|
|
45
|
+
if (typeof v !== "string")
|
|
46
|
+
return undefined;
|
|
47
|
+
const n = parseInt(v, 10);
|
|
48
|
+
return Number.isFinite(n) ? n : undefined;
|
|
49
|
+
}
|
|
50
|
+
function requireString(value, flag) {
|
|
51
|
+
if (!value) {
|
|
52
|
+
console.error(`Error: --${flag} is required.`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
throw new Error("unreachable");
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
export function normalizeFormat(entry, index) {
|
|
59
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
60
|
+
throw new Error(`formats[${index}] must be an object.`);
|
|
61
|
+
}
|
|
62
|
+
const e = entry;
|
|
63
|
+
const layer = e["layer"];
|
|
64
|
+
if (layer !== 1 && layer !== 2 && layer !== 3) {
|
|
65
|
+
throw new Error(`formats[${index}].layer must be 1, 2, or 3.`);
|
|
66
|
+
}
|
|
67
|
+
const name = typeof e["name"] === "string" ? e["name"].trim() : "";
|
|
68
|
+
if (!name) {
|
|
69
|
+
throw new Error(`formats[${index}].name is required.`);
|
|
70
|
+
}
|
|
71
|
+
if (layer === 1) {
|
|
72
|
+
const templateId = e["template_id"] ?? e["imgflip_template_id"] ?? e["format_id"];
|
|
73
|
+
if (templateId === undefined || templateId === null || templateId === "") {
|
|
74
|
+
throw new Error(`formats[${index}] (layer 1) needs template_id (or imgflip_template_id / format_id from recommend output).`);
|
|
75
|
+
}
|
|
76
|
+
const templateName = typeof e["template_name"] === "string" && e["template_name"].trim()
|
|
77
|
+
? e["template_name"].trim()
|
|
78
|
+
: name;
|
|
79
|
+
const boxRaw = e["box_count"] ?? e["imgflip_box_count"];
|
|
80
|
+
const boxCount = typeof boxRaw === "number" && Number.isFinite(boxRaw) ? boxRaw : 2;
|
|
81
|
+
return {
|
|
82
|
+
layer: 1,
|
|
83
|
+
name,
|
|
84
|
+
template_id: String(templateId),
|
|
85
|
+
template_name: templateName,
|
|
86
|
+
box_count: boxCount,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const formatId = typeof e["format_id"] === "string" ? e["format_id"].trim() : "";
|
|
90
|
+
if (!formatId) {
|
|
91
|
+
throw new Error(`formats[${index}] (layer ${layer}) needs format_id.`);
|
|
92
|
+
}
|
|
93
|
+
return { layer, name, format_id: formatId };
|
|
94
|
+
}
|
|
95
|
+
export function normalizeFormats(raw) {
|
|
96
|
+
if (!Array.isArray(raw)) {
|
|
97
|
+
throw new Error("--formats must be a JSON array.");
|
|
98
|
+
}
|
|
99
|
+
if (raw.length === 0) {
|
|
100
|
+
throw new Error("--formats must contain at least one format.");
|
|
101
|
+
}
|
|
102
|
+
if (raw.length > 50) {
|
|
103
|
+
throw new Error(`--formats supports at most 50 formats per run (got ${raw.length}).`);
|
|
104
|
+
}
|
|
105
|
+
return raw.map((entry, i) => normalizeFormat(entry, i));
|
|
106
|
+
}
|
|
107
|
+
async function runRecommend(flags) {
|
|
108
|
+
const brief = requireString(flagString(flags, "brief"), "brief");
|
|
109
|
+
const avatar = flagString(flags, "avatar");
|
|
110
|
+
const res = await apiPostDashboard("/api/meme/recommend", {
|
|
111
|
+
brief,
|
|
112
|
+
...(avatar ? { avatar_description: avatar } : {}),
|
|
113
|
+
});
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
console.error(formatError(res));
|
|
116
|
+
if (res.status === 400) {
|
|
117
|
+
console.error("Hint: run `npx @aicopycoders/exodus doctor` to check your keys.");
|
|
118
|
+
}
|
|
119
|
+
process.exit(1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
123
|
+
}
|
|
124
|
+
async function runRun(flags) {
|
|
125
|
+
const brief = requireString(flagString(flags, "brief"), "brief");
|
|
126
|
+
const avatar = flagString(flags, "avatar");
|
|
127
|
+
const name = flagString(flags, "name");
|
|
128
|
+
const formatsInline = flagString(flags, "formats");
|
|
129
|
+
const formatsFile = flagString(flags, "formats-file");
|
|
130
|
+
if (!formatsInline && !formatsFile) {
|
|
131
|
+
console.error("Error: meme run requires --formats '<json>' or --formats-file <path>.");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
let formatsRaw;
|
|
136
|
+
if (formatsFile) {
|
|
137
|
+
try {
|
|
138
|
+
formatsRaw = readFileSync(formatsFile, "utf8");
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error(`Error: could not read --formats-file "${formatsFile}": ${err instanceof Error ? err.message : err}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
formatsRaw = formatsInline;
|
|
148
|
+
}
|
|
149
|
+
let formats;
|
|
150
|
+
try {
|
|
151
|
+
formats = normalizeFormats(JSON.parse(formatsRaw));
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (err instanceof SyntaxError) {
|
|
155
|
+
console.error("Error: --formats must be valid JSON (an array of format objects).");
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
159
|
+
}
|
|
160
|
+
process.exit(1);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const classics = formats.filter((f) => f.layer === 1).length;
|
|
164
|
+
console.log(`Meme run: ${formats.length} format${formats.length === 1 ? "" : "s"} (${classics} classic, ${formats.length - classics} AI)`);
|
|
165
|
+
const res = await apiPostDashboard("/api/meme/run", {
|
|
166
|
+
brief,
|
|
167
|
+
...(avatar ? { avatar_description: avatar } : {}),
|
|
168
|
+
...(name ? { name } : {}),
|
|
169
|
+
formats,
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
console.error(formatError(res));
|
|
173
|
+
if (res.status === 400) {
|
|
174
|
+
console.error("Hint: run `npx @aicopycoders/exodus doctor` to check your keys.");
|
|
175
|
+
}
|
|
176
|
+
process.exit(1);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(`✓ Meme run started`);
|
|
181
|
+
console.log(` runId: ${res.data.runId}`);
|
|
182
|
+
if (res.data.triggerRunId)
|
|
183
|
+
console.log(` triggerRunId: ${res.data.triggerRunId}`);
|
|
184
|
+
console.log(` memes: ${formats.length} queued`);
|
|
185
|
+
console.log(` dashboard: ${dashboardUrlForRun(res.data.runId)}`);
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log(`Poll: exodus status --id ${res.data.runId} --type creative`);
|
|
188
|
+
console.log("(The batch renders server-side — closing this session won't kill it.)");
|
|
189
|
+
}
|
|
190
|
+
async function runRegenerate(flags) {
|
|
191
|
+
const brief = requireString(flagString(flags, "brief"), "brief");
|
|
192
|
+
const layer = flagInt(flags, "layer");
|
|
193
|
+
if (layer !== 1 && layer !== 2 && layer !== 3) {
|
|
194
|
+
console.error("Error: meme regenerate requires --layer 1|2|3.");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const avatar = flagString(flags, "avatar");
|
|
199
|
+
const runId = flagString(flags, "id");
|
|
200
|
+
let body;
|
|
201
|
+
if (layer === 1) {
|
|
202
|
+
const templateId = requireString(flagString(flags, "template-id"), "template-id");
|
|
203
|
+
const templateName = requireString(flagString(flags, "template-name"), "template-name");
|
|
204
|
+
const boxes = flagInt(flags, "boxes");
|
|
205
|
+
if (boxes === undefined || boxes < 1 || boxes > 10) {
|
|
206
|
+
console.error("Error: --boxes <N> is required for layer 1 (1–10).");
|
|
207
|
+
process.exit(1);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
body = {
|
|
211
|
+
layer: 1,
|
|
212
|
+
brief,
|
|
213
|
+
template_id: templateId,
|
|
214
|
+
template_name: templateName,
|
|
215
|
+
box_count: boxes,
|
|
216
|
+
...(avatar ? { avatar_description: avatar } : {}),
|
|
217
|
+
...(runId ? { runId } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const formatId = requireString(flagString(flags, "format"), "format");
|
|
222
|
+
const hint = flagString(flags, "hint");
|
|
223
|
+
body = {
|
|
224
|
+
layer,
|
|
225
|
+
brief,
|
|
226
|
+
format_id: formatId,
|
|
227
|
+
...(hint ? { userHint: hint } : {}),
|
|
228
|
+
...(avatar ? { avatar_description: avatar } : {}),
|
|
229
|
+
...(runId ? { runId } : {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const res = await apiPostDashboard("/api/meme/regenerate", body);
|
|
233
|
+
if (!res.ok) {
|
|
234
|
+
console.error(formatError(res));
|
|
235
|
+
if (res.status === 400) {
|
|
236
|
+
console.error("Hint: run `npx @aicopycoders/exodus doctor` to check your keys.");
|
|
237
|
+
}
|
|
238
|
+
process.exit(1);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (res.data.image_url) {
|
|
242
|
+
console.log(`✓ Meme regenerated`);
|
|
243
|
+
console.log(` image: ${res.data.image_url}`);
|
|
244
|
+
if (runId)
|
|
245
|
+
console.log(` dashboard: ${dashboardUrlForRun(runId)}`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const SUBCOMMANDS = {
|
|
252
|
+
recommend: runRecommend,
|
|
253
|
+
run: runRun,
|
|
254
|
+
regenerate: runRegenerate,
|
|
255
|
+
};
|
|
256
|
+
export async function run(flags) {
|
|
257
|
+
const sub = process.argv[3] ?? "";
|
|
258
|
+
const handler = SUBCOMMANDS[sub];
|
|
259
|
+
if (!handler) {
|
|
260
|
+
if (sub && !sub.startsWith("--")) {
|
|
261
|
+
console.error(`Error: unknown meme subcommand "${sub}".`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.error("Error: meme requires a subcommand (recommend | run | regenerate).");
|
|
265
|
+
}
|
|
266
|
+
console.error("");
|
|
267
|
+
console.error(helpText);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
await handler(flags);
|
|
272
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { apiGet, apiGetDashboard } from "../lib/client.js";
|
|
5
|
+
import { getActiveBrand, setLayoutVersion, findWorkspaceRoot } from "../lib/state.js";
|
|
6
|
+
import { detectLayout, brandDirFor, ensureBrandDir, BRAND_MARKER_FILE, } from "../lib/layout.js";
|
|
7
|
+
export const helpText = `
|
|
8
|
+
exodus migrate — convert a single-brand install to the multi-brand layout
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
exodus migrate
|
|
12
|
+
|
|
13
|
+
What it does (one-time, opt-in — updates never force this):
|
|
14
|
+
1. Figures out which brand this install belongs to (your active brand,
|
|
15
|
+
falling back to the key's bound brand).
|
|
16
|
+
2. Creates a subfolder for it and MOVES your brand-specific files in:
|
|
17
|
+
state/ and output/ → <brand>/state/ and <brand>/output/
|
|
18
|
+
3. Marks the install as multi-brand. From then on, \`npx @aicopycoders/exodus@latest init\`
|
|
19
|
+
creates a subfolder for every brand you own, and running a command
|
|
20
|
+
from inside a brand's folder targets that brand automatically.
|
|
21
|
+
|
|
22
|
+
Shared files (.env, exodus/, .claude/skills/, references/) stay where they
|
|
23
|
+
are. Safe to re-run: an already-migrated install exits immediately. Your
|
|
24
|
+
state/ files are backed up under .backup/ before anything moves.
|
|
25
|
+
|
|
26
|
+
Already on the multi-brand layout (fresh installs after the redesign)?
|
|
27
|
+
You don't need this command.
|
|
28
|
+
`.trim();
|
|
29
|
+
function fail(msg, code = 1) {
|
|
30
|
+
console.error(`exodus migrate: ${msg}`);
|
|
31
|
+
process.exit(code);
|
|
32
|
+
}
|
|
33
|
+
function ts() {
|
|
34
|
+
const d = new Date();
|
|
35
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
36
|
+
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
37
|
+
}
|
|
38
|
+
export function findMergeConflicts(src, dest) {
|
|
39
|
+
if (!fs.existsSync(src) || !fs.existsSync(dest))
|
|
40
|
+
return [];
|
|
41
|
+
const conflicts = [];
|
|
42
|
+
const walk = (rel) => {
|
|
43
|
+
const srcFull = path.join(src, rel);
|
|
44
|
+
for (const entry of fs.readdirSync(srcFull, { withFileTypes: true })) {
|
|
45
|
+
const childRel = path.join(rel, entry.name);
|
|
46
|
+
const destFull = path.join(dest, childRel);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
if (fs.existsSync(destFull))
|
|
49
|
+
walk(childRel);
|
|
50
|
+
}
|
|
51
|
+
else if (entry.isFile() && fs.existsSync(destFull)) {
|
|
52
|
+
try {
|
|
53
|
+
const a = fs.readFileSync(path.join(src, childRel));
|
|
54
|
+
const b = fs.readFileSync(destFull);
|
|
55
|
+
if (!a.equals(b))
|
|
56
|
+
conflicts.push(childRel);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
conflicts.push(childRel);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
walk("");
|
|
65
|
+
return conflicts;
|
|
66
|
+
}
|
|
67
|
+
function moveDir(src, dest) {
|
|
68
|
+
if (!fs.existsSync(src))
|
|
69
|
+
return;
|
|
70
|
+
if (!fs.existsSync(dest)) {
|
|
71
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
72
|
+
try {
|
|
73
|
+
fs.renameSync(src, dest);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
}
|
|
78
|
+
execSync(`cp -R "${src}" "${dest}"`);
|
|
79
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
execSync(`cp -R "${src}/." "${dest}/"`);
|
|
83
|
+
fs.rmSync(src, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
async function resolveBrand() {
|
|
86
|
+
let slug = getActiveBrand();
|
|
87
|
+
let name = null;
|
|
88
|
+
if (!slug) {
|
|
89
|
+
const res = await apiGet("/api/v2/whoami");
|
|
90
|
+
if (res.ok && res.data?.workspaceSlug) {
|
|
91
|
+
slug = res.data.workspaceSlug;
|
|
92
|
+
name = res.data.workspaceName ?? null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!slug) {
|
|
96
|
+
fail("could not determine which brand this install belongs to.\n" +
|
|
97
|
+
" Run `npx @aicopycoders/exodus brand use <slug>` first (or check your network/.env), then re-run `npx @aicopycoders/exodus migrate`.");
|
|
98
|
+
}
|
|
99
|
+
if (!name) {
|
|
100
|
+
try {
|
|
101
|
+
const res = await apiGetDashboard("/api/brands/mine", {
|
|
102
|
+
timeoutMs: 10_000,
|
|
103
|
+
});
|
|
104
|
+
if (res.ok) {
|
|
105
|
+
name = res.data?.brands?.find((b) => b.slug === slug)?.name ?? null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { slug, name: name ?? slug };
|
|
112
|
+
}
|
|
113
|
+
export async function run(flags) {
|
|
114
|
+
void flags;
|
|
115
|
+
const root = findWorkspaceRoot();
|
|
116
|
+
if (detectLayout(root) === "v2") {
|
|
117
|
+
console.log("✓ Already on the multi-brand layout — nothing to migrate.");
|
|
118
|
+
console.log(" Run `npx @aicopycoders/exodus@latest init` to sync brand folders.");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const brand = await resolveBrand();
|
|
122
|
+
const brandDir = brandDirFor(root, brand.slug);
|
|
123
|
+
const brandDirRel = path.relative(root, brandDir) || brandDir;
|
|
124
|
+
const conflicts = [
|
|
125
|
+
...findMergeConflicts(path.join(root, "state"), path.join(brandDir, "state")).map((f) => path.join("state", f)),
|
|
126
|
+
...findMergeConflicts(path.join(root, "output"), path.join(brandDir, "output")).map((f) => path.join("output", f)),
|
|
127
|
+
];
|
|
128
|
+
if (conflicts.length > 0) {
|
|
129
|
+
fail(`"${brandDirRel}/" already contains different versions of:\n` +
|
|
130
|
+
conflicts.map((f) => ` ${f}`).join("\n") +
|
|
131
|
+
"\n Reconcile or remove them, then re-run `npx @aicopycoders/exodus migrate`. Nothing was changed.");
|
|
132
|
+
}
|
|
133
|
+
console.log(`Migrating this install to the multi-brand layout...`);
|
|
134
|
+
console.log(` brand: ${brand.slug} (${brand.name})`);
|
|
135
|
+
const stateSrc = path.join(root, "state");
|
|
136
|
+
if (fs.existsSync(stateSrc)) {
|
|
137
|
+
const backupRoot = path.join(root, ".backup", `${ts()}-migrate`);
|
|
138
|
+
fs.mkdirSync(backupRoot, { recursive: true });
|
|
139
|
+
execSync(`cp -R "${stateSrc}" "${path.join(backupRoot, "state")}"`);
|
|
140
|
+
console.log(` backup: ${path.relative(root, backupRoot)}/state/`);
|
|
141
|
+
}
|
|
142
|
+
moveDir(path.join(root, "state"), path.join(brandDir, "state"));
|
|
143
|
+
moveDir(path.join(root, "output"), path.join(brandDir, "output"));
|
|
144
|
+
ensureBrandDir(root, brand);
|
|
145
|
+
setLayoutVersion(2);
|
|
146
|
+
console.log(`
|
|
147
|
+
✓ Migrated. New structure:
|
|
148
|
+
${brandDirRel}/${path.basename(BRAND_MARKER_FILE)} (brand marker)
|
|
149
|
+
${brandDirRel}/state/ ${brandDirRel}/output/
|
|
150
|
+
|
|
151
|
+
Shared files (.env, exodus/, .claude/skills/, references/) did not move.
|
|
152
|
+
|
|
153
|
+
Next steps:
|
|
154
|
+
• Run \`npx @aicopycoders/exodus@latest init\` to pull folders for your other brands.
|
|
155
|
+
• Run commands from inside ${brandDirRel}/ to target it automatically,
|
|
156
|
+
or keep using \`npx @aicopycoders/exodus brand use <slug>\` from anywhere.
|
|
157
|
+
`.trimEnd());
|
|
158
|
+
}
|