@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.
@@ -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,2 @@
1
+ export declare const helpText: string;
2
+ export declare function run(flags: Record<string, string | boolean>): Promise<void>;
@@ -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>;