@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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { apiGet, apiPost } from "../lib/client.js";
|
|
3
|
+
import { formatError } from "../lib/format.js";
|
|
4
|
+
export const helpText = `
|
|
5
|
+
exodus segment — Segment map + personas (WHAT / WHO / WHY), agentic I/O
|
|
6
|
+
|
|
7
|
+
The JSON contract is the edit surface: export the map, edit the file, import it
|
|
8
|
+
back. Import is a FULL REPLACE, slug-stable by name-match — existing outcomes and
|
|
9
|
+
values keep their slugs, new names get new ones, and names absent from the file
|
|
10
|
+
are deleted. Personas the new map orphans are ARCHIVED (never deleted).
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
exodus segment show Human-readable summary of the map
|
|
14
|
+
exodus segment show --json Raw normalized map JSON
|
|
15
|
+
exodus segment export [--out <file>] Contract JSON (map + personas) to stdout or a file
|
|
16
|
+
exodus segment import <file> [--yes] Import a contract JSON file (dry-run first)
|
|
17
|
+
exodus segment personas List saved personas (incl. archived)
|
|
18
|
+
exodus segment personas --json Raw persona list JSON
|
|
19
|
+
|
|
20
|
+
Flags:
|
|
21
|
+
--json Machine-readable JSON output (show / personas)
|
|
22
|
+
--out <file> Write export to a file instead of stdout
|
|
23
|
+
--yes Apply a destructive import (deletes segments / archives personas)
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
• All operations scope to your active brand. Check with: exodus brand current
|
|
27
|
+
• 'import' ALWAYS previews via a server dry-run first and prints what would
|
|
28
|
+
change. If anything would be deleted at ANY layer (outcome, sub-slice,
|
|
29
|
+
demo/facet value) or a persona archived/deleted, it refuses to apply
|
|
30
|
+
without --yes and exits non-zero (never prompts — agents hang on prompts).
|
|
31
|
+
• 'import' on a workspace with no map yet just creates it.
|
|
32
|
+
• 'export' output imports back unchanged — it is the contract document.
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
exodus segment show
|
|
36
|
+
exodus segment export --out segments.json
|
|
37
|
+
exodus segment import segments.json
|
|
38
|
+
exodus segment import segments.json --yes
|
|
39
|
+
exodus segment personas --json
|
|
40
|
+
`.trim();
|
|
41
|
+
export function asSummary(data) {
|
|
42
|
+
const d = (typeof data === "object" && data !== null ? data : {});
|
|
43
|
+
const list = (v) => (Array.isArray(v) ? v : []);
|
|
44
|
+
return {
|
|
45
|
+
added: list(d.added),
|
|
46
|
+
keptRenamed: list(d.keptRenamed),
|
|
47
|
+
deleted: list(d.deleted),
|
|
48
|
+
subsDeleted: list(d.subsDeleted),
|
|
49
|
+
demoValuesDeleted: list(d.demoValuesDeleted),
|
|
50
|
+
facetValuesDeleted: list(d.facetValuesDeleted),
|
|
51
|
+
personasArchived: list(d.personasArchived),
|
|
52
|
+
personasDeleted: list(d.personasDeleted),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function isDestructive(summary) {
|
|
56
|
+
return (summary.deleted.length > 0 ||
|
|
57
|
+
summary.subsDeleted.length > 0 ||
|
|
58
|
+
summary.demoValuesDeleted.length > 0 ||
|
|
59
|
+
summary.facetValuesDeleted.length > 0 ||
|
|
60
|
+
summary.personasArchived.length > 0 ||
|
|
61
|
+
summary.personasDeleted.length > 0);
|
|
62
|
+
}
|
|
63
|
+
function line(label, names) {
|
|
64
|
+
const detail = names.length > 0 ? ` (${names.join(", ")})` : "";
|
|
65
|
+
return ` ${label.padEnd(24)} ${String(names.length).padStart(3)}${detail}`;
|
|
66
|
+
}
|
|
67
|
+
export function formatImportSummary(summary) {
|
|
68
|
+
return [
|
|
69
|
+
"Import preview:",
|
|
70
|
+
line("+ outcomes added", summary.added),
|
|
71
|
+
line("~ outcomes kept", summary.keptRenamed),
|
|
72
|
+
line("- outcomes deleted", summary.deleted),
|
|
73
|
+
line("- subs deleted", summary.subsDeleted),
|
|
74
|
+
line("- demo values deleted", summary.demoValuesDeleted),
|
|
75
|
+
line("- facet values deleted", summary.facetValuesDeleted),
|
|
76
|
+
line("personas archived", summary.personasArchived),
|
|
77
|
+
line("personas deleted", summary.personasDeleted),
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
export function formatMapSummary(map) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push(`Segment map — ${map.brandLabel} (product: ${map.productWord})`);
|
|
83
|
+
lines.push(`\nOutcomes (${map.outcomes.length}):`);
|
|
84
|
+
for (const o of map.outcomes) {
|
|
85
|
+
const lens = o.lens ? ` [${o.lens}]` : "";
|
|
86
|
+
const subs = o.subs.length > 0 ? ` · subs: ${o.subs.map((s) => s.name).join(", ")}` : "";
|
|
87
|
+
lines.push(` ${o.slug.padEnd(24)} ${o.name}${lens}${subs}`);
|
|
88
|
+
}
|
|
89
|
+
const groupBlock = (title, groups) => {
|
|
90
|
+
lines.push(`\n${title}:`);
|
|
91
|
+
for (const g of groups) {
|
|
92
|
+
const vals = g.values.length > 0 ? g.values.map((val) => val.name).join(", ") : "—";
|
|
93
|
+
lines.push(` ${g.name} (${g.values.length}): ${vals}`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
groupBlock("Demographics (WHO)", map.demoGroups);
|
|
97
|
+
groupBlock("Facets (WHY)", map.facetFamilies);
|
|
98
|
+
return lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
const IMPORT_PATH = "/api/v2/segments/import";
|
|
101
|
+
export async function importFlow(file, opts, deps) {
|
|
102
|
+
const calls = [];
|
|
103
|
+
let text;
|
|
104
|
+
try {
|
|
105
|
+
text = deps.readFile(file);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { code: 1, lines: [`Error: file not found: ${file}`], calls };
|
|
109
|
+
}
|
|
110
|
+
let contract;
|
|
111
|
+
try {
|
|
112
|
+
contract = JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
116
|
+
return { code: 1, lines: [`Error: ${file} is not valid JSON: ${msg}`], calls };
|
|
117
|
+
}
|
|
118
|
+
if (typeof contract !== "object" || contract === null || Array.isArray(contract)) {
|
|
119
|
+
return { code: 1, lines: [`Error: ${file} is not a segment map (expected a JSON object).`], calls };
|
|
120
|
+
}
|
|
121
|
+
const clean = { ...contract };
|
|
122
|
+
delete clean.dryRun;
|
|
123
|
+
calls.push({ dryRun: true });
|
|
124
|
+
const dry = await deps.post(IMPORT_PATH, { ...clean, dryRun: true });
|
|
125
|
+
if (!dry.ok) {
|
|
126
|
+
const lines = opts.json
|
|
127
|
+
? [JSON.stringify({ ok: false, status: dry.status, data: dry.data })]
|
|
128
|
+
: [formatError({ ok: dry.ok, status: dry.status, data: dry.data })];
|
|
129
|
+
return { code: 1, lines, calls };
|
|
130
|
+
}
|
|
131
|
+
const summary = asSummary(dry.data);
|
|
132
|
+
const destructive = isDestructive(summary);
|
|
133
|
+
if (destructive && !opts.yes) {
|
|
134
|
+
const lines = opts.json
|
|
135
|
+
? [JSON.stringify({ ok: false, applied: false, destructive: true, summary })]
|
|
136
|
+
: [
|
|
137
|
+
formatImportSummary(summary),
|
|
138
|
+
"",
|
|
139
|
+
"This import is destructive (deletes outcomes, sub-slices, or values,",
|
|
140
|
+
"and/or archives or deletes personas). Re-run with --yes to apply it.",
|
|
141
|
+
];
|
|
142
|
+
return { code: 1, lines, calls };
|
|
143
|
+
}
|
|
144
|
+
calls.push({ dryRun: false });
|
|
145
|
+
const real = await deps.post(IMPORT_PATH, clean);
|
|
146
|
+
if (!real.ok) {
|
|
147
|
+
const lines = opts.json
|
|
148
|
+
? [JSON.stringify({ ok: false, status: real.status, data: real.data })]
|
|
149
|
+
: [formatError({ ok: real.ok, status: real.status, data: real.data })];
|
|
150
|
+
return { code: 1, lines, calls };
|
|
151
|
+
}
|
|
152
|
+
const applied = asSummary(real.data);
|
|
153
|
+
const lines = opts.json
|
|
154
|
+
? [JSON.stringify({ ok: true, applied: true, destructive, summary: applied })]
|
|
155
|
+
: [formatImportSummary(applied), "", "Imported."];
|
|
156
|
+
return { code: 0, lines, calls };
|
|
157
|
+
}
|
|
158
|
+
export async function run(flags) {
|
|
159
|
+
const positional = parsePositional();
|
|
160
|
+
const [sub, ...rest] = positional;
|
|
161
|
+
if (!sub || sub === "show")
|
|
162
|
+
return runShow(flags);
|
|
163
|
+
if (sub === "export")
|
|
164
|
+
return runExport(flags);
|
|
165
|
+
if (sub === "import")
|
|
166
|
+
return runImport(rest, flags);
|
|
167
|
+
if (sub === "personas")
|
|
168
|
+
return runPersonas(flags);
|
|
169
|
+
console.error(`Unknown subcommand: "${sub}"\n`);
|
|
170
|
+
console.log(helpText);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
function parsePositional() {
|
|
174
|
+
const args = process.argv.slice(3);
|
|
175
|
+
const out = [];
|
|
176
|
+
let i = 0;
|
|
177
|
+
while (i < args.length) {
|
|
178
|
+
const a = args[i];
|
|
179
|
+
if (a.startsWith("--")) {
|
|
180
|
+
const next = args[i + 1];
|
|
181
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
182
|
+
i += 2;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
i++;
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
out.push(a);
|
|
190
|
+
i++;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
async function runShow(flags) {
|
|
195
|
+
const json = !!flags["json"];
|
|
196
|
+
const res = await apiGet("/api/v2/segments");
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
if (json)
|
|
199
|
+
console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
|
|
200
|
+
else
|
|
201
|
+
console.log(formatError(res));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
if (json) {
|
|
205
|
+
console.log(JSON.stringify(res.data.map));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!res.data.map) {
|
|
209
|
+
console.log("No segment map yet for the active brand.");
|
|
210
|
+
console.log("Build one and import it: exodus segment import <file>");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.log(formatMapSummary(res.data.map));
|
|
214
|
+
}
|
|
215
|
+
async function runExport(flags) {
|
|
216
|
+
const out = typeof flags["out"] === "string" ? flags["out"] : undefined;
|
|
217
|
+
const res = await apiGet("/api/v2/segments/export");
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
console.log(formatError(res));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const doc = JSON.stringify(res.data, null, 2);
|
|
223
|
+
if (out) {
|
|
224
|
+
fs.writeFileSync(out, doc + "\n", "utf-8");
|
|
225
|
+
console.log(`Wrote segment contract to ${out}.`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.log(doc);
|
|
229
|
+
}
|
|
230
|
+
async function runImport(positional, flags) {
|
|
231
|
+
const file = positional[0];
|
|
232
|
+
if (!file) {
|
|
233
|
+
console.error("Error: a contract JSON file is required");
|
|
234
|
+
console.log("Usage: exodus segment import <file> [--yes]");
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
const result = await importFlow(file, { yes: !!flags["yes"], json: !!flags["json"] }, {
|
|
238
|
+
post: (path, body) => apiPost(path, body),
|
|
239
|
+
readFile: (p) => fs.readFileSync(p, "utf-8"),
|
|
240
|
+
});
|
|
241
|
+
for (const l of result.lines)
|
|
242
|
+
console.log(l);
|
|
243
|
+
if (result.code !== 0)
|
|
244
|
+
process.exit(result.code);
|
|
245
|
+
}
|
|
246
|
+
async function runPersonas(flags) {
|
|
247
|
+
const json = !!flags["json"];
|
|
248
|
+
const res = await apiGet("/api/v2/segments/personas");
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
if (json)
|
|
251
|
+
console.log(JSON.stringify({ ok: false, status: res.status, data: res.data }));
|
|
252
|
+
else
|
|
253
|
+
console.log(formatError(res));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
const personas = res.data.personas ?? [];
|
|
257
|
+
if (json) {
|
|
258
|
+
console.log(JSON.stringify({ personas }));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (personas.length === 0) {
|
|
262
|
+
console.log("No personas saved yet for the active brand.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
console.log(`Personas (${personas.length}):`);
|
|
266
|
+
for (const p of personas) {
|
|
267
|
+
const tag = p.archived ? " [archived]" : "";
|
|
268
|
+
const code = p.code ? `${p.code} ` : "";
|
|
269
|
+
const type = p.type ? ` (${p.type})` : "";
|
|
270
|
+
console.log(` ${code}${p.name}${type}${tag}`);
|
|
271
|
+
if (p.description)
|
|
272
|
+
console.log(` ${p.description}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getVersion } from "../lib/version.js";
|
|
2
|
+
import { run as runInit } from "./init.js";
|
|
3
|
+
export const helpText = `
|
|
4
|
+
exodus update — refresh this workspace from the installed CLI (alias of init)
|
|
5
|
+
|
|
6
|
+
Re-runs the init refresh: rewrites .claude/skills/, the workspace docs
|
|
7
|
+
(CLAUDE.md, PIPELINES.md), and syncs brand folders from the package you
|
|
8
|
+
invoked. Your .env, state/, and outputs are left untouched.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
exodus update
|
|
12
|
+
|
|
13
|
+
Note: the CLI itself updates because npx resolves your channel fresh on each
|
|
14
|
+
run — so invoke this as \`npx @aicopycoders/exodus update\` (or @beta on the
|
|
15
|
+
beta channel), not from a stale local install.
|
|
16
|
+
`.trim();
|
|
17
|
+
export async function run(flags) {
|
|
18
|
+
console.log(`Exodus ${getVersion()} — refreshing workspace scaffold…`);
|
|
19
|
+
await runInit(flags);
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const helpText: string;
|
|
2
|
+
export interface LocalValidation {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
warnings: string[];
|
|
6
|
+
winnerCount: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function validatePackageLocally(pkg: unknown): LocalValidation;
|
|
9
|
+
export declare function contentTypeFor(filePath: string): string;
|
|
10
|
+
export declare function run(flags: Record<string, string | boolean>): Promise<void>;
|