@sprid/cli 0.1.2 → 0.1.4
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 +25 -0
- package/README-post.md +0 -1
- package/package.json +1 -1
- package/src/cli.mjs +2 -2
- package/src/commands/auth.mjs +7 -1
- package/src/commands/family.mjs +33 -43
- package/src/commands/init.mjs +1 -0
- package/src/commands/reviews.mjs +26 -17
- package/src/commands/studio.mjs +4 -2
- package/src/docs/commands.mjs +4 -11
- package/src/docs/guides.generated.mjs +267 -255
- package/src/docs/queries.mjs +1 -1
- package/src/post/cli.mjs +3 -8
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
sprid follows semver: a breaking change to a command's arguments, its output
|
|
4
4
|
shape or its exit codes is a major release.
|
|
5
5
|
|
|
6
|
+
## 0.1.4 - 2026-09-23
|
|
7
|
+
|
|
8
|
+
- `sprid whoami` warns when the credentials file is readable by other users
|
|
9
|
+
on the machine, with the `chmod 600` that fixes it, and reports
|
|
10
|
+
`credentialsFileSecure` in `--json`.
|
|
11
|
+
- `sprid docs` carries the tightened connect guides and the new distribution,
|
|
12
|
+
first-seconds and store-listing guides.
|
|
13
|
+
- `sprid post create --file` saves an idea when the file says `"status":
|
|
14
|
+
"idea"`: notes, a `sourceUrl` and any gathered assets, with no assets
|
|
15
|
+
required. The same `requestId` makes a retry safe, so a repo's hook bank
|
|
16
|
+
can be pushed in and pushed again.
|
|
17
|
+
- `sprid family` is removed with market editions, which the server no longer
|
|
18
|
+
has. `sprid apps`, `sprid insights` and `sprid account` are unchanged.
|
|
19
|
+
|
|
20
|
+
## 0.1.3 - 2026-09-23
|
|
21
|
+
|
|
22
|
+
- Posts and reviews have refs: `BND-78` is post 78 of the account keyed BND,
|
|
23
|
+
`BND-R4` is review 4 of the app keyed BND. Every command that takes a post id
|
|
24
|
+
takes its ref, and `sprid reviews draft|reply` take a review ref. `sprid
|
|
25
|
+
status` and the review mail print refs, which older versions refuse.
|
|
26
|
+
- `sprid reviews reply` reads the one review it replies to, so a review
|
|
27
|
+
older than the latest hundred can be answered from its draft.
|
|
28
|
+
- `sprid account avatar <account> [file|url]` sets an account's avatar; with
|
|
29
|
+
no source it uses the app's own icon. `sprid init` reports the avatar it set.
|
|
30
|
+
|
|
6
31
|
## 0.1.2 - 2026-09-22
|
|
7
32
|
|
|
8
33
|
- Published as `@sprid/cli`. The command is still `sprid`.
|
package/README-post.md
CHANGED
|
@@ -269,7 +269,6 @@ the *copy* here anyway:
|
|
|
269
269
|
carousels: {
|
|
270
270
|
kind: "composed",
|
|
271
271
|
spec: "social/specs/<slug>.json",
|
|
272
|
-
// format: <format id>, // choose from this account’s formats
|
|
273
272
|
// template: <template id>, // choose from this account’s templates
|
|
274
273
|
expect: { slides: [5, 9], words: [0, 30] },
|
|
275
274
|
neverSay: ["the phrase this account decided it does not say"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprid/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
package/src/cli.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import * as mcp from "./commands/mcp.mjs";
|
|
|
29
29
|
import * as reviews from "./commands/reviews.mjs";
|
|
30
30
|
import { marketingReview } from "./commands/marketing-review.mjs";
|
|
31
31
|
import { plan, research } from './commands/plan.mjs';
|
|
32
|
-
import { apps,
|
|
32
|
+
import { apps, insights, account } from "./commands/family.mjs";
|
|
33
33
|
import * as post from "./commands/post.mjs";
|
|
34
34
|
import { docs } from "./commands/docs.mjs";
|
|
35
35
|
import { screenshots, release } from './commands/local-tools.mjs';
|
|
@@ -55,7 +55,7 @@ export const COMMANDS = {
|
|
|
55
55
|
plan, research,
|
|
56
56
|
media, capabilities, completion,
|
|
57
57
|
screenshots, release, doctor, update,
|
|
58
|
-
apps,
|
|
58
|
+
apps, insights, account,
|
|
59
59
|
docs,
|
|
60
60
|
login: auth.login,
|
|
61
61
|
logout: auth.logout,
|
package/src/commands/auth.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a long secret, the person types a short code into a signed-in browser, and
|
|
3
3
|
// the server hands over a personal access token exactly once.
|
|
4
4
|
|
|
5
|
-
import { DEFAULT_API_URL, deleteCredentials, credentialsPath, readCredentials, stripSlash, writeCredentials } from "../creds.mjs";
|
|
5
|
+
import { DEFAULT_API_URL, deleteCredentials, credentialsModeOk, credentialsPath, readCredentials, stripSlash, writeCredentials } from "../creds.mjs";
|
|
6
6
|
import { ApiError } from "../http.mjs";
|
|
7
7
|
import { C, OK } from "../format.mjs";
|
|
8
8
|
import { pickWorkspace } from "../cli.mjs";
|
|
@@ -124,6 +124,8 @@ export async function whoami(ctx) {
|
|
|
124
124
|
// The current one, if it can be known without asking; never exit 2 from whoami.
|
|
125
125
|
const current = await ctx.workspace().catch(() => null);
|
|
126
126
|
const source = a.source === "env" ? "SPRID_PAT" : credentialsPath(ctx.env);
|
|
127
|
+
// Written 0600, but a later copy, restore or chmod can loosen it silently.
|
|
128
|
+
const fileSecure = a.source === "env" ? null : credentialsModeOk(ctx.env);
|
|
127
129
|
if (ctx.json) {
|
|
128
130
|
ctx.out({
|
|
129
131
|
email: user.email ?? a.email ?? null,
|
|
@@ -133,6 +135,7 @@ export async function whoami(ctx) {
|
|
|
133
135
|
apiUrl: a.apiUrl,
|
|
134
136
|
tokenSource: source,
|
|
135
137
|
tokenId: a.tokenId,
|
|
138
|
+
credentialsFileSecure: fileSecure,
|
|
136
139
|
});
|
|
137
140
|
return 0;
|
|
138
141
|
}
|
|
@@ -143,6 +146,9 @@ export async function whoami(ctx) {
|
|
|
143
146
|
: "no workspace";
|
|
144
147
|
ctx.print(` ${user.email ?? a.email ?? "(unknown)"} · workspace ${wsLine}`);
|
|
145
148
|
ctx.print(C.dim(` ${a.apiUrl} · token from ${source}`));
|
|
149
|
+
if (fileSecure === false) {
|
|
150
|
+
ctx.print(` Warning: ${source} is readable by other users on this machine. Fix: chmod 600 "${source}"`);
|
|
151
|
+
}
|
|
146
152
|
if (workspaces.length > 1) {
|
|
147
153
|
ctx.print(workspaceList(workspaces, current));
|
|
148
154
|
if (!current) ctx.print(C.dim(" Pick one: sprid use <slug>"));
|
package/src/commands/family.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
3
|
import { UsageError } from "../args.mjs";
|
|
4
4
|
import { resolveProfile } from "../profiles.mjs";
|
|
5
5
|
|
|
@@ -18,53 +18,13 @@ function objectFile(ctx) {
|
|
|
18
18
|
function positiveId(raw) {
|
|
19
19
|
const id = Number(raw);
|
|
20
20
|
if (!Number.isInteger(id) || id <= 0)
|
|
21
|
-
throw new UsageError("Pass a positive
|
|
21
|
+
throw new UsageError("Pass a positive account ID.");
|
|
22
22
|
return id;
|
|
23
23
|
}
|
|
24
24
|
export async function apps(ctx) {
|
|
25
25
|
ctx.out(await ctx.api().get("/api/app-profiles/hierarchy"));
|
|
26
26
|
return 0;
|
|
27
27
|
}
|
|
28
|
-
export async function family(ctx) {
|
|
29
|
-
const [verb = "list", raw] = ctx.positionals;
|
|
30
|
-
let result;
|
|
31
|
-
if (verb === "list" || verb === "compare") {
|
|
32
|
-
const workspaceId = await ctx.workspaceId();
|
|
33
|
-
const app = await resolveProfile(ctx, workspaceId);
|
|
34
|
-
result = await ctx
|
|
35
|
-
.api()
|
|
36
|
-
.get(
|
|
37
|
-
`/api/content-families${verb === "compare" ? "/performance" : ""}?workspaceId=${workspaceId}&app=${app.id}&ageDays=${encodeURIComponent(ctx.flags.days ?? 7)}`,
|
|
38
|
-
);
|
|
39
|
-
} else if (verb === "create")
|
|
40
|
-
result = await ctx.api().post("/api/content-families", objectFile(ctx));
|
|
41
|
-
else {
|
|
42
|
-
const id = positiveId(raw);
|
|
43
|
-
const edition = ["get", "preview", "review", "localize"].includes(verb);
|
|
44
|
-
const path = edition
|
|
45
|
-
? `/api/content-families/editions/${id}`
|
|
46
|
-
: `/api/content-families/${id}`;
|
|
47
|
-
if (verb === "get") result = await ctx.api().get(path);
|
|
48
|
-
else if (verb === "preview")
|
|
49
|
-
result = await ctx.api().post(`${path}/preview`, {});
|
|
50
|
-
else if (
|
|
51
|
-
["edition", "review", "localize", "plan", "schedule"].includes(verb)
|
|
52
|
-
)
|
|
53
|
-
result = await ctx
|
|
54
|
-
.api()
|
|
55
|
-
.post(
|
|
56
|
-
`${path}/${verb === "edition" ? "editions" : verb}`,
|
|
57
|
-
objectFile(ctx),
|
|
58
|
-
);
|
|
59
|
-
else
|
|
60
|
-
throw new UsageError(
|
|
61
|
-
"sprid family list|create|edition|get|localize|preview|review|plan|schedule|compare [id] [--file payload.json]",
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
// Always print exact IDs, preview URLs, blockers and revision fingerprints.
|
|
65
|
-
ctx.out(result);
|
|
66
|
-
return 0;
|
|
67
|
-
}
|
|
68
28
|
export async function insights(ctx) {
|
|
69
29
|
const workspaceId = await ctx.workspaceId();
|
|
70
30
|
const app = await resolveProfile(ctx, workspaceId);
|
|
@@ -88,9 +48,39 @@ export async function account(ctx) {
|
|
|
88
48
|
.api()
|
|
89
49
|
.patch(`/api/accounts/${positiveId(raw)}`, objectFile(ctx)),
|
|
90
50
|
);
|
|
51
|
+
else if (verb === "avatar") return accountAvatar(ctx, raw, ctx.positionals[2]);
|
|
91
52
|
else
|
|
92
53
|
throw new UsageError(
|
|
93
|
-
"sprid account create|update [id] --file payload.json",
|
|
54
|
+
"sprid account create|update [id] --file payload.json | sprid account avatar <account> [file|url]",
|
|
94
55
|
);
|
|
95
56
|
return 0;
|
|
96
57
|
}
|
|
58
|
+
|
|
59
|
+
const AVATAR_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" };
|
|
60
|
+
|
|
61
|
+
// The account's avatar: what Sprid's mails and the account.avatarUrl template
|
|
62
|
+
// binding draw. No source = the app's own icon, looked up by the server.
|
|
63
|
+
async function accountAvatar(ctx, ref, source) {
|
|
64
|
+
if (!ref) throw new UsageError("sprid account avatar <account> [file|url]");
|
|
65
|
+
const account = encodeURIComponent(String(ref));
|
|
66
|
+
if (!source || /^https?:\/\//i.test(source)) {
|
|
67
|
+
const result = await ctx.api().post(`/api/accounts/${account}/avatar/from-url`, source ? { url: source } : {});
|
|
68
|
+
if (ctx.flags.json) ctx.out(result);
|
|
69
|
+
else ctx.print(`Avatar set from ${result.source} (${result.size}): ${result.avatarUrl}`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const path = resolve(ctx.cwd, source);
|
|
73
|
+
const ext = extname(path).toLowerCase();
|
|
74
|
+
const contentType = AVATAR_TYPES[ext];
|
|
75
|
+
if (!contentType) throw new UsageError("The avatar must be a PNG, JPEG or WebP file. SVG and ICO do not render in mail clients.");
|
|
76
|
+
const bytes = readFileSync(path);
|
|
77
|
+
if (bytes.length > 5 * 1024 * 1024) throw new UsageError("The avatar file is over 5 MB. Export the icon at 512 or 1024 px.");
|
|
78
|
+
const api = ctx.api();
|
|
79
|
+
const { uploadUrl, key } = await api.post(`/api/accounts/${account}/avatar/upload-url`, { contentType });
|
|
80
|
+
const put = await (ctx.fetch ?? fetch)(uploadUrl, { method: "PUT", headers: { "content-type": contentType }, body: bytes, redirect: "error", signal: AbortSignal.timeout(60000) });
|
|
81
|
+
if (!put.ok) throw new Error(`Uploading the avatar failed (${put.status}).`);
|
|
82
|
+
const result = await api.post(`/api/accounts/${account}/avatar/confirm`, { key });
|
|
83
|
+
if (ctx.flags.json) ctx.out(result);
|
|
84
|
+
else ctx.print(`Avatar set from ${source}: ${result.avatarUrl}`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -128,6 +128,7 @@ export async function init(ctx) {
|
|
|
128
128
|
ctx.print(` ${C.ok(OK)} ${existing ? "Updated" : "Created"} App Profile ${C.bold(profile.slug)} (${profile.name}) from ${source}`);
|
|
129
129
|
const set = PROFILE_FIELDS.filter((k) => k !== "slug" && k !== "name" && profile[k]).map((k) => [k, profile[k]]);
|
|
130
130
|
if (set.length) ctx.print(table(set, { indent: " " }));
|
|
131
|
+
if (profile.avatar) ctx.print(` ${C.ok(OK)} Avatar for ${profile.avatar.accounts.join(", ")} from the ${profile.avatar.source} icon (${profile.avatar.size})`);
|
|
131
132
|
if (missing.length) {
|
|
132
133
|
ctx.print(" Still missing:");
|
|
133
134
|
ctx.print(table(missing.map((m) => [`${NONE} ${m.label.split(" · ")[0]}`, C.dim("→"), m.cli]), { indent: " " }));
|
package/src/commands/reviews.mjs
CHANGED
|
@@ -70,18 +70,24 @@ export function renderReview(r) {
|
|
|
70
70
|
return lines.join("\n");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// A review is named by its row id (41) or its ref (BND-R41); the server
|
|
74
|
+
// resolves either, so the CLI only checks the shape.
|
|
73
75
|
function needId(arg, usage) {
|
|
74
|
-
const id =
|
|
75
|
-
if (
|
|
76
|
-
return id;
|
|
76
|
+
const id = String(arg ?? "").trim();
|
|
77
|
+
if (/^[1-9]\d*$/.test(id)) return Number(id); // --json keeps printing a number
|
|
78
|
+
if (/^[A-Za-z][A-Za-z0-9]{1,4}-[Rr]\d+$/.test(id)) return id.toUpperCase();
|
|
79
|
+
throw new UsageError(usage);
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
/** `BND-R41` as is; a bare row id as `#41`. */
|
|
83
|
+
const label = (id) => (/^\d+$/.test(String(id)) ? `#${id}` : String(id));
|
|
84
|
+
|
|
79
85
|
async function draft(ctx, arg) {
|
|
80
|
-
const id = needId(arg, "sprid reviews draft <id>");
|
|
81
|
-
const res = await ctx.api().post(`/api/store-reviews/${id}/draft`);
|
|
86
|
+
const id = needId(arg, "sprid reviews draft <id|ref>");
|
|
87
|
+
const res = await ctx.api().post(`/api/store-reviews/${encodeURIComponent(id)}/draft`);
|
|
82
88
|
if (ctx.json) ctx.out(res);
|
|
83
89
|
else {
|
|
84
|
-
ctx.print(` Draft for review
|
|
90
|
+
ctx.print(` Draft for review ${label(id)}:`);
|
|
85
91
|
ctx.print(` ${res.draftReply ?? ""}`);
|
|
86
92
|
ctx.print(C.dim(` Send it: sprid reviews reply ${id} (edit first: --text "…")`));
|
|
87
93
|
}
|
|
@@ -89,15 +95,18 @@ async function draft(ctx, arg) {
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
async function reply(ctx, arg) {
|
|
92
|
-
const id = needId(arg, "sprid reviews reply <id> [--text …] [--yes]");
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
const id = needId(arg, "sprid reviews reply <id|ref> [--text …] [--yes]");
|
|
99
|
+
// The one review, by id or ref. Any age, not only the latest hundred.
|
|
100
|
+
let row = null;
|
|
101
|
+
try {
|
|
102
|
+
row = (await ctx.api().get(`/api/store-reviews/${encodeURIComponent(id)}`)).review ?? null;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (!(err instanceof ApiError) || err.status !== 404) throw err;
|
|
105
|
+
}
|
|
97
106
|
let text = ctx.flags.text && ctx.flags.text !== true ? String(ctx.flags.text) : null;
|
|
98
107
|
if (!text) {
|
|
99
|
-
if (!row) throw new ApiError(404, `Review
|
|
100
|
-
if (!row.draftReply) throw new UsageError(`Review
|
|
108
|
+
if (!row) throw new ApiError(404, `Review ${label(id)} was not found; pass --text to reply anyway.`);
|
|
109
|
+
if (!row.draftReply) throw new UsageError(`Review ${label(id)} has no draft. Run \`sprid reviews draft ${id}\` or pass --text.`);
|
|
101
110
|
text = row.draftReply;
|
|
102
111
|
}
|
|
103
112
|
const source = row?.source ?? null;
|
|
@@ -113,13 +122,13 @@ async function reply(ctx, arg) {
|
|
|
113
122
|
if (ctx.json) {
|
|
114
123
|
ctx.out({ ok: false, error: msg, id, text, store: source, limit });
|
|
115
124
|
} else {
|
|
116
|
-
ctx.print(` Will send to ${STORE_LABEL[source] ?? "the store"}${limit ? ` (${text.length}/${limit} chars)` : ""} for review
|
|
125
|
+
ctx.print(` Will send to ${STORE_LABEL[source] ?? "the store"}${limit ? ` (${text.length}/${limit} chars)` : ""} for review ${label(row?.ref ?? id)}:`);
|
|
117
126
|
ctx.print(` ${text}`);
|
|
118
127
|
ctx.warn(` ✗ ${msg}`);
|
|
119
128
|
}
|
|
120
129
|
return 2;
|
|
121
130
|
}
|
|
122
|
-
ctx.print(` Will send to ${STORE_LABEL[source] ?? "the store"}${limit ? ` (${text.length}/${limit} chars)` : ""} for review
|
|
131
|
+
ctx.print(` Will send to ${STORE_LABEL[source] ?? "the store"}${limit ? ` (${text.length}/${limit} chars)` : ""} for review ${label(row?.ref ?? id)}:`);
|
|
123
132
|
if (row) ctx.print(C.dim(` re: ${stars(row.rating)} ${truncate([row.title, row.body].filter(Boolean).join(" · "), 120)}`));
|
|
124
133
|
ctx.print("");
|
|
125
134
|
ctx.print(` ${text}`);
|
|
@@ -131,9 +140,9 @@ async function reply(ctx, arg) {
|
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
|
|
134
|
-
const res = await ctx.api().post(`/api/store-reviews/${id}/reply`, { text });
|
|
143
|
+
const res = await ctx.api().post(`/api/store-reviews/${encodeURIComponent(id)}/reply`, { text });
|
|
135
144
|
if (ctx.json) ctx.out({ ok: true, id, text, review: res.review ?? null });
|
|
136
|
-
else ctx.print(` ${C.ok(OK)} Reply sent for review
|
|
145
|
+
else ctx.print(` ${C.ok(OK)} Reply sent for review ${label(row?.ref ?? id)}${source ? ` on ${STORE_LABEL[source]}` : ""}.`);
|
|
137
146
|
return 0;
|
|
138
147
|
}
|
|
139
148
|
|
package/src/commands/studio.mjs
CHANGED
|
@@ -11,10 +11,12 @@ async function payload(ctx) {
|
|
|
11
11
|
}
|
|
12
12
|
export async function remotePost(ctx) {
|
|
13
13
|
const [verb, ref] = ctx.positionals;
|
|
14
|
-
|
|
14
|
+
let id = ctx.flags.id ?? ref;
|
|
15
15
|
if (!['create', 'get', 'update', 'deliveries', 'handoff'].includes(verb) && !(verb === 'preview' && ctx.flags.id)) return false;
|
|
16
|
-
|
|
16
|
+
// The server post id (271) or its ref (BND-78); the server resolves either.
|
|
17
|
+
if (verb !== 'create' && !/^([1-9]\d*|[A-Za-z][A-Za-z0-9]{1,4}-\d+)$/.test(String(id))) throw new UsageError('Supply the server post ID or its ref, such as 271 or BND-78. Local preview slugs remain unchanged.');
|
|
17
18
|
const api = ctx.api();
|
|
19
|
+
if (verb !== 'create') id = encodeURIComponent(String(id).toUpperCase());
|
|
18
20
|
const result = verb === 'create' ? await api.post('/api/studio/posts', await payload(ctx))
|
|
19
21
|
: verb === 'update' ? await api.patch(`/api/posts/${id}`, await payload(ctx))
|
|
20
22
|
: verb === 'preview' ? await api.get(`/api/studio/posts/${id}/preview`)
|
package/src/docs/commands.mjs
CHANGED
|
@@ -28,8 +28,8 @@ export const COMMAND_GROUPS = [
|
|
|
28
28
|
['media', 'sprid media reel --file <recipe.json>', 'Create a metered hosted photos/text reel after explicit user authorization. Returns a durable job ID.'],
|
|
29
29
|
['media', 'sprid media job <UUID>', 'Read saved upload results or creation progress without repeating the operation.'],
|
|
30
30
|
['media', 'sprid media init|build|register|check|push|preview …', 'Run the existing local media pipeline. Historical sprid post invocations remain supported.'],
|
|
31
|
-
['post', 'sprid post create --file <draft.json>', 'Create a shared draft from ordered asset references and a stable requestId. Never publishes.'],
|
|
32
|
-
['post', 'sprid post get <postId>', 'Read the shared post and its slides.'],
|
|
31
|
+
['post', 'sprid post create --file <draft.json>', 'Create a shared draft from ordered asset references and a stable requestId. With "status": "idea" it saves an idea instead: notes, a sourceUrl and any gathered assets, before layout. Never publishes.'],
|
|
32
|
+
['post', 'sprid post get <postId>', 'Read the shared post and its slides. Every <postId> here takes the number or the post\'s ref, such as BND-142.'],
|
|
33
33
|
['post', 'sprid post update <postId> --file <changes.json>', 'Apply the existing post-update contract to a shared draft.'],
|
|
34
34
|
['post', 'sprid post preview --id <postId>', 'Preview a shared post. Without --id, historical local preview arguments are unchanged.'],
|
|
35
35
|
['post', 'sprid post deliveries <postId>', 'Read scheduled, failed and delivered publications with platform receipts.'],
|
|
@@ -45,18 +45,11 @@ export const COMMAND_GROUPS = [
|
|
|
45
45
|
['release', 'sprid release metadata|screenshots|graphics [--config release.config.ts] [--execute]', 'Preview store listing changes locally; --execute sends the reviewed changes. Requires Bun.'],
|
|
46
46
|
['release', 'sprid release ship [--execute] [--ios-only|--android-only]', 'Build and submit your app using its release configuration. Dry run by default; use --help for all options.'],
|
|
47
47
|
] },
|
|
48
|
-
{ title: 'Apps and
|
|
48
|
+
{ title: 'Apps and accounts', entries: [
|
|
49
49
|
['account', 'sprid account create|update [id] --file payload.json', 'Create an app content account or update its identity. Creation requires workspaceId, appProfileId, name and slug; market, locale, persona and timezone are explicit.'],
|
|
50
|
+
['account', 'sprid account avatar <account> [file|url]', 'Set the avatar Sprid’s mails and templates draw. No source uses the app’s icon: App Store artwork, else the website’s manifest or touch icon. A file must be PNG, JPEG or WebP; a 1024 px app icon from the repo is the best source.'],
|
|
50
51
|
['apps', 'sprid apps', 'List apps with their content accounts and workspace IDs.'],
|
|
51
52
|
['insights', 'sprid insights --app <slug> [--account <slug>] [--days N]', 'Read app results once, with social content optionally narrowed to one account.'],
|
|
52
|
-
['family', 'sprid family compare --app <slug> [--days N]', 'Compare themes at matched post age with sample counts and channel baselines.'],
|
|
53
|
-
['family', 'sprid family list --app <slug>', 'List content families, themes and market editions.'],
|
|
54
|
-
['family', 'sprid family create --file payload.json', 'Create a family: sourcePostId, title, theme and sourceReference.'],
|
|
55
|
-
['family', 'sprid family edition <familyId> --file payload.json', 'Copy an independent draft: accountId, locale and market. Translate it in the editor or through the API.'],
|
|
56
|
-
['family', 'sprid family localize <postId> --file payload.json', 'Save translated captions and slide text or reel beats atomically, clearing old approval.'],
|
|
57
|
-
['family', 'sprid family get|preview <postId>', 'Read the edition or render its current content for review.'],
|
|
58
|
-
['family', 'sprid family review <postId> --file payload.json', 'Approve a rendered revision with fingerprint, sourceReference and confirmed: true.'],
|
|
59
|
-
['family', 'sprid family plan|schedule <familyId> --file payload.json', 'Plan exact postId/channelId/scheduledFor destinations. Scheduling also requires the returned planFingerprint.'],
|
|
60
53
|
] },
|
|
61
54
|
{ title: 'Sign in', entries: [
|
|
62
55
|
['setup', 'sprid setup begin --file <artifact> --task publish|review|repo [--source <source>] [--entry website|installed-skill|browser|paid]', 'Save the first useful result locally before signup. Repeating begin preserves the same task and retry key.'],
|