@anaella/cli 0.1.0
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/README.md +49 -0
- package/dist/index.js +175 -0
- package/package.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @anaella/cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for anaella.com — schedule and publish social posts via the `/v1` API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @anaella/cli
|
|
9
|
+
# or run without installing:
|
|
10
|
+
npx @anaella/cli --help
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Auth
|
|
14
|
+
|
|
15
|
+
The API key is resolved with this precedence: `--api-key` flag > `ANAELLA_API_KEY` env var > the
|
|
16
|
+
file saved by `anaella login` (`~/.config/anaella/config.json`, or under `$XDG_CONFIG_HOME` if set).
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
anaella login --api-key ana_xxx # verifies the key against the API, then saves it
|
|
20
|
+
echo ana_xxx | anaella login # or pipe it on stdin
|
|
21
|
+
anaella logout # remove the saved key
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
anaella channels # list connected channels
|
|
28
|
+
anaella post -c <channelId> -t "hello" -w now # publish now
|
|
29
|
+
anaella post -c <channelId> -t "hello" # -w/--when defaults to "draft"
|
|
30
|
+
anaella post -c c1 -c c2 -m https://x/a.jpg -t hi # -c and -m are repeatable
|
|
31
|
+
anaella posts --limit 20 --cursor 0 # list recent posts
|
|
32
|
+
anaella get <id> # fetch one post
|
|
33
|
+
anaella delete <id> # delete a draft / cancel a scheduled post
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Global options
|
|
37
|
+
|
|
38
|
+
- `--api-key <key>` — override the resolved API key for this invocation.
|
|
39
|
+
- `--api-url <url>` — override the API base URL (default `https://api.anaella.com`).
|
|
40
|
+
- `--json` — print raw JSON instead of a table.
|
|
41
|
+
|
|
42
|
+
## post options
|
|
43
|
+
|
|
44
|
+
- `-c, --channel <id>` — channel id, repeatable, required.
|
|
45
|
+
- `-t, --text <text>` — post text.
|
|
46
|
+
- `-w, --when <when>` — `"now"` | `"draft"` | an ISO-8601 timestamp (default `draft`).
|
|
47
|
+
- `-m, --media <url>` — public media URL, repeatable.
|
|
48
|
+
- `--type <type>` — `post` | `reel` | `thread` | `story`.
|
|
49
|
+
- `--first-comment <text>` — first comment text.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { AnaellaClient } from "@anaella/client";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
|
|
8
|
+
// src/commands.ts
|
|
9
|
+
var cmdChannels = (client) => client.listChannels();
|
|
10
|
+
function cmdPost(client, o) {
|
|
11
|
+
return client.createPost({
|
|
12
|
+
channels: o.channel,
|
|
13
|
+
text: o.text,
|
|
14
|
+
when: o.when,
|
|
15
|
+
media: (o.media ?? []).map((url) => ({ url })),
|
|
16
|
+
type: o.type,
|
|
17
|
+
firstComment: o.firstComment
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
var cmdPosts = (client, o) => client.listPosts({
|
|
21
|
+
limit: o.limit === void 0 ? void 0 : Number(o.limit),
|
|
22
|
+
cursor: o.cursor === void 0 ? void 0 : Number(o.cursor)
|
|
23
|
+
});
|
|
24
|
+
var cmdGet = (client, id) => client.getPost(id);
|
|
25
|
+
var cmdDelete = (client, id) => client.deletePost(id);
|
|
26
|
+
|
|
27
|
+
// src/config.ts
|
|
28
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
29
|
+
import { homedir } from "os";
|
|
30
|
+
import { dirname, join } from "path";
|
|
31
|
+
var DEFAULT_API_URL = "https://api.anaella.com";
|
|
32
|
+
function configPath(env) {
|
|
33
|
+
const base = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
34
|
+
return join(base, "anaella", "config.json");
|
|
35
|
+
}
|
|
36
|
+
function readConfigFile(env) {
|
|
37
|
+
const path = configPath(env);
|
|
38
|
+
if (!existsSync(path)) return {};
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
41
|
+
} catch {
|
|
42
|
+
process.stderr.write(`[anaella] warning: could not parse ${path}; ignoring.
|
|
43
|
+
`);
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function writeConfigFile(env, cfg) {
|
|
48
|
+
const path = configPath(env);
|
|
49
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
50
|
+
writeFileSync(path, `${JSON.stringify(cfg, null, 2)}
|
|
51
|
+
`, { mode: 384 });
|
|
52
|
+
chmodSync(path, 384);
|
|
53
|
+
}
|
|
54
|
+
function clearConfigFile(env) {
|
|
55
|
+
rmSync(configPath(env), { force: true });
|
|
56
|
+
}
|
|
57
|
+
function resolveConfig(opts, env) {
|
|
58
|
+
const file = readConfigFile(env);
|
|
59
|
+
return {
|
|
60
|
+
apiKey: opts.apiKey || env.ANAELLA_API_KEY || file.apiKey,
|
|
61
|
+
apiUrl: (opts.apiUrl || env.ANAELLA_API_URL || file.apiUrl || DEFAULT_API_URL).replace(
|
|
62
|
+
/\/+$/,
|
|
63
|
+
""
|
|
64
|
+
)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/output.ts
|
|
69
|
+
import { AnaellaApiError } from "@anaella/client";
|
|
70
|
+
function printJson(data) {
|
|
71
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
function printTable(rows, columns) {
|
|
75
|
+
if (rows.length === 0) {
|
|
76
|
+
process.stdout.write("(none)\n");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const widths = columns.map(
|
|
80
|
+
(c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length))
|
|
81
|
+
);
|
|
82
|
+
const line = (cells) => cells.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd();
|
|
83
|
+
process.stdout.write(`${line(columns)}
|
|
84
|
+
`);
|
|
85
|
+
for (const r of rows) {
|
|
86
|
+
process.stdout.write(`${line(columns.map((c) => String(r[c] ?? "")))}
|
|
87
|
+
`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function printResult(data, json, columns) {
|
|
91
|
+
if (json || !columns) {
|
|
92
|
+
printJson(data);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
96
|
+
printTable(rows, columns);
|
|
97
|
+
}
|
|
98
|
+
function printError(err, json) {
|
|
99
|
+
if (err instanceof AnaellaApiError) {
|
|
100
|
+
if (json) {
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
`${JSON.stringify({ error: { code: err.code, message: err.message, details: err.details } }, null, 2)}
|
|
103
|
+
`
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
const detail = err.details?.length ? `
|
|
107
|
+
${err.details.map((d) => ` - ${d.field}: ${d.message}`).join("\n")}` : "";
|
|
108
|
+
process.stderr.write(`Error [${err.code}] (HTTP ${err.status}): ${err.message}${detail}
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/index.ts
|
|
118
|
+
var collect = (val, acc) => {
|
|
119
|
+
acc.push(val);
|
|
120
|
+
return acc;
|
|
121
|
+
};
|
|
122
|
+
var program = new Command();
|
|
123
|
+
program.name("anaella").description("CLI for anaella.com \u2014 schedule and publish social posts via the /v1 API.").version("0.1.0").option("--api-key <key>", "ana_ API key (overrides env + config)").option("--api-url <url>", "API base URL").option("--json", "output raw JSON");
|
|
124
|
+
async function withClient(command, fn, columns) {
|
|
125
|
+
const opts = command.optsWithGlobals();
|
|
126
|
+
const cfg = resolveConfig({ apiKey: opts.apiKey, apiUrl: opts.apiUrl }, process.env);
|
|
127
|
+
if (!cfg.apiKey) {
|
|
128
|
+
process.stderr.write(
|
|
129
|
+
"No API key. Run 'anaella login', set ANAELLA_API_KEY, or pass --api-key.\n"
|
|
130
|
+
);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const data = await fn(new AnaellaClient({ apiKey: cfg.apiKey, baseUrl: cfg.apiUrl }), opts);
|
|
136
|
+
printResult(data, Boolean(opts.json), columns);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
printError(err, Boolean(opts.json));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
program.command("login").description("Save an API key (verified against the API) to ~/.config/anaella/config.json").option("--api-key <key>", "the key (else read from stdin)").action(async (opts, command) => {
|
|
143
|
+
const globals = command.optsWithGlobals();
|
|
144
|
+
const key = opts.apiKey || readFileSync2(0, "utf8").trim();
|
|
145
|
+
if (!key) {
|
|
146
|
+
process.stderr.write("No key provided (pass --api-key or pipe it on stdin).\n");
|
|
147
|
+
process.exitCode = 1;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const apiUrl = resolveConfig({ apiUrl: globals.apiUrl }, process.env).apiUrl;
|
|
151
|
+
try {
|
|
152
|
+
await new AnaellaClient({ apiKey: key, baseUrl: apiUrl }).listChannels();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
process.stderr.write("Key verification failed:\n");
|
|
155
|
+
printError(err, false);
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
writeConfigFile(process.env, { apiKey: key, apiUrl });
|
|
160
|
+
process.stdout.write("Logged in. Key saved.\n");
|
|
161
|
+
});
|
|
162
|
+
program.command("logout").description("Remove the saved API key").action(() => {
|
|
163
|
+
clearConfigFile(process.env);
|
|
164
|
+
process.stdout.write("Logged out.\n");
|
|
165
|
+
});
|
|
166
|
+
program.command("channels").description("List connected channels").action(
|
|
167
|
+
(_opts, command) => withClient(command, (c) => cmdChannels(c), ["id", "platform", "name", "active"])
|
|
168
|
+
);
|
|
169
|
+
program.command("post").description("Create/schedule/publish a post").requiredOption("-c, --channel <id>", "channel id (repeatable)", collect, []).option("-t, --text <text>", "post text").option("-w, --when <when>", '"now" | "draft" | ISO-8601', "draft").option("-m, --media <url>", "public media URL (repeatable)", collect, []).option("--type <type>", "post | reel | thread | story").option("--first-comment <text>", "first comment").action((opts, command) => withClient(command, (c) => cmdPost(c, opts)));
|
|
170
|
+
program.command("posts").description("List recent posts").option("--limit <n>", "max results (1-100)").option("--cursor <n>", "offset").action(
|
|
171
|
+
(opts, command) => withClient(command, (c) => cmdPosts(c, opts), ["id", "type", "status", "scheduledAt"])
|
|
172
|
+
);
|
|
173
|
+
program.command("get <id>").description("Get one post by id").action((id, _opts, command) => withClient(command, (c) => cmdGet(c, id)));
|
|
174
|
+
program.command("delete <id>").description("Delete a draft or cancel a scheduled post").option("--yes", "skip confirmation").action((id, opts, command) => withClient(command, (c) => cmdDelete(c, id)));
|
|
175
|
+
program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anaella/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Command-line tool for anaella.com — schedule and publish social posts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "anaella": "dist/index.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"publishConfig": { "access": "public" },
|
|
9
|
+
"scripts": { "build": "tsup", "prepublishOnly": "npm run build", "test": "bun test" },
|
|
10
|
+
"dependencies": { "@anaella/client": "^0.1.0", "commander": "^12.0.0" },
|
|
11
|
+
"devDependencies": { "tsup": "^8.0.0", "typescript": "^5", "bun-types": "^1.1.0" }
|
|
12
|
+
}
|