@series-a/cli 1.3.0 → 1.3.2
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/CHANGELOG.md +184 -51
- package/LICENSE +7 -0
- package/README.md +249 -115
- package/SKILLS.md +359 -0
- package/bin/advisoros.mjs +2 -0
- package/package.json +50 -15
- package/src/api.mjs +102 -0
- package/src/commands/auth.mjs +193 -0
- package/src/commands/brand.mjs +439 -0
- package/src/commands/email.mjs +130 -0
- package/src/commands/flow.mjs +175 -0
- package/src/commands/insights.mjs +1813 -0
- package/src/commands/knowledge-sync.mjs +519 -0
- package/src/commands/resources.mjs +2214 -0
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +212 -0
- package/src/config.mjs +82 -0
- package/src/index.mjs +79 -0
- package/src/ui.mjs +104 -0
- package/bin/setu.mjs +0 -797
- package/lib/api.mjs +0 -84
- package/lib/config.mjs +0 -42
- package/lib/ui.mjs +0 -68
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getProfile, setProfiles, listProfiles, deleteProfile, allProfiles, CONFIG_PATH, DEFAULTS } from "../config.mjs";
|
|
4
|
+
import { call } from "../api.mjs";
|
|
5
|
+
import { emit, fail, ctxFrom, withCommonOpts } from "../ui.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Replacing the "default" profile silently re-points every unqualified command
|
|
9
|
+
* at another workspace. Always confirm first; in a non-interactive shell the
|
|
10
|
+
* caller must pass --force.
|
|
11
|
+
*/
|
|
12
|
+
async function confirmDefaultReplacement(workspace, opts, currentWorkspace) {
|
|
13
|
+
if (opts.force) return true;
|
|
14
|
+
if (!currentWorkspace) return true;
|
|
15
|
+
if (!process.stdin.isTTY) {
|
|
16
|
+
process.stdout.write(chalk.yellow(
|
|
17
|
+
`\n! Default profile currently points at workspace ${currentWorkspace}.\n` +
|
|
18
|
+
` Not replacing it — re-run with --force to move it to ${workspace}.\n`,
|
|
19
|
+
));
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const r = await prompts({
|
|
23
|
+
type: "confirm",
|
|
24
|
+
name: "v",
|
|
25
|
+
message: `Replace the default profile (workspace ${currentWorkspace}) with ${workspace}?`,
|
|
26
|
+
initial: false,
|
|
27
|
+
});
|
|
28
|
+
return !!r.v;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerAuth(program) {
|
|
32
|
+
program
|
|
33
|
+
.command("login")
|
|
34
|
+
.description("Save an API key + workspace to ~/.advisoros/config.json")
|
|
35
|
+
.option("-p, --profile <name>", "profile name (defaults to the workspace id)")
|
|
36
|
+
.option("--set-default", "also point the \"default\" profile at this workspace", false)
|
|
37
|
+
.option("--force", "replace an existing default profile without asking", false)
|
|
38
|
+
.option("--api-key <key>", "API key (otherwise prompted)")
|
|
39
|
+
.option("--workspace <id>", "workspace id (otherwise prompted, unless --admin)")
|
|
40
|
+
.option("--admin", "admin key login — verify cross-workspace, no workspace required", false)
|
|
41
|
+
.option("--base-url <url>", "agent-api base URL", DEFAULTS.baseUrl)
|
|
42
|
+
.option("--as-agent <name>", "caller identity to send as agent_name", "advisoros-cli")
|
|
43
|
+
.option("--email <email>", "your email — recorded on the audit trail for every action")
|
|
44
|
+
.action(async (opts) => {
|
|
45
|
+
try {
|
|
46
|
+
let { apiKey, workspace, baseUrl, profile } = opts;
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
const r = await prompts({
|
|
49
|
+
type: "password",
|
|
50
|
+
name: "v",
|
|
51
|
+
message: "API key (from Settings → API Keys; advos_admin_… for admin)",
|
|
52
|
+
validate: (v) => v && v.length > 10 ? true : "Required",
|
|
53
|
+
});
|
|
54
|
+
apiKey = r.v;
|
|
55
|
+
}
|
|
56
|
+
if (!apiKey) return fail(new Error("Aborted"));
|
|
57
|
+
|
|
58
|
+
const isAdmin = opts.admin || apiKey.startsWith("advos_admin_");
|
|
59
|
+
|
|
60
|
+
if (!isAdmin && !workspace) {
|
|
61
|
+
const r = await prompts({
|
|
62
|
+
type: "text",
|
|
63
|
+
name: "v",
|
|
64
|
+
message: "Workspace ID (6-char slug, e.g. dmw9wj)",
|
|
65
|
+
validate: (v) => v && v.length >= 3 ? true : "Required",
|
|
66
|
+
});
|
|
67
|
+
workspace = r.v;
|
|
68
|
+
}
|
|
69
|
+
if (!isAdmin && !workspace) return fail(new Error("Aborted"));
|
|
70
|
+
|
|
71
|
+
const agentName = opts.asAgent || "advisoros-cli";
|
|
72
|
+
|
|
73
|
+
if (isAdmin) {
|
|
74
|
+
// Verify the admin key cross-workspace; no workspace slug needed.
|
|
75
|
+
const verify = await call("workspaces", "list", { limit: 1000 }, {
|
|
76
|
+
apiKey, baseUrl, agentName,
|
|
77
|
+
});
|
|
78
|
+
const count = verify?.pagination?.total ?? verify?.workspaces?.length ?? 0;
|
|
79
|
+
const adminData = { apiKey, baseUrl, admin: true, agentName, ...(opts.email ? { email: opts.email } : {}), ...(workspace ? { workspaceId: workspace } : {}) };
|
|
80
|
+
const adminName = profile || "admin";
|
|
81
|
+
const adminWrites = { [adminName]: adminData };
|
|
82
|
+
if (adminName !== "default" && (opts.setDefault || !(await getProfile("default")))) {
|
|
83
|
+
const okDefault = await confirmDefaultReplacement(workspace || "(admin)", opts);
|
|
84
|
+
if (okDefault) adminWrites.default = adminData;
|
|
85
|
+
}
|
|
86
|
+
await setProfiles(adminWrites, adminName);
|
|
87
|
+
profile = adminName;
|
|
88
|
+
process.stdout.write(chalk.green(
|
|
89
|
+
`\n✓ Logged in as admin profile "${profile}" — ${count} workspaces reachable\n` +
|
|
90
|
+
` Config: ${CONFIG_PATH}\n` +
|
|
91
|
+
` Tip: pass --workspace <slug> per command, or set ADVISOROS_WORKSPACE_ID.\n`,
|
|
92
|
+
));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Verify the credentials with a cheap round-trip.
|
|
97
|
+
const verify = await call("agents", "list", {}, {
|
|
98
|
+
apiKey, workspaceId: workspace, baseUrl, agentName,
|
|
99
|
+
});
|
|
100
|
+
const count = verify?.agents?.length ?? 0;
|
|
101
|
+
// Named workspace profiles: without -p, the profile is named after the
|
|
102
|
+
// workspace so a second login can never silently overwrite the first.
|
|
103
|
+
const profileName = profile || workspace;
|
|
104
|
+
const data = { apiKey, workspaceId: workspace, baseUrl, agentName, ...(opts.email ? { email: opts.email } : {}) };
|
|
105
|
+
const writes = { [profileName]: data };
|
|
106
|
+
|
|
107
|
+
let defaultNote = "";
|
|
108
|
+
if (profileName !== "default") {
|
|
109
|
+
const existingDefault = await getProfile("default");
|
|
110
|
+
if (!existingDefault) {
|
|
111
|
+
writes.default = data;
|
|
112
|
+
defaultNote = ` Default profile set to workspace ${workspace}.\n`;
|
|
113
|
+
} else if (existingDefault.workspaceId === workspace) {
|
|
114
|
+
writes.default = data;
|
|
115
|
+
} else if (opts.setDefault || opts.force) {
|
|
116
|
+
const ok = await confirmDefaultReplacement(workspace, opts, existingDefault.workspaceId);
|
|
117
|
+
if (ok) {
|
|
118
|
+
writes.default = data;
|
|
119
|
+
defaultNote = ` Default profile moved from ${existingDefault.workspaceId} to ${workspace}.\n`;
|
|
120
|
+
} else {
|
|
121
|
+
defaultNote = ` Default profile left on workspace ${existingDefault.workspaceId}.\n`;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
defaultNote =
|
|
125
|
+
` Default profile still points at workspace ${existingDefault.workspaceId} — unchanged.\n` +
|
|
126
|
+
` Use this workspace with: --profile ${profileName} (or re-run login with --set-default)\n`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await setProfiles(writes, profileName);
|
|
131
|
+
process.stdout.write(chalk.green(
|
|
132
|
+
`\n✓ Logged in as profile "${profileName}" — workspace ${workspace} (${count} agents visible)\n` +
|
|
133
|
+
` Config: ${CONFIG_PATH}\n`,
|
|
134
|
+
) + chalk.gray(defaultNote));
|
|
135
|
+
} catch (e) { fail(e); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command("whoami")
|
|
140
|
+
.description("Show the active profile and workspace")
|
|
141
|
+
.option("-p, --profile <name>", "profile name", "default")
|
|
142
|
+
.action(async (opts) => {
|
|
143
|
+
try {
|
|
144
|
+
const p = await getProfile(opts.profile);
|
|
145
|
+
if (!p) return fail(new Error(`Profile "${opts.profile}" not found. Run \`advisoros login\`.`));
|
|
146
|
+
const masked = p.apiKey ? p.apiKey.slice(0, 8) + "…" + p.apiKey.slice(-4) : "(none)";
|
|
147
|
+
const isAdmin = Boolean(p.admin) || String(p.apiKey || "").startsWith("advos_admin_");
|
|
148
|
+
|
|
149
|
+
let reachable = null;
|
|
150
|
+
if (isAdmin) {
|
|
151
|
+
try {
|
|
152
|
+
const r = await call("workspaces", "list", { limit: 1000 }, {
|
|
153
|
+
apiKey: p.apiKey, baseUrl: p.baseUrl, agentName: p.agentName,
|
|
154
|
+
});
|
|
155
|
+
reachable = r?.pagination?.total ?? r?.workspaces?.length ?? null;
|
|
156
|
+
} catch { reachable = null; }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
`profile: ${opts.profile}\n` +
|
|
161
|
+
`scope: ${isAdmin ? "admin (cross-workspace)" : "workspace"}\n` +
|
|
162
|
+
`workspace: ${p.workspaceId || "(none — pass --workspace per command)"}\n` +
|
|
163
|
+
(isAdmin ? `reachable: ${reachable == null ? "(could not verify)" : `${reachable} workspaces`}\n` : "") +
|
|
164
|
+
`agent: ${p.agentName || "advisoros-cli"}\n` +
|
|
165
|
+
`base url: ${p.baseUrl || DEFAULTS.baseUrl}\n` +
|
|
166
|
+
`api key: ${masked}\n`,
|
|
167
|
+
);
|
|
168
|
+
} catch (e) { fail(e); }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
program
|
|
172
|
+
.command("profiles")
|
|
173
|
+
.description("List saved profiles")
|
|
174
|
+
.action(async () => {
|
|
175
|
+
const profiles = await allProfiles();
|
|
176
|
+
const names = Object.keys(profiles);
|
|
177
|
+
if (!names.length) return process.stdout.write("(no profiles — run `advisoros login`)\n");
|
|
178
|
+
for (const name of names) {
|
|
179
|
+
const p = profiles[name];
|
|
180
|
+
const scope = p?.admin ? "admin" : (p?.workspaceId || "(no workspace)");
|
|
181
|
+
process.stdout.write(`- ${name} → ${scope}\n`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
program
|
|
186
|
+
.command("logout")
|
|
187
|
+
.description("Remove a saved profile")
|
|
188
|
+
.option("-p, --profile <name>", "profile name", "default")
|
|
189
|
+
.action(async (opts) => {
|
|
190
|
+
const ok = await deleteProfile(opts.profile);
|
|
191
|
+
process.stdout.write(ok ? chalk.green(`✓ Removed profile "${opts.profile}"\n`) : `(no profile "${opts.profile}")\n`);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { call } from "../api.mjs";
|
|
2
|
+
import { emit, fail, ctxFrom, withCommonOpts, confirmDestructive } from "../ui.mjs";
|
|
3
|
+
|
|
4
|
+
const list = (v) => (v ? String(v).split(",").map((s) => s.trim()).filter(Boolean) : undefined);
|
|
5
|
+
|
|
6
|
+
function collectFields(o) {
|
|
7
|
+
const body = {};
|
|
8
|
+
const text = {
|
|
9
|
+
primary: "primary_color", secondary: "secondary_color", accent: "accent_color",
|
|
10
|
+
ctaColor: "cta_color", ctaTextColor: "cta_text_color",
|
|
11
|
+
fontHeading: "font_heading", fontBody: "font_body", tone: "tone",
|
|
12
|
+
tagline: "tagline", notes: "notes",
|
|
13
|
+
logo: "logo_url", logoFull: "logo_full_url", logoFullDark: "logo_full_dark_url",
|
|
14
|
+
logoIconLight: "logo_icon_light_url", logoIconDark: "logo_icon_dark_url",
|
|
15
|
+
videoCtaType: "video_default_cta_type", videoCtaLabel: "video_default_cta_label",
|
|
16
|
+
videoCtaUrl: "video_default_cta_url", videoIntro: "video_intro_text",
|
|
17
|
+
videoShareDomain: "video_share_domain",
|
|
18
|
+
};
|
|
19
|
+
for (const [opt, field] of Object.entries(text)) if (o[opt] !== undefined) body[field] = o[opt];
|
|
20
|
+
if (o.extraColors !== undefined) body.extra_colors = list(o.extraColors) ?? [];
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printBrand(b) {
|
|
25
|
+
if (!b) return process.stdout.write("No brand record for this workspace yet.\n");
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
`Colours: primary ${b.primary_color || "-"} secondary ${b.secondary_color || "-"} accent ${b.accent_color || "-"}\n` +
|
|
28
|
+
`CTA: ${b.cta_color || "-"} on ${b.cta_text_color || "-"}\n` +
|
|
29
|
+
`Fonts: heading ${b.font_heading || "-"} body ${b.font_body || "-"}\n` +
|
|
30
|
+
`Tone: ${b.tone || "-"}\n` +
|
|
31
|
+
`Tagline: ${b.tagline || "-"}\n` +
|
|
32
|
+
`Logos: ${[b.logo_url, b.logo_full_url, b.logo_full_dark_url, b.logo_icon_light_url, b.logo_icon_dark_url].filter(Boolean).length} set\n` +
|
|
33
|
+
`Imagery: style ${b.imagery_style || "-"} | mood ${b.imagery_mood || "-"}\n` +
|
|
34
|
+
` motifs: ${(b.imagery_motifs || []).join(", ") || "-"}\n` +
|
|
35
|
+
` avoid: ${b.imagery_avoid || "-"}\n` +
|
|
36
|
+
` refs: ${(b.imagery_reference_urls || []).join(", ") || "-"}\n` +
|
|
37
|
+
`Voice: ${b.tone_of_voice_directive ? `${String(b.tone_of_voice_directive).slice(0, 120)}${String(b.tone_of_voice_directive).length > 120 ? "…" : ""}` : "-"}\n` +
|
|
38
|
+
` source: ${b.tone_of_voice_source || "-"}${b.tone_of_voice_synced_at ? ` (synced ${b.tone_of_voice_synced_at})` : ""}\n` +
|
|
39
|
+
`Video: cta ${b.video_default_cta_type || "-"}${b.video_default_cta_label ? ` "${b.video_default_cta_label}"` : ""} | domain ${b.video_share_domain || "-"}\n` +
|
|
40
|
+
`Social: ${Object.entries(b.reference_library || {}).filter(([k, v]) => k.startsWith("social_") && v).map(([k]) => k.replace(/^social_|_url$/g, "")).join(", ") || "-"}\n` +
|
|
41
|
+
(b.notes ? `Notes: ${b.notes}\n` : ""),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printImagery(i) {
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
`Style: ${i.imagery_style || "-"}\nMood: ${i.imagery_mood || "-"}\n` +
|
|
48
|
+
`Motifs: ${(i.imagery_motifs || []).join(", ") || "-"}\n` +
|
|
49
|
+
`Avoid: ${i.imagery_avoid || "-"}\nRefs: ${(i.imagery_reference_urls || []).join(", ") || "-"}\n`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerBrand(program) {
|
|
54
|
+
const b = program
|
|
55
|
+
.command("brand")
|
|
56
|
+
.description("Read and edit the workspace brand: colours, fonts, logos, tagline and imagery direction");
|
|
57
|
+
|
|
58
|
+
withCommonOpts(b.command("show"))
|
|
59
|
+
.alias("get")
|
|
60
|
+
.description("Show the workspace brand record")
|
|
61
|
+
.action(async function () {
|
|
62
|
+
try {
|
|
63
|
+
const r = await call("brand", "get", {}, ctxFrom(this));
|
|
64
|
+
emit(this, r, (d) => printBrand(d.brand));
|
|
65
|
+
} catch (e) { fail(e); }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
withCommonOpts(b.command("set"))
|
|
69
|
+
.alias("update")
|
|
70
|
+
.description("Update any brand field (only the flags you pass are changed)")
|
|
71
|
+
.option("--primary <hex>", "primary colour")
|
|
72
|
+
.option("--secondary <hex>", "secondary colour")
|
|
73
|
+
.option("--accent <hex>", "accent colour")
|
|
74
|
+
.option("--extra-colors <list>", "comma-separated extra colours")
|
|
75
|
+
.option("--cta-color <hex>", "CTA background colour")
|
|
76
|
+
.option("--cta-text-color <hex>", "CTA text colour")
|
|
77
|
+
.option("--font-heading <name>", "heading font")
|
|
78
|
+
.option("--font-body <name>", "body font")
|
|
79
|
+
.option("--tone <tone>", "brand tone keyword")
|
|
80
|
+
.option("--tagline <text>", "tagline")
|
|
81
|
+
.option("--notes <text>", "brand notes")
|
|
82
|
+
.option("--logo <url>", "primary logo URL")
|
|
83
|
+
.option("--logo-full <url>", "full logo URL")
|
|
84
|
+
.option("--logo-full-dark <url>", "full logo (dark) URL")
|
|
85
|
+
.option("--logo-icon-light <url>", "icon logo (light) URL")
|
|
86
|
+
.option("--logo-icon-dark <url>", "icon logo (dark) URL")
|
|
87
|
+
.option("--video-cta-type <type>", "default video CTA type")
|
|
88
|
+
.option("--video-cta-label <text>", "default video CTA label")
|
|
89
|
+
.option("--video-cta-url <url>", "default video CTA URL")
|
|
90
|
+
.option("--video-intro <text>", "default video intro text")
|
|
91
|
+
.option("--video-share-domain <domain>", "video share domain")
|
|
92
|
+
.action(async function (o) {
|
|
93
|
+
try {
|
|
94
|
+
const body = collectFields(o);
|
|
95
|
+
if (!Object.keys(body).length) fail(new Error("Pass at least one field to update."));
|
|
96
|
+
const r = await call("brand", "update", body, ctxFrom(this));
|
|
97
|
+
emit(this, r, (d) => { process.stdout.write(`Updated: ${(d.updated || []).join(", ")}\n\n`); printBrand(d.brand); });
|
|
98
|
+
} catch (e) { fail(e); }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── imagery direction ──────────────────────────────────────────────────
|
|
102
|
+
const img = b.command("imagery").description("Manage the imagery direction used for every generated image");
|
|
103
|
+
|
|
104
|
+
withCommonOpts(img.command("show"))
|
|
105
|
+
.alias("get")
|
|
106
|
+
.description("Show the imagery direction")
|
|
107
|
+
.action(async function () {
|
|
108
|
+
try {
|
|
109
|
+
const r = await call("brand", "imagery", {}, ctxFrom(this));
|
|
110
|
+
emit(this, r, (d) => printImagery(d.imagery || {}));
|
|
111
|
+
} catch (e) { fail(e); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
withCommonOpts(img.command("set"))
|
|
115
|
+
.alias("update")
|
|
116
|
+
.description("Set imagery style, motifs, mood, avoid list or reference URLs")
|
|
117
|
+
.option("--style <text>", "imagery style")
|
|
118
|
+
.option("--mood <text>", "imagery mood")
|
|
119
|
+
.option("--avoid <text>", "what to avoid")
|
|
120
|
+
.option("--motifs <list>", "comma-separated motifs (replaces the list)")
|
|
121
|
+
.option("--refs <list>", "comma-separated reference URLs (replaces the list)")
|
|
122
|
+
.action(async function (o) {
|
|
123
|
+
try {
|
|
124
|
+
const body = {};
|
|
125
|
+
if (o.style !== undefined) body.imagery_style = o.style;
|
|
126
|
+
if (o.mood !== undefined) body.imagery_mood = o.mood;
|
|
127
|
+
if (o.avoid !== undefined) body.imagery_avoid = o.avoid;
|
|
128
|
+
if (o.motifs !== undefined) body.imagery_motifs = list(o.motifs) ?? [];
|
|
129
|
+
if (o.refs !== undefined) body.imagery_reference_urls = list(o.refs) ?? [];
|
|
130
|
+
if (!Object.keys(body).length) fail(new Error("Pass at least one imagery field."));
|
|
131
|
+
const r = await call("brand", "set_imagery", body, ctxFrom(this));
|
|
132
|
+
emit(this, r, (d) => printImagery(d.imagery || {}));
|
|
133
|
+
} catch (e) { fail(e); }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
withCommonOpts(img.command("add-motifs <motifs>"))
|
|
137
|
+
.description("Add motifs (comma-separated) without replacing existing ones")
|
|
138
|
+
.action(async function (m) {
|
|
139
|
+
try {
|
|
140
|
+
const r = await call("brand", "add_motifs", { motifs: list(m) }, ctxFrom(this));
|
|
141
|
+
emit(this, r, (d) => process.stdout.write(`Motifs: ${(d.imagery_motifs || []).join(", ") || "-"}\n`));
|
|
142
|
+
} catch (e) { fail(e); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
withCommonOpts(img.command("remove-motifs <motifs>"))
|
|
146
|
+
.description("Remove motifs (comma-separated)")
|
|
147
|
+
.action(async function (m) {
|
|
148
|
+
try {
|
|
149
|
+
const r = await call("brand", "remove_motifs", { motifs: list(m) }, ctxFrom(this));
|
|
150
|
+
emit(this, r, (d) => process.stdout.write(`Motifs: ${(d.imagery_motifs || []).join(", ") || "-"}\n`));
|
|
151
|
+
} catch (e) { fail(e); }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
withCommonOpts(img.command("add-refs <urls>"))
|
|
155
|
+
.description("Add reference URLs (comma-separated)")
|
|
156
|
+
.action(async function (u) {
|
|
157
|
+
try {
|
|
158
|
+
const r = await call("brand", "add_reference_urls", { urls: list(u) }, ctxFrom(this));
|
|
159
|
+
emit(this, r, (d) => process.stdout.write(`Refs: ${(d.imagery_reference_urls || []).join(", ") || "-"}\n`));
|
|
160
|
+
} catch (e) { fail(e); }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
withCommonOpts(img.command("remove-refs <urls>"))
|
|
164
|
+
.description("Remove reference URLs (comma-separated)")
|
|
165
|
+
.action(async function (u) {
|
|
166
|
+
try {
|
|
167
|
+
const r = await call("brand", "remove_reference_urls", { urls: list(u) }, ctxFrom(this));
|
|
168
|
+
emit(this, r, (d) => process.stdout.write(`Refs: ${(d.imagery_reference_urls || []).join(", ") || "-"}\n`));
|
|
169
|
+
} catch (e) { fail(e); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
withCommonOpts(img.command("clear"))
|
|
173
|
+
.description("Clear the imagery direction (or only --fields style,motifs...)")
|
|
174
|
+
.option("--fields <list>", "comma-separated imagery fields to clear")
|
|
175
|
+
.action(async function (o) {
|
|
176
|
+
try {
|
|
177
|
+
await confirmDestructive(this, "Clear the workspace imagery direction.");
|
|
178
|
+
const fields = list(o.fields)?.map((f) => (f.startsWith("imagery_") ? f : `imagery_${f}`));
|
|
179
|
+
const r = await call("brand", "clear_imagery", fields ? { fields } : {}, ctxFrom(this));
|
|
180
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
181
|
+
} catch (e) { fail(e); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── logos ──────────────────────────────────────────────────────────────
|
|
185
|
+
const printLogos = (l = {}) =>
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
`Primary: ${l.logo_url || "-"}\nFull: ${l.logo_full_url || "-"}\n` +
|
|
188
|
+
`Full dark: ${l.logo_full_dark_url || "-"}\nIcon light: ${l.logo_icon_light_url || "-"}\n` +
|
|
189
|
+
`Icon dark: ${l.logo_icon_dark_url || "-"}\n`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const lg = b.command("logos").alias("logo").description("Manage the workspace logo files and URLs");
|
|
193
|
+
|
|
194
|
+
withCommonOpts(lg.command("show"))
|
|
195
|
+
.alias("get")
|
|
196
|
+
.description("Show every logo variant")
|
|
197
|
+
.action(async function () {
|
|
198
|
+
try {
|
|
199
|
+
const r = await call("brand", "logos", {}, ctxFrom(this));
|
|
200
|
+
emit(this, r, (d) => printLogos(d.logos || {}));
|
|
201
|
+
} catch (e) { fail(e); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
withCommonOpts(lg.command("set <variant> <url>"))
|
|
205
|
+
.description("Point a logo variant (full, full_dark, icon_light, icon_dark, primary) at a URL")
|
|
206
|
+
.action(async function (variant, url) {
|
|
207
|
+
try {
|
|
208
|
+
const r = await call("brand", "set_logos", { variant, url }, ctxFrom(this));
|
|
209
|
+
emit(this, r, (d) => printLogos(d.logos || {}));
|
|
210
|
+
} catch (e) { fail(e); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
withCommonOpts(lg.command("upload <file>"))
|
|
214
|
+
.description("Upload a logo image file and store it against a variant")
|
|
215
|
+
.option("--variant <variant>", "full, full_dark, icon_light, icon_dark or primary", "full")
|
|
216
|
+
.action(async function (file, o) {
|
|
217
|
+
try {
|
|
218
|
+
const { readFile } = await import("node:fs/promises");
|
|
219
|
+
const path = await import("node:path");
|
|
220
|
+
const buf = await readFile(file);
|
|
221
|
+
if (buf.byteLength > 5 * 1024 * 1024) fail(new Error("Logo must be under 5 MB."));
|
|
222
|
+
const body = {
|
|
223
|
+
variant: o.variant,
|
|
224
|
+
filename: path.basename(file),
|
|
225
|
+
data: buf.toString("base64"),
|
|
226
|
+
};
|
|
227
|
+
const r = await call("brand", "upload_logo", body, ctxFrom(this));
|
|
228
|
+
emit(this, r, (d) => {
|
|
229
|
+
process.stdout.write(`Uploaded ${d.uploaded?.variant}: ${d.uploaded?.url}\n\n`);
|
|
230
|
+
printLogos(d.logos || {});
|
|
231
|
+
});
|
|
232
|
+
} catch (e) { fail(e); }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
withCommonOpts(lg.command("clear"))
|
|
236
|
+
.description("Remove all logos, or only --variants full,icon_dark")
|
|
237
|
+
.option("--variants <list>", "comma-separated variants to clear")
|
|
238
|
+
.action(async function (o) {
|
|
239
|
+
try {
|
|
240
|
+
await confirmDestructive(this, "Clear the workspace logos.");
|
|
241
|
+
const variants = list(o.variants);
|
|
242
|
+
const r = await call("brand", "clear_logos", variants ? { variants } : {}, ctxFrom(this));
|
|
243
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
244
|
+
} catch (e) { fail(e); }
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ── social links ───────────────────────────────────────────────────────
|
|
248
|
+
const NETWORKS = ["linkedin", "twitter", "facebook", "instagram", "youtube", "tiktok", "threads", "github"];
|
|
249
|
+
const printSocial = (s = {}) =>
|
|
250
|
+
process.stdout.write(NETWORKS.map((n) => `${(n + ":").padEnd(11)}${s[n] || "-"}`).join("\n") + "\n");
|
|
251
|
+
|
|
252
|
+
const so = b.command("social").description("Manage the brand's social profile links");
|
|
253
|
+
|
|
254
|
+
withCommonOpts(so.command("show"))
|
|
255
|
+
.alias("get")
|
|
256
|
+
.description("Show the social profile links")
|
|
257
|
+
.action(async function () {
|
|
258
|
+
try {
|
|
259
|
+
const r = await call("brand", "social", {}, ctxFrom(this));
|
|
260
|
+
emit(this, r, (d) => printSocial(d.social || {}));
|
|
261
|
+
} catch (e) { fail(e); }
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const soSet = withCommonOpts(so.command("set"))
|
|
265
|
+
.alias("update")
|
|
266
|
+
.description("Set one or more social profile links");
|
|
267
|
+
for (const n of NETWORKS) soSet.option(`--${n} <url>`, `${n} profile URL`);
|
|
268
|
+
soSet.action(async function (o) {
|
|
269
|
+
try {
|
|
270
|
+
const body = {};
|
|
271
|
+
for (const n of NETWORKS) if (o[n] !== undefined) body[n] = o[n];
|
|
272
|
+
if (!Object.keys(body).length) fail(new Error(`Pass at least one of: ${NETWORKS.map((n) => `--${n}`).join(", ")}.`));
|
|
273
|
+
const r = await call("brand", "set_social", body, ctxFrom(this));
|
|
274
|
+
emit(this, r, (d) => printSocial(d.social || {}));
|
|
275
|
+
} catch (e) { fail(e); }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
withCommonOpts(so.command("clear"))
|
|
279
|
+
.description("Remove all social links, or only --networks linkedin,github")
|
|
280
|
+
.option("--networks <list>", "comma-separated networks to clear")
|
|
281
|
+
.action(async function (o) {
|
|
282
|
+
try {
|
|
283
|
+
await confirmDestructive(this, "Clear the workspace social links.");
|
|
284
|
+
const networks = list(o.networks);
|
|
285
|
+
const r = await call("brand", "clear_social", networks ? { networks } : {}, ctxFrom(this));
|
|
286
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
287
|
+
} catch (e) { fail(e); }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ── video share branding ───────────────────────────────────────────────
|
|
291
|
+
const printVideo = (v = {}) =>
|
|
292
|
+
process.stdout.write(
|
|
293
|
+
`CTA type: ${v.video_default_cta_type || "-"}\nCTA label: ${v.video_default_cta_label || "-"}\n` +
|
|
294
|
+
`CTA URL: ${v.video_default_cta_url || "-"}\nIntro text: ${v.video_intro_text || "-"}\n` +
|
|
295
|
+
`Share domain: ${v.video_share_domain || "-"}\n`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const vd = b.command("video").description("Manage the branded video share page defaults");
|
|
299
|
+
|
|
300
|
+
withCommonOpts(vd.command("show"))
|
|
301
|
+
.alias("get")
|
|
302
|
+
.description("Show the video share branding defaults")
|
|
303
|
+
.action(async function () {
|
|
304
|
+
try {
|
|
305
|
+
const r = await call("brand", "video", {}, ctxFrom(this));
|
|
306
|
+
emit(this, r, (d) => printVideo(d.video || {}));
|
|
307
|
+
} catch (e) { fail(e); }
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
withCommonOpts(vd.command("set"))
|
|
311
|
+
.alias("update")
|
|
312
|
+
.description("Set the default video CTA, intro text or share domain")
|
|
313
|
+
.option("--cta-type <type>", "default CTA type (e.g. booking, website, none)")
|
|
314
|
+
.option("--cta-label <text>", "button label")
|
|
315
|
+
.option("--cta-url <url>", "CTA destination URL")
|
|
316
|
+
.option("--intro <text>", "intro text shown on the share page")
|
|
317
|
+
.option("--share-domain <domain>", "custom share domain")
|
|
318
|
+
.action(async function (o) {
|
|
319
|
+
try {
|
|
320
|
+
const body = {};
|
|
321
|
+
if (o.ctaType !== undefined) body.video_default_cta_type = o.ctaType;
|
|
322
|
+
if (o.ctaLabel !== undefined) body.video_default_cta_label = o.ctaLabel;
|
|
323
|
+
if (o.ctaUrl !== undefined) body.video_default_cta_url = o.ctaUrl;
|
|
324
|
+
if (o.intro !== undefined) body.video_intro_text = o.intro;
|
|
325
|
+
if (o.shareDomain !== undefined) body.video_share_domain = o.shareDomain;
|
|
326
|
+
if (!Object.keys(body).length) fail(new Error("Pass at least one video field."));
|
|
327
|
+
const r = await call("brand", "set_video", body, ctxFrom(this));
|
|
328
|
+
emit(this, r, (d) => printVideo(d.video || {}));
|
|
329
|
+
} catch (e) { fail(e); }
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
withCommonOpts(vd.command("clear"))
|
|
333
|
+
.description("Clear the video share defaults (or only --fields cta_url,intro_text)")
|
|
334
|
+
.option("--fields <list>", "comma-separated video fields to clear")
|
|
335
|
+
.action(async function (o) {
|
|
336
|
+
try {
|
|
337
|
+
await confirmDestructive(this, "Clear the video share branding defaults.");
|
|
338
|
+
const fields = list(o.fields)?.map((f) => (f.startsWith("video_") ? f : `video_${f}`));
|
|
339
|
+
const r = await call("brand", "clear_video", fields ? { fields } : {}, ctxFrom(this));
|
|
340
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
341
|
+
} catch (e) { fail(e); }
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ── tone of voice ──────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
const tv = b.command("tone").description("Manage the tone of voice directive used when writing copy");
|
|
347
|
+
|
|
348
|
+
const printTone = (t = {}) =>
|
|
349
|
+
process.stdout.write(
|
|
350
|
+
`Keyword: ${t.tone || "-"}\nDirective: ${t.directive || "-"}\n` +
|
|
351
|
+
`Source: ${t.source || "-"}\nSynced: ${t.synced_at || "-"}\n` +
|
|
352
|
+
(t.apply_by_default === undefined ? "" : `Applied by default: ${t.apply_by_default ? "yes" : "no"}\n`),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
withCommonOpts(tv.command("show"))
|
|
356
|
+
.alias("get")
|
|
357
|
+
.description("Show the tone of voice directive")
|
|
358
|
+
.action(async function () {
|
|
359
|
+
try {
|
|
360
|
+
const r = await call("brand", "tone", {}, ctxFrom(this));
|
|
361
|
+
emit(this, r, (d) => printTone(d.tone || {}));
|
|
362
|
+
} catch (e) { fail(e); }
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
withCommonOpts(tv.command("set"))
|
|
366
|
+
.alias("update")
|
|
367
|
+
.description("Set the tone of voice directive (and optional keyword/source)")
|
|
368
|
+
.option("--directive <text>", "the tone of voice directive")
|
|
369
|
+
.option("--file <path>", "read the directive from a file")
|
|
370
|
+
.option("--source <name>", "where it came from (default: manual)")
|
|
371
|
+
.option("--keyword <tone>", "short brand tone keyword")
|
|
372
|
+
.action(async function (o) {
|
|
373
|
+
try {
|
|
374
|
+
const body = {};
|
|
375
|
+
if (o.file) body.directive = await (await import("node:fs/promises")).readFile(o.file, "utf8");
|
|
376
|
+
if (o.directive !== undefined) body.directive = o.directive;
|
|
377
|
+
if (o.source !== undefined) body.source = o.source;
|
|
378
|
+
if (o.keyword !== undefined) body.tone = o.keyword;
|
|
379
|
+
if (!Object.keys(body).length) fail(new Error("Pass --directive, --file or --keyword."));
|
|
380
|
+
const r = await call("brand", "set_tone", body, ctxFrom(this));
|
|
381
|
+
emit(this, r, (d) => printTone(d.tone || {}));
|
|
382
|
+
} catch (e) { fail(e); }
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
withCommonOpts(tv.command("clear"))
|
|
386
|
+
.description("Remove the tone of voice directive")
|
|
387
|
+
.action(async function () {
|
|
388
|
+
try {
|
|
389
|
+
await confirmDestructive(this, "Clear this workspace's tone of voice directive.");
|
|
390
|
+
const r = await call("brand", "clear_tone", {}, ctxFrom(this));
|
|
391
|
+
emit(this, r, () => process.stdout.write("Tone of voice cleared.\n"));
|
|
392
|
+
} catch (e) { fail(e); }
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
withCommonOpts(tv.command("apply <on|off>"))
|
|
396
|
+
.description("Apply the tone of voice by default when generating content")
|
|
397
|
+
.action(async function (v) {
|
|
398
|
+
try {
|
|
399
|
+
const enabled = ["on", "true", "yes", "1"].includes(String(v).toLowerCase());
|
|
400
|
+
const r = await call("brand", "set_tone_default", { apply_by_default: enabled }, ctxFrom(this));
|
|
401
|
+
emit(this, r, (d) => process.stdout.write(`Applied by default: ${d.apply_by_default ? "yes" : "no"}\n`));
|
|
402
|
+
} catch (e) { fail(e); }
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
withCommonOpts(tv.command("sync"))
|
|
406
|
+
.description("Pull the latest tone of voice from Business Build")
|
|
407
|
+
.option("--email <email>", "client identity email to resolve against")
|
|
408
|
+
.action(async function (o) {
|
|
409
|
+
try {
|
|
410
|
+
const r = await call("brand", "sync_tone", o.email ? { email: o.email } : {}, ctxFrom(this));
|
|
411
|
+
emit(this, r, (d) => printTone(d.tone || {}));
|
|
412
|
+
} catch (e) { fail(e); }
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
withCommonOpts(b.command("delete"))
|
|
416
|
+
.alias("reset")
|
|
417
|
+
.description("Delete the entire brand record for this workspace")
|
|
418
|
+
.action(async function () {
|
|
419
|
+
try {
|
|
420
|
+
await confirmDestructive(this, "Delete this workspace's whole brand record.");
|
|
421
|
+
const r = await call("brand", "delete", {}, ctxFrom(this));
|
|
422
|
+
emit(this, r, () => process.stdout.write("Brand record deleted.\n"));
|
|
423
|
+
} catch (e) { fail(e); }
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
withCommonOpts(b.command("fields"))
|
|
427
|
+
.description("List every editable brand field")
|
|
428
|
+
.action(async function () {
|
|
429
|
+
try {
|
|
430
|
+
const r = await call("brand", "fields", {}, ctxFrom(this));
|
|
431
|
+
emit(this, r, (d) => process.stdout.write(
|
|
432
|
+
`Text: ${(d.text_fields || []).join(", ")}\nLists: ${(d.list_fields || []).join(", ")}\n` +
|
|
433
|
+
`Imagery: ${(d.imagery_fields || []).join(", ")}\nTone: ${(d.tone_fields || []).join(", ")}\n` +
|
|
434
|
+
`Logos: ${(d.logo_fields || []).join(", ")}\nLogo variants: ${(d.logo_variants || []).join(", ")}\n` +
|
|
435
|
+
`Video: ${(d.video_fields || []).join(", ")}\nSocial: ${(d.social_networks || []).join(", ")}\n`,
|
|
436
|
+
));
|
|
437
|
+
} catch (e) { fail(e); }
|
|
438
|
+
});
|
|
439
|
+
}
|