@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.
Files changed (3) hide show
  1. package/README.md +49 -0
  2. package/dist/index.js +175 -0
  3. 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
+ }