@wahlu/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/LICENSE +21 -0
- package/README.md +484 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1294 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/lib/config.ts
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var CONFIG_DIR = join(homedir(), ".config", "wahlu");
|
|
14
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
17
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function readConfig() {
|
|
21
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeConfig(config) {
|
|
29
|
+
ensureDir();
|
|
30
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
31
|
+
}
|
|
32
|
+
function getApiKey() {
|
|
33
|
+
const envKey = process.env.WAHLU_API_KEY;
|
|
34
|
+
if (envKey) return envKey;
|
|
35
|
+
const config = readConfig();
|
|
36
|
+
if (config.api_key) return config.api_key;
|
|
37
|
+
console.error(
|
|
38
|
+
"No API key found. Set WAHLU_API_KEY or run: wahlu auth login"
|
|
39
|
+
);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
function getApiUrl() {
|
|
43
|
+
return process.env.WAHLU_API_URL || readConfig().api_url || "https://api.wahlu.com";
|
|
44
|
+
}
|
|
45
|
+
function getDefaultBrandId() {
|
|
46
|
+
return process.env.WAHLU_BRAND_ID || readConfig().default_brand_id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/commands/auth.ts
|
|
50
|
+
var authCommand = new Command("auth").description("Authenticate with your Wahlu API key").addHelpText(
|
|
51
|
+
"after",
|
|
52
|
+
`
|
|
53
|
+
Manage your Wahlu API key for CLI authentication.
|
|
54
|
+
|
|
55
|
+
Subcommands:
|
|
56
|
+
login <key> Save an API key to ~/.config/wahlu/config.json
|
|
57
|
+
logout Remove the saved API key
|
|
58
|
+
status Show current authentication state and config
|
|
59
|
+
|
|
60
|
+
Authentication priority:
|
|
61
|
+
1. WAHLU_API_KEY environment variable (highest priority)
|
|
62
|
+
2. Saved key in ~/.config/wahlu/config.json
|
|
63
|
+
|
|
64
|
+
Generate an API key at wahlu.com under Settings > API Keys.
|
|
65
|
+
Full documentation: https://wahlu.com/docs`
|
|
66
|
+
);
|
|
67
|
+
authCommand.command("login").description("Save your API key").argument("<api-key>", "Your Wahlu API key (wahlu_live_... or wahlu_test_...)").addHelpText(
|
|
68
|
+
"after",
|
|
69
|
+
`
|
|
70
|
+
Saves the API key to ~/.config/wahlu/config.json for use in future commands.
|
|
71
|
+
The key must start with wahlu_live_ (production) or wahlu_test_ (development).
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
wahlu auth login wahlu_live_abc123def456...
|
|
75
|
+
wahlu auth login wahlu_test_abc123def456...
|
|
76
|
+
|
|
77
|
+
You can also skip this and use an environment variable instead:
|
|
78
|
+
export WAHLU_API_KEY=wahlu_live_abc123...`
|
|
79
|
+
).action((apiKey) => {
|
|
80
|
+
if (!apiKey.startsWith("wahlu_live_") && !apiKey.startsWith("wahlu_test_")) {
|
|
81
|
+
console.error(
|
|
82
|
+
"Invalid API key format. Keys start with wahlu_live_ or wahlu_test_"
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const config = readConfig();
|
|
87
|
+
config.api_key = apiKey;
|
|
88
|
+
writeConfig(config);
|
|
89
|
+
console.log("API key saved to ~/.config/wahlu/config.json");
|
|
90
|
+
});
|
|
91
|
+
authCommand.command("logout").description("Remove saved API key").addHelpText(
|
|
92
|
+
"after",
|
|
93
|
+
`
|
|
94
|
+
Removes the API key from ~/.config/wahlu/config.json.
|
|
95
|
+
Does not affect the WAHLU_API_KEY environment variable.`
|
|
96
|
+
).action(() => {
|
|
97
|
+
const config = readConfig();
|
|
98
|
+
delete config.api_key;
|
|
99
|
+
writeConfig(config);
|
|
100
|
+
console.log("API key removed.");
|
|
101
|
+
});
|
|
102
|
+
authCommand.command("status").description("Show current auth status").addHelpText(
|
|
103
|
+
"after",
|
|
104
|
+
`
|
|
105
|
+
Shows which authentication method is active and any saved configuration.
|
|
106
|
+
|
|
107
|
+
Displays:
|
|
108
|
+
- Auth source (env var or config file) and masked key
|
|
109
|
+
- Custom API URL if set
|
|
110
|
+
- Default brand ID if set`
|
|
111
|
+
).action(() => {
|
|
112
|
+
const envKey = process.env.WAHLU_API_KEY;
|
|
113
|
+
const config = readConfig();
|
|
114
|
+
if (envKey) {
|
|
115
|
+
console.log(
|
|
116
|
+
`Authenticated via WAHLU_API_KEY env var (${mask(envKey)})`
|
|
117
|
+
);
|
|
118
|
+
} else if (config.api_key) {
|
|
119
|
+
console.log(
|
|
120
|
+
`Authenticated via config file (${mask(config.api_key)})`
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
console.log("Not authenticated. Run: wahlu auth login <api-key>");
|
|
124
|
+
}
|
|
125
|
+
if (config.api_url) {
|
|
126
|
+
console.log(`API URL: ${config.api_url}`);
|
|
127
|
+
}
|
|
128
|
+
if (config.default_brand_id) {
|
|
129
|
+
console.log(`Default brand: ${config.default_brand_id}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
function mask(key) {
|
|
133
|
+
if (key.length <= 16) return "***";
|
|
134
|
+
return `${key.slice(0, 12)}...${key.slice(-4)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/commands/brand.ts
|
|
138
|
+
import { Command as Command2 } from "commander";
|
|
139
|
+
|
|
140
|
+
// src/lib/client.ts
|
|
141
|
+
var WahluClient = class {
|
|
142
|
+
baseUrl;
|
|
143
|
+
apiKey;
|
|
144
|
+
constructor(apiKey, baseUrl = "https://api.wahlu.com") {
|
|
145
|
+
this.apiKey = apiKey;
|
|
146
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
147
|
+
}
|
|
148
|
+
async request(method, path, body) {
|
|
149
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
150
|
+
const headers = {
|
|
151
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
152
|
+
"User-Agent": "wahlu-cli"
|
|
153
|
+
};
|
|
154
|
+
if (body) {
|
|
155
|
+
headers["Content-Type"] = "application/json";
|
|
156
|
+
}
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
method,
|
|
159
|
+
headers,
|
|
160
|
+
body: body ? JSON.stringify(body) : void 0
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const text = await res.text();
|
|
164
|
+
let message;
|
|
165
|
+
try {
|
|
166
|
+
const json = JSON.parse(text);
|
|
167
|
+
message = json.error?.message || json.message || `HTTP ${res.status}`;
|
|
168
|
+
} catch {
|
|
169
|
+
message = text || `HTTP ${res.status} ${res.statusText}`;
|
|
170
|
+
}
|
|
171
|
+
throw new CliError(message, res.status);
|
|
172
|
+
}
|
|
173
|
+
return res.json();
|
|
174
|
+
}
|
|
175
|
+
async get(path) {
|
|
176
|
+
return this.request("GET", path);
|
|
177
|
+
}
|
|
178
|
+
async list(path, page, limit) {
|
|
179
|
+
const params = new URLSearchParams();
|
|
180
|
+
if (page !== void 0) params.set("page", String(page));
|
|
181
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
182
|
+
const qs = params.toString();
|
|
183
|
+
return this.request(
|
|
184
|
+
"GET",
|
|
185
|
+
qs ? `${path}?${qs}` : path
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
async post(path, body) {
|
|
189
|
+
return this.request("POST", path, body);
|
|
190
|
+
}
|
|
191
|
+
async patch(path, body) {
|
|
192
|
+
return this.request("PATCH", path, body);
|
|
193
|
+
}
|
|
194
|
+
async delete(path) {
|
|
195
|
+
return this.request("DELETE", path);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
var CliError = class extends Error {
|
|
199
|
+
status;
|
|
200
|
+
constructor(message, status) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "CliError";
|
|
203
|
+
this.status = status;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/lib/output.ts
|
|
208
|
+
function output(data, opts = {}) {
|
|
209
|
+
if (opts.json) {
|
|
210
|
+
console.log(JSON.stringify(data, null, 2));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (opts.columns && Array.isArray(data)) {
|
|
214
|
+
printTable(data, opts.columns);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
218
|
+
const record = data;
|
|
219
|
+
const maxKey = Math.max(...Object.keys(record).map((k) => k.length));
|
|
220
|
+
for (const [key, value] of Object.entries(record)) {
|
|
221
|
+
const display = value === null || value === void 0 ? "-" : typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
222
|
+
console.log(`${key.padEnd(maxKey + 2)}${display}`);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.log(JSON.stringify(data, null, 2));
|
|
227
|
+
}
|
|
228
|
+
function printTable(rows, columns) {
|
|
229
|
+
if (rows.length === 0) {
|
|
230
|
+
console.log("No results.");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const widths = columns.map((col) => {
|
|
234
|
+
const headerLen = col.header.length;
|
|
235
|
+
const maxData = rows.reduce((max, row) => {
|
|
236
|
+
const val = formatCell(row[col.key], col.transform);
|
|
237
|
+
return Math.max(max, val.length);
|
|
238
|
+
}, 0);
|
|
239
|
+
return col.width || Math.min(Math.max(headerLen, maxData), 50);
|
|
240
|
+
});
|
|
241
|
+
const header = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
242
|
+
console.log(header);
|
|
243
|
+
console.log(widths.map((w) => "\u2500".repeat(w)).join(" "));
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
const line = columns.map((col, i) => {
|
|
246
|
+
const val = formatCell(row[col.key], col.transform);
|
|
247
|
+
return truncate(val, widths[i]).padEnd(widths[i]);
|
|
248
|
+
}).join(" ");
|
|
249
|
+
console.log(line);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function formatCell(value, transform) {
|
|
253
|
+
if (transform) return transform(value);
|
|
254
|
+
if (value === null || value === void 0) return "-";
|
|
255
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
256
|
+
return String(value);
|
|
257
|
+
}
|
|
258
|
+
function truncate(str, max) {
|
|
259
|
+
if (str.length <= max) return str;
|
|
260
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/commands/brand.ts
|
|
264
|
+
var brandCommand = new Command2("brand").description("List brands, view details, set a default brand").addHelpText(
|
|
265
|
+
"after",
|
|
266
|
+
`
|
|
267
|
+
A brand represents a social media profile in Wahlu. All posts, media,
|
|
268
|
+
schedules, and queues belong to a brand. Most commands require a brand.
|
|
269
|
+
|
|
270
|
+
Subcommands:
|
|
271
|
+
list List all brands accessible to your API key
|
|
272
|
+
get <id> View full details for a brand
|
|
273
|
+
switch <id> Set a default brand for all future commands
|
|
274
|
+
|
|
275
|
+
Setting a default brand:
|
|
276
|
+
wahlu brand switch <brand-id>
|
|
277
|
+
|
|
278
|
+
This saves the brand ID to ~/.config/wahlu/config.json so you don't
|
|
279
|
+
need --brand on every command. You can also set WAHLU_BRAND_ID as
|
|
280
|
+
an environment variable.
|
|
281
|
+
|
|
282
|
+
Full documentation: https://wahlu.com/docs`
|
|
283
|
+
);
|
|
284
|
+
brandCommand.command("list").description("List all brands").option("--json", "Output as JSON").addHelpText(
|
|
285
|
+
"after",
|
|
286
|
+
`
|
|
287
|
+
Lists all brands accessible to your API key.
|
|
288
|
+
|
|
289
|
+
Response fields:
|
|
290
|
+
id string Brand ID
|
|
291
|
+
name string Brand name
|
|
292
|
+
description string|null Brand description
|
|
293
|
+
logo_url string|null Logo URL
|
|
294
|
+
timezone string|null IANA timezone (e.g. "Australia/Sydney")
|
|
295
|
+
website string|null Brand website URL
|
|
296
|
+
business_category string|null Business category
|
|
297
|
+
brand_kit object|null Brand kit (fonts, colours, voice)
|
|
298
|
+
content_preferences object|null CTA, logo frequency settings
|
|
299
|
+
image_posting object|null Image posting preferences
|
|
300
|
+
video_posting object|null Video posting preferences
|
|
301
|
+
created_at string ISO 8601 timestamp
|
|
302
|
+
updated_at string ISO 8601 timestamp
|
|
303
|
+
|
|
304
|
+
Examples:
|
|
305
|
+
wahlu brand list
|
|
306
|
+
wahlu brand list --json`
|
|
307
|
+
).action(async (opts) => {
|
|
308
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
309
|
+
const res = await client.get("/brands");
|
|
310
|
+
output(res.data, {
|
|
311
|
+
json: opts.json,
|
|
312
|
+
columns: [
|
|
313
|
+
{ key: "id", header: "ID", width: 24 },
|
|
314
|
+
{ key: "name", header: "Name", width: 30 },
|
|
315
|
+
{ key: "timezone", header: "Timezone", width: 20 },
|
|
316
|
+
{ key: "website", header: "Website", width: 30 }
|
|
317
|
+
]
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
brandCommand.command("get").description("Get brand details").argument("<brand-id>", "Brand ID").option("--json", "Output as JSON").addHelpText(
|
|
321
|
+
"after",
|
|
322
|
+
`
|
|
323
|
+
Returns full details for a single brand including brand kit, content
|
|
324
|
+
preferences, and posting configuration.
|
|
325
|
+
|
|
326
|
+
Examples:
|
|
327
|
+
wahlu brand get abc123
|
|
328
|
+
wahlu brand get abc123 --json`
|
|
329
|
+
).action(async (brandId, opts) => {
|
|
330
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
331
|
+
const res = await client.get(`/brands/${brandId}`);
|
|
332
|
+
output(res.data, { json: opts.json });
|
|
333
|
+
});
|
|
334
|
+
brandCommand.command("switch").description("Set default brand for all commands").argument("<brand-id>", "Brand ID to use as default").addHelpText(
|
|
335
|
+
"after",
|
|
336
|
+
`
|
|
337
|
+
Saves the brand ID to ~/.config/wahlu/config.json. All commands that
|
|
338
|
+
require a brand will use this ID unless overridden with --brand.
|
|
339
|
+
|
|
340
|
+
Examples:
|
|
341
|
+
wahlu brand switch abc123
|
|
342
|
+
wahlu post list # uses abc123 automatically
|
|
343
|
+
wahlu post list --brand xyz789 # overrides for this command`
|
|
344
|
+
).action((brandId) => {
|
|
345
|
+
const config = readConfig();
|
|
346
|
+
config.default_brand_id = brandId;
|
|
347
|
+
writeConfig(config);
|
|
348
|
+
console.log(`Default brand set to ${brandId}`);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// src/commands/idea.ts
|
|
352
|
+
import { Command as Command3 } from "commander";
|
|
353
|
+
|
|
354
|
+
// src/lib/resolve-brand.ts
|
|
355
|
+
function resolveBrandId(cmd) {
|
|
356
|
+
const brandId = cmd.optsWithGlobals().brand || getDefaultBrandId();
|
|
357
|
+
if (!brandId) {
|
|
358
|
+
console.error(
|
|
359
|
+
"No brand specified. Use --brand <id>, set WAHLU_BRAND_ID, or run: wahlu brand switch <id>"
|
|
360
|
+
);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
return brandId;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/commands/idea.ts
|
|
367
|
+
var ideaCommand = new Command3("idea").description("Save, list, and manage content ideas").addHelpText(
|
|
368
|
+
"after",
|
|
369
|
+
`
|
|
370
|
+
Ideas are a scratchpad for future content concepts. Save ideas as you
|
|
371
|
+
think of them, then develop them into full posts later.
|
|
372
|
+
|
|
373
|
+
Subcommands:
|
|
374
|
+
list List all ideas
|
|
375
|
+
create <name> Save a new idea
|
|
376
|
+
delete <id> Delete an idea
|
|
377
|
+
|
|
378
|
+
Full documentation: https://wahlu.com/docs`
|
|
379
|
+
);
|
|
380
|
+
ideaCommand.command("list").description("List all ideas").option("--page <n>", "Page number (default: 1)", parseInt).option("--limit <n>", "Items per page (default: 50, max: 100)", parseInt).option("--json", "Output as JSON").addHelpText(
|
|
381
|
+
"after",
|
|
382
|
+
`
|
|
383
|
+
Returns a paginated list of content ideas.
|
|
384
|
+
|
|
385
|
+
Response fields:
|
|
386
|
+
id string Idea ID
|
|
387
|
+
name string|null Idea name/title
|
|
388
|
+
description string|null Detailed description
|
|
389
|
+
type string|null Idea type
|
|
390
|
+
status string Status
|
|
391
|
+
labels string[] Text labels
|
|
392
|
+
last_used_at string|null Last used timestamp
|
|
393
|
+
created_at string ISO 8601 timestamp
|
|
394
|
+
updated_at string ISO 8601 timestamp
|
|
395
|
+
|
|
396
|
+
Examples:
|
|
397
|
+
wahlu idea list
|
|
398
|
+
wahlu idea list --limit 10 --json`
|
|
399
|
+
).action(async function(opts) {
|
|
400
|
+
const brandId = resolveBrandId(this);
|
|
401
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
402
|
+
const res = await client.list(
|
|
403
|
+
`/brands/${brandId}/ideas`,
|
|
404
|
+
opts.page,
|
|
405
|
+
opts.limit
|
|
406
|
+
);
|
|
407
|
+
output(res.data, {
|
|
408
|
+
json: opts.json,
|
|
409
|
+
columns: [
|
|
410
|
+
{ key: "id", header: "ID", width: 24 },
|
|
411
|
+
{ key: "name", header: "Name", width: 40 },
|
|
412
|
+
{ key: "status", header: "Status", width: 12 },
|
|
413
|
+
{ key: "type", header: "Type", width: 12 }
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
ideaCommand.command("create").description("Save a new content idea").argument("<name>", "Idea name/title (max 500 chars)").option("--description <text>", "Detailed description (max 10000 chars)").option("--type <type>", "Idea type (max 50 chars)").option("--json", "Output as JSON").addHelpText(
|
|
418
|
+
"after",
|
|
419
|
+
`
|
|
420
|
+
Saves a new content idea. Only the name is required.
|
|
421
|
+
|
|
422
|
+
Fields:
|
|
423
|
+
<name> string Idea name/title (required, max 500 chars)
|
|
424
|
+
--description string Detailed description (max 10000 chars)
|
|
425
|
+
--type string Idea type (max 50 chars)
|
|
426
|
+
|
|
427
|
+
Examples:
|
|
428
|
+
wahlu idea create "Blog about summer trends"
|
|
429
|
+
wahlu idea create "Product launch" --description "Announce the new feature" --type "campaign"
|
|
430
|
+
wahlu idea create "Quick tip series" --json`
|
|
431
|
+
).action(async function(name, opts) {
|
|
432
|
+
const brandId = resolveBrandId(this);
|
|
433
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
434
|
+
const body = { name };
|
|
435
|
+
if (opts.description) body.description = opts.description;
|
|
436
|
+
if (opts.type) body.type = opts.type;
|
|
437
|
+
const res = await client.post(`/brands/${brandId}/ideas`, body);
|
|
438
|
+
output(res.data, { json: opts.json });
|
|
439
|
+
});
|
|
440
|
+
ideaCommand.command("delete").description("Delete an idea").argument("<idea-id>", "Idea ID").addHelpText(
|
|
441
|
+
"after",
|
|
442
|
+
`
|
|
443
|
+
Permanently deletes an idea. This cannot be undone.
|
|
444
|
+
|
|
445
|
+
Examples:
|
|
446
|
+
wahlu idea delete idea-abc123`
|
|
447
|
+
).action(async function(ideaId) {
|
|
448
|
+
const brandId = resolveBrandId(this);
|
|
449
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
450
|
+
await client.delete(`/brands/${brandId}/ideas/${ideaId}`);
|
|
451
|
+
console.log(`Idea ${ideaId} deleted.`);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// src/commands/integration.ts
|
|
455
|
+
import { Command as Command4 } from "commander";
|
|
456
|
+
var integrationCommand = new Command4("integration").description("View connected social media accounts (read-only)").addHelpText(
|
|
457
|
+
"after",
|
|
458
|
+
`
|
|
459
|
+
Integrations are connected social media accounts. You need integration IDs
|
|
460
|
+
when scheduling posts (wahlu schedule create --integrations <id>).
|
|
461
|
+
|
|
462
|
+
Subcommands:
|
|
463
|
+
list List all connected integrations
|
|
464
|
+
|
|
465
|
+
Integrations are connected and managed in the Wahlu web app.
|
|
466
|
+
This command is read-only \u2014 you cannot connect or disconnect accounts via the CLI.
|
|
467
|
+
|
|
468
|
+
Full documentation: https://wahlu.com/docs`
|
|
469
|
+
);
|
|
470
|
+
integrationCommand.command("list").description("List connected integrations").option("--json", "Output as JSON").addHelpText(
|
|
471
|
+
"after",
|
|
472
|
+
`
|
|
473
|
+
Returns all connected integrations for the brand (no pagination).
|
|
474
|
+
Credentials (tokens, secrets) are never exposed.
|
|
475
|
+
|
|
476
|
+
Response fields:
|
|
477
|
+
id string Integration ID (use in --integrations when scheduling)
|
|
478
|
+
platform string Platform: "instagram" | "tiktok" | "facebook" | "youtube" | "linkedin"
|
|
479
|
+
status string Connection status
|
|
480
|
+
brand_id string Brand ID
|
|
481
|
+
display_name string|null Display name on the platform
|
|
482
|
+
username string|null Platform username/handle
|
|
483
|
+
avatar_url string|null Profile avatar URL
|
|
484
|
+
permissions object|null Granted permissions
|
|
485
|
+
created_at string ISO 8601 timestamp
|
|
486
|
+
updated_at string ISO 8601 timestamp
|
|
487
|
+
|
|
488
|
+
Examples:
|
|
489
|
+
wahlu integration list
|
|
490
|
+
wahlu integration list --json
|
|
491
|
+
wahlu integration list --json | jq '.[] | {id, platform, username}'`
|
|
492
|
+
).action(async function(opts) {
|
|
493
|
+
const brandId = resolveBrandId(this);
|
|
494
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
495
|
+
const res = await client.get(
|
|
496
|
+
`/brands/${brandId}/integrations`
|
|
497
|
+
);
|
|
498
|
+
output(res.data, {
|
|
499
|
+
json: opts.json,
|
|
500
|
+
columns: [
|
|
501
|
+
{ key: "id", header: "ID", width: 24 },
|
|
502
|
+
{ key: "platform", header: "Platform", width: 12 },
|
|
503
|
+
{ key: "username", header: "Username", width: 20 },
|
|
504
|
+
{ key: "status", header: "Status", width: 12 }
|
|
505
|
+
]
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// src/commands/label.ts
|
|
510
|
+
import { Command as Command5 } from "commander";
|
|
511
|
+
var labelCommand = new Command5("label").description("Create and manage labels for organising posts").addHelpText(
|
|
512
|
+
"after",
|
|
513
|
+
`
|
|
514
|
+
Labels are used to categorise and organise posts and media.
|
|
515
|
+
|
|
516
|
+
Subcommands:
|
|
517
|
+
list List all labels
|
|
518
|
+
create <name> Create a new label
|
|
519
|
+
delete <id> Delete a label
|
|
520
|
+
|
|
521
|
+
Labels can be attached to posts:
|
|
522
|
+
wahlu post create --name "My post" --labels label-1 label-2
|
|
523
|
+
|
|
524
|
+
Full documentation: https://wahlu.com/docs`
|
|
525
|
+
);
|
|
526
|
+
labelCommand.command("list").description("List all labels").option("--json", "Output as JSON").addHelpText(
|
|
527
|
+
"after",
|
|
528
|
+
`
|
|
529
|
+
Returns all labels for the brand (no pagination \u2014 returns all at once).
|
|
530
|
+
|
|
531
|
+
Response fields:
|
|
532
|
+
id string Label ID
|
|
533
|
+
name string Label name
|
|
534
|
+
color string|null Colour hex code (e.g. "#ff5500")
|
|
535
|
+
created_at string ISO 8601 timestamp
|
|
536
|
+
updated_at string ISO 8601 timestamp
|
|
537
|
+
|
|
538
|
+
Examples:
|
|
539
|
+
wahlu label list
|
|
540
|
+
wahlu label list --json`
|
|
541
|
+
).action(async function(opts) {
|
|
542
|
+
const brandId = resolveBrandId(this);
|
|
543
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
544
|
+
const res = await client.get(`/brands/${brandId}/labels`);
|
|
545
|
+
output(res.data, {
|
|
546
|
+
json: opts.json,
|
|
547
|
+
columns: [
|
|
548
|
+
{ key: "id", header: "ID", width: 24 },
|
|
549
|
+
{ key: "name", header: "Name", width: 30 },
|
|
550
|
+
{ key: "color", header: "Colour", width: 10 }
|
|
551
|
+
]
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
labelCommand.command("create").description("Create a label").argument("<name>", "Label name (required, max 100 chars)").option("--color <hex>", 'Colour hex code (e.g. "#ff5500", max 20 chars)').option("--json", "Output as JSON").addHelpText(
|
|
555
|
+
"after",
|
|
556
|
+
`
|
|
557
|
+
Creates a new label for organising posts and media.
|
|
558
|
+
|
|
559
|
+
Fields:
|
|
560
|
+
<name> string Label name (required, max 100 chars)
|
|
561
|
+
--color string Colour hex code (optional, max 20 chars)
|
|
562
|
+
|
|
563
|
+
Examples:
|
|
564
|
+
wahlu label create "Urgent"
|
|
565
|
+
wahlu label create "Promo" --color "#ff5500"
|
|
566
|
+
wahlu label create "Campaign Q1" --json`
|
|
567
|
+
).action(async function(name, opts) {
|
|
568
|
+
const brandId = resolveBrandId(this);
|
|
569
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
570
|
+
const body = { name };
|
|
571
|
+
if (opts.color) body.color = opts.color;
|
|
572
|
+
const res = await client.post(`/brands/${brandId}/labels`, body);
|
|
573
|
+
output(res.data, { json: opts.json });
|
|
574
|
+
});
|
|
575
|
+
labelCommand.command("delete").description("Delete a label").argument("<label-id>", "Label ID").addHelpText(
|
|
576
|
+
"after",
|
|
577
|
+
`
|
|
578
|
+
Permanently deletes a label. This does not remove the label from
|
|
579
|
+
posts that already have it attached.
|
|
580
|
+
|
|
581
|
+
Examples:
|
|
582
|
+
wahlu label delete label-abc123`
|
|
583
|
+
).action(async function(labelId) {
|
|
584
|
+
const brandId = resolveBrandId(this);
|
|
585
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
586
|
+
await client.delete(`/brands/${brandId}/labels/${labelId}`);
|
|
587
|
+
console.log(`Label ${labelId} deleted.`);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// src/commands/media.ts
|
|
591
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
592
|
+
import { basename } from "path";
|
|
593
|
+
import { Command as Command6 } from "commander";
|
|
594
|
+
var MIME_TYPES = {
|
|
595
|
+
".png": "image/png",
|
|
596
|
+
".jpg": "image/jpeg",
|
|
597
|
+
".jpeg": "image/jpeg",
|
|
598
|
+
".gif": "image/gif",
|
|
599
|
+
".webp": "image/webp",
|
|
600
|
+
".mp4": "video/mp4",
|
|
601
|
+
".mov": "video/quicktime",
|
|
602
|
+
".webm": "video/webm"
|
|
603
|
+
};
|
|
604
|
+
var mediaCommand = new Command6("media").description("Upload images/videos and manage your media library").addHelpText(
|
|
605
|
+
"after",
|
|
606
|
+
`
|
|
607
|
+
Media files (images and videos) that can be attached to posts via media_ids
|
|
608
|
+
in platform settings.
|
|
609
|
+
|
|
610
|
+
Subcommands:
|
|
611
|
+
list List all media files in the library
|
|
612
|
+
upload <file> Upload a local image or video file
|
|
613
|
+
delete <id> Permanently delete a media file
|
|
614
|
+
|
|
615
|
+
Supported formats:
|
|
616
|
+
Images: .png, .jpg, .jpeg, .gif, .webp
|
|
617
|
+
Videos: .mp4, .mov, .webm
|
|
618
|
+
|
|
619
|
+
Typical workflow:
|
|
620
|
+
1. Upload: wahlu media upload ./photo.jpg
|
|
621
|
+
2. Get the media ID from the output
|
|
622
|
+
3. Use in a post: wahlu post create --name "Photo post" \\
|
|
623
|
+
--instagram '{"description":"Nice!","post_type":"grid_post","media_ids":["<media-id>"]}'
|
|
624
|
+
|
|
625
|
+
Full documentation: https://wahlu.com/docs`
|
|
626
|
+
);
|
|
627
|
+
mediaCommand.command("list").description("List media files").option("--page <n>", "Page number (default: 1)", parseInt).option("--limit <n>", "Items per page (default: 50, max: 100)", parseInt).option("--json", "Output as JSON").addHelpText(
|
|
628
|
+
"after",
|
|
629
|
+
`
|
|
630
|
+
Returns a paginated list of media files for the brand.
|
|
631
|
+
|
|
632
|
+
Response fields:
|
|
633
|
+
id string Media ID (use in media_ids arrays)
|
|
634
|
+
file_name string Original filename
|
|
635
|
+
content_type string MIME type (e.g. "image/jpeg", "video/mp4")
|
|
636
|
+
size number File size in bytes
|
|
637
|
+
duration number|null Duration in seconds (video/audio only)
|
|
638
|
+
status string Processing status: "available" | "processing" | "completed" | "failed"
|
|
639
|
+
workflow_status string|null Normalised lifecycle status
|
|
640
|
+
label_ids string[] Attached label IDs
|
|
641
|
+
folder_id string|null Folder ID
|
|
642
|
+
download_url string|null Signed download URL
|
|
643
|
+
thumbnail_large_url string|null Large thumbnail URL
|
|
644
|
+
thumbnail_small_url string|null Small thumbnail URL
|
|
645
|
+
source string|null Upload source: "upload" | "generated" | "stock" | "scan"
|
|
646
|
+
description string|null Media description
|
|
647
|
+
last_used_at string|null Last used in a post
|
|
648
|
+
created_at string ISO 8601 timestamp
|
|
649
|
+
updated_at string ISO 8601 timestamp
|
|
650
|
+
|
|
651
|
+
Examples:
|
|
652
|
+
wahlu media list
|
|
653
|
+
wahlu media list --limit 10 --json`
|
|
654
|
+
).action(async function(opts) {
|
|
655
|
+
const brandId = resolveBrandId(this);
|
|
656
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
657
|
+
const res = await client.list(
|
|
658
|
+
`/brands/${brandId}/media`,
|
|
659
|
+
opts.page,
|
|
660
|
+
opts.limit
|
|
661
|
+
);
|
|
662
|
+
output(res.data, {
|
|
663
|
+
json: opts.json,
|
|
664
|
+
columns: [
|
|
665
|
+
{ key: "id", header: "ID", width: 24 },
|
|
666
|
+
{ key: "file_name", header: "Filename", width: 30 },
|
|
667
|
+
{ key: "content_type", header: "Type", width: 16 },
|
|
668
|
+
{ key: "status", header: "Status", width: 12 },
|
|
669
|
+
{
|
|
670
|
+
key: "size",
|
|
671
|
+
header: "Size",
|
|
672
|
+
width: 10,
|
|
673
|
+
transform: (v) => {
|
|
674
|
+
if (!v) return "-";
|
|
675
|
+
const bytes = v;
|
|
676
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
677
|
+
if (bytes < 1024 * 1024)
|
|
678
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
679
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
]
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
mediaCommand.command("upload").description("Upload a local file to the media library").argument("<file>", "Local file path (e.g. ./photo.jpg, ~/videos/clip.mp4)").option("--json", "Output as JSON (returns media ID)").addHelpText(
|
|
686
|
+
"after",
|
|
687
|
+
`
|
|
688
|
+
Uploads a local file to your brand's media library. The upload is a 3-step
|
|
689
|
+
process handled automatically:
|
|
690
|
+
1. Request a signed upload URL from the API
|
|
691
|
+
2. Upload the file bytes to the signed URL
|
|
692
|
+
3. Mark the media as available for processing
|
|
693
|
+
|
|
694
|
+
After upload, Wahlu processes the media (generates thumbnails, etc.).
|
|
695
|
+
Use 'wahlu media list' to check when status changes to "completed".
|
|
696
|
+
|
|
697
|
+
The returned media ID can be used in post platform settings:
|
|
698
|
+
--instagram '{"media_ids":["<media-id>"],...}'
|
|
699
|
+
|
|
700
|
+
Examples:
|
|
701
|
+
wahlu media upload ./photo.jpg
|
|
702
|
+
wahlu media upload ~/Desktop/video.mp4 --json`
|
|
703
|
+
).action(async function(filePath, opts) {
|
|
704
|
+
const brandId = resolveBrandId(this);
|
|
705
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
706
|
+
const filename = basename(filePath);
|
|
707
|
+
const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
|
|
708
|
+
const contentType = MIME_TYPES[ext];
|
|
709
|
+
if (!contentType) {
|
|
710
|
+
console.error(
|
|
711
|
+
`Unsupported file type: ${ext}
|
|
712
|
+
Supported: ${Object.keys(MIME_TYPES).join(", ")}`
|
|
713
|
+
);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
const stat = statSync(filePath);
|
|
717
|
+
const urlRes = await client.post(`/brands/${brandId}/media/upload-url`, {
|
|
718
|
+
filename,
|
|
719
|
+
content_type: contentType,
|
|
720
|
+
size: stat.size
|
|
721
|
+
});
|
|
722
|
+
const { id, upload_url } = urlRes.data;
|
|
723
|
+
const fileBytes = readFileSync2(filePath);
|
|
724
|
+
const uploadRes = await fetch(upload_url, {
|
|
725
|
+
method: "PUT",
|
|
726
|
+
headers: { "Content-Type": contentType },
|
|
727
|
+
body: fileBytes
|
|
728
|
+
});
|
|
729
|
+
if (!uploadRes.ok) {
|
|
730
|
+
console.error(`Upload failed: HTTP ${uploadRes.status}`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
await client.patch(`/brands/${brandId}/media/${id}`, {
|
|
734
|
+
status: "available"
|
|
735
|
+
});
|
|
736
|
+
if (opts.json) {
|
|
737
|
+
console.log(
|
|
738
|
+
JSON.stringify({ id, filename, status: "processing" }, null, 2)
|
|
739
|
+
);
|
|
740
|
+
} else {
|
|
741
|
+
console.log(`Uploaded ${filename} \u2014 media ID: ${id}`);
|
|
742
|
+
console.log(
|
|
743
|
+
"Processing will complete shortly. Use 'wahlu media list' to check status."
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
mediaCommand.command("delete").description("Permanently delete a media file").argument("<media-id>", "Media ID").addHelpText(
|
|
748
|
+
"after",
|
|
749
|
+
`
|
|
750
|
+
Permanently deletes a media file. This cannot be undone.
|
|
751
|
+
|
|
752
|
+
Examples:
|
|
753
|
+
wahlu media delete mid-abc123`
|
|
754
|
+
).action(async function(mediaId) {
|
|
755
|
+
const brandId = resolveBrandId(this);
|
|
756
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
757
|
+
await client.delete(`/brands/${brandId}/media/${mediaId}`);
|
|
758
|
+
console.log(`Media ${mediaId} deleted.`);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// src/commands/post.ts
|
|
762
|
+
import { Command as Command7 } from "commander";
|
|
763
|
+
var postCommand = new Command7("post").description("Create, update, list, and delete posts with per-platform settings").addHelpText(
|
|
764
|
+
"after",
|
|
765
|
+
`
|
|
766
|
+
Posts are the core content unit in Wahlu. Each post can have platform-specific
|
|
767
|
+
settings for Instagram, TikTok, Facebook, YouTube, and LinkedIn.
|
|
768
|
+
|
|
769
|
+
Subcommands:
|
|
770
|
+
list List all posts for the brand
|
|
771
|
+
get <id> View full post details including platform settings
|
|
772
|
+
create Create a new post with optional platform settings
|
|
773
|
+
update <id> Update an existing post (only provided fields change)
|
|
774
|
+
delete <id> Permanently delete a post
|
|
775
|
+
|
|
776
|
+
Typical workflow:
|
|
777
|
+
1. Upload media: wahlu media upload ./photo.jpg
|
|
778
|
+
2. Create a post: wahlu post create --name "My post" --instagram '...'
|
|
779
|
+
3. Schedule or queue: wahlu schedule create <post-id> --at <datetime> --integrations <id>
|
|
780
|
+
|
|
781
|
+
Run 'wahlu post create --help' for full platform settings reference.
|
|
782
|
+
Full documentation: https://wahlu.com/docs`
|
|
783
|
+
);
|
|
784
|
+
postCommand.command("list").description("List posts").option("--page <n>", "Page number (default: 1)", parseInt).option("--limit <n>", "Items per page (default: 50, max: 100)", parseInt).option("--json", "Output as JSON").addHelpText(
|
|
785
|
+
"after",
|
|
786
|
+
`
|
|
787
|
+
Returns a paginated list of posts for the brand.
|
|
788
|
+
|
|
789
|
+
Response fields (per post):
|
|
790
|
+
id string Post ID
|
|
791
|
+
name string|null Post name
|
|
792
|
+
brand_id string Brand ID
|
|
793
|
+
label_ids string[] Attached label IDs
|
|
794
|
+
created_by string|null Creator user ID
|
|
795
|
+
thumbnail_timestamp number Thumbnail timestamp (seconds)
|
|
796
|
+
instagram_settings object|null Instagram configuration
|
|
797
|
+
tiktok_settings object|null TikTok configuration
|
|
798
|
+
facebook_settings object|null Facebook configuration
|
|
799
|
+
youtube_settings object|null YouTube configuration
|
|
800
|
+
linkedin_settings object|null LinkedIn configuration
|
|
801
|
+
created_at string ISO 8601 timestamp
|
|
802
|
+
updated_at string ISO 8601 timestamp
|
|
803
|
+
|
|
804
|
+
Examples:
|
|
805
|
+
wahlu post list
|
|
806
|
+
wahlu post list --limit 10 --page 2
|
|
807
|
+
wahlu post list --json | jq '.[].name'`
|
|
808
|
+
).action(async function(opts) {
|
|
809
|
+
const brandId = resolveBrandId(this);
|
|
810
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
811
|
+
const res = await client.list(
|
|
812
|
+
`/brands/${brandId}/posts`,
|
|
813
|
+
opts.page,
|
|
814
|
+
opts.limit
|
|
815
|
+
);
|
|
816
|
+
output(res.data, {
|
|
817
|
+
json: opts.json,
|
|
818
|
+
columns: [
|
|
819
|
+
{ key: "id", header: "ID", width: 24 },
|
|
820
|
+
{ key: "name", header: "Name", width: 40 },
|
|
821
|
+
{
|
|
822
|
+
key: "created_at",
|
|
823
|
+
header: "Created",
|
|
824
|
+
width: 20,
|
|
825
|
+
transform: (v) => v ? new Date(v).toLocaleDateString() : "-"
|
|
826
|
+
}
|
|
827
|
+
]
|
|
828
|
+
});
|
|
829
|
+
if (!opts.json && res.pagination?.has_more) {
|
|
830
|
+
console.log(
|
|
831
|
+
`
|
|
832
|
+
Page ${res.pagination.page} \u2014 more results available (--page ${res.pagination.page + 1})`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
postCommand.command("get").description("Get post details").argument("<post-id>", "Post ID").option("--json", "Output as JSON").addHelpText(
|
|
837
|
+
"after",
|
|
838
|
+
`
|
|
839
|
+
Returns full details for a single post including all platform settings.
|
|
840
|
+
|
|
841
|
+
Examples:
|
|
842
|
+
wahlu post get abc123
|
|
843
|
+
wahlu post get abc123 --json`
|
|
844
|
+
).action(async function(postId, opts) {
|
|
845
|
+
const brandId = resolveBrandId(this);
|
|
846
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
847
|
+
const res = await client.get(`/brands/${brandId}/posts/${postId}`);
|
|
848
|
+
output(res.data, { json: opts.json });
|
|
849
|
+
});
|
|
850
|
+
postCommand.command("create").description("Create a new post").option("--name <name>", "Post name (max 500 chars)").option("--instagram <json>", "Instagram settings as JSON string").option("--tiktok <json>", "TikTok settings as JSON string").option("--facebook <json>", "Facebook settings as JSON string").option("--youtube <json>", "YouTube settings as JSON string").option("--linkedin <json>", "LinkedIn settings as JSON string").option("--labels <ids...>", "Label IDs to attach (max 50)").option("--json", "Output as JSON").addHelpText(
|
|
851
|
+
"after",
|
|
852
|
+
`
|
|
853
|
+
Creates a new post with optional platform-specific settings. You can target
|
|
854
|
+
multiple platforms in a single post by providing multiple --<platform> flags.
|
|
855
|
+
|
|
856
|
+
Examples:
|
|
857
|
+
wahlu post create --name "Monday post" \\
|
|
858
|
+
--instagram '{"description":"Hello!","post_type":"grid_post"}'
|
|
859
|
+
|
|
860
|
+
wahlu post create --name "Cross-platform video" \\
|
|
861
|
+
--tiktok '{"description":"Check this out","post_type":"video","media_ids":["mid-123"]}' \\
|
|
862
|
+
--instagram '{"description":"Check this out","post_type":"reel","media_ids":["mid-123"]}'
|
|
863
|
+
|
|
864
|
+
wahlu post create --name "Article share" \\
|
|
865
|
+
--linkedin '{"description":"Read our latest post","post_type":"li_article","original_url":"https://example.com/post","title":"Our Latest Post"}'
|
|
866
|
+
|
|
867
|
+
Platform settings reference:
|
|
868
|
+
|
|
869
|
+
Instagram (--instagram):
|
|
870
|
+
Field Type Values / Description
|
|
871
|
+
description string Caption text
|
|
872
|
+
post_type string "grid_post" | "reel" | "story"
|
|
873
|
+
media_ids string[] Media IDs to attach
|
|
874
|
+
trial_reel boolean Post as trial reel (shown to non-followers first)
|
|
875
|
+
graduation_strategy string "MANUAL" | "SS_PERFORMANCE" (auto-graduate trial reels)
|
|
876
|
+
|
|
877
|
+
TikTok (--tiktok):
|
|
878
|
+
Field Type Values / Description
|
|
879
|
+
description string Caption text
|
|
880
|
+
post_type string "video" | "image" | "carousel"
|
|
881
|
+
media_ids string[] Media IDs to attach
|
|
882
|
+
privacy_level string "PUBLIC_TO_EVERYONE" | "MUTUAL_FOLLOW_FRIENDS" | "FOLLOWER_OF_CREATOR" | "SELF_ONLY"
|
|
883
|
+
allow_comment boolean Allow comments (default: true)
|
|
884
|
+
allow_duet boolean Allow duets (default: true, video only)
|
|
885
|
+
allow_stitch boolean Allow stitches (default: true, video only)
|
|
886
|
+
auto_add_music boolean Auto-add music (photo/carousel only)
|
|
887
|
+
is_aigc boolean Disclose as AI-generated content
|
|
888
|
+
is_commercial_content boolean Mark as commercial/branded content
|
|
889
|
+
|
|
890
|
+
Facebook (--facebook):
|
|
891
|
+
Field Type Values / Description
|
|
892
|
+
description string Caption text
|
|
893
|
+
post_type string "fb_post" | "fb_story" | "fb_reel" | "fb_text"
|
|
894
|
+
media_ids string[] Media IDs to attach
|
|
895
|
+
|
|
896
|
+
YouTube (--youtube):
|
|
897
|
+
Field Type Values / Description
|
|
898
|
+
title string Video title
|
|
899
|
+
description string Video description
|
|
900
|
+
post_type string "yt_short" | "yt_video"
|
|
901
|
+
media_ids string[] Media IDs to attach
|
|
902
|
+
privacy_level string "public" | "unlisted" | "private"
|
|
903
|
+
notify_subscribers boolean Notify subscribers on publish
|
|
904
|
+
|
|
905
|
+
LinkedIn (--linkedin):
|
|
906
|
+
Field Type Values / Description
|
|
907
|
+
description string Post text
|
|
908
|
+
post_type string "li_text" | "li_image" | "li_video" | "li_article"
|
|
909
|
+
media_ids string[] Media IDs to attach
|
|
910
|
+
visibility string "PUBLIC" | "CONNECTIONS"
|
|
911
|
+
title string Article title (li_article only)
|
|
912
|
+
original_url string Article URL (li_article only)
|
|
913
|
+
|
|
914
|
+
Full documentation: https://wahlu.com/docs`
|
|
915
|
+
).action(async function(opts) {
|
|
916
|
+
const brandId = resolveBrandId(this);
|
|
917
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
918
|
+
const body = {};
|
|
919
|
+
if (opts.name) body.name = opts.name;
|
|
920
|
+
if (opts.labels) body.label_ids = opts.labels;
|
|
921
|
+
if (opts.instagram)
|
|
922
|
+
body.instagram_settings = JSON.parse(opts.instagram);
|
|
923
|
+
if (opts.tiktok) body.tiktok_settings = JSON.parse(opts.tiktok);
|
|
924
|
+
if (opts.facebook)
|
|
925
|
+
body.facebook_settings = JSON.parse(opts.facebook);
|
|
926
|
+
if (opts.youtube) body.youtube_settings = JSON.parse(opts.youtube);
|
|
927
|
+
if (opts.linkedin)
|
|
928
|
+
body.linkedin_settings = JSON.parse(opts.linkedin);
|
|
929
|
+
const res = await client.post(`/brands/${brandId}/posts`, body);
|
|
930
|
+
output(res.data, { json: opts.json });
|
|
931
|
+
});
|
|
932
|
+
postCommand.command("update").description("Update a post (only provided fields are changed)").argument("<post-id>", "Post ID").option("--name <name>", "Post name (max 500 chars)").option("--instagram <json>", "Instagram settings as JSON string").option("--tiktok <json>", "TikTok settings as JSON string").option("--facebook <json>", "Facebook settings as JSON string").option("--youtube <json>", "YouTube settings as JSON string").option("--linkedin <json>", "LinkedIn settings as JSON string").option("--labels <ids...>", "Label IDs to attach (max 50)").option("--json", "Output as JSON").addHelpText(
|
|
933
|
+
"after",
|
|
934
|
+
`
|
|
935
|
+
Updates an existing post. Only the fields you provide are changed \u2014
|
|
936
|
+
omitted fields remain unchanged.
|
|
937
|
+
|
|
938
|
+
Examples:
|
|
939
|
+
wahlu post update abc123 --name "New name"
|
|
940
|
+
wahlu post update abc123 --instagram '{"description":"Updated caption"}'
|
|
941
|
+
wahlu post update abc123 --labels label-1 label-2
|
|
942
|
+
|
|
943
|
+
See 'wahlu post create --help' for full platform settings reference.
|
|
944
|
+
Full documentation: https://wahlu.com/docs`
|
|
945
|
+
).action(async function(postId, opts) {
|
|
946
|
+
const brandId = resolveBrandId(this);
|
|
947
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
948
|
+
const body = {};
|
|
949
|
+
if (opts.name) body.name = opts.name;
|
|
950
|
+
if (opts.labels) body.label_ids = opts.labels;
|
|
951
|
+
if (opts.instagram)
|
|
952
|
+
body.instagram_settings = JSON.parse(opts.instagram);
|
|
953
|
+
if (opts.tiktok) body.tiktok_settings = JSON.parse(opts.tiktok);
|
|
954
|
+
if (opts.facebook)
|
|
955
|
+
body.facebook_settings = JSON.parse(opts.facebook);
|
|
956
|
+
if (opts.youtube) body.youtube_settings = JSON.parse(opts.youtube);
|
|
957
|
+
if (opts.linkedin)
|
|
958
|
+
body.linkedin_settings = JSON.parse(opts.linkedin);
|
|
959
|
+
const res = await client.patch(
|
|
960
|
+
`/brands/${brandId}/posts/${postId}`,
|
|
961
|
+
body
|
|
962
|
+
);
|
|
963
|
+
output(res.data, { json: opts.json });
|
|
964
|
+
});
|
|
965
|
+
postCommand.command("delete").description("Permanently delete a post").argument("<post-id>", "Post ID").addHelpText(
|
|
966
|
+
"after",
|
|
967
|
+
`
|
|
968
|
+
Permanently deletes a post. This cannot be undone.
|
|
969
|
+
|
|
970
|
+
Examples:
|
|
971
|
+
wahlu post delete abc123`
|
|
972
|
+
).action(async function(postId) {
|
|
973
|
+
const brandId = resolveBrandId(this);
|
|
974
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
975
|
+
await client.delete(`/brands/${brandId}/posts/${postId}`);
|
|
976
|
+
console.log(`Post ${postId} deleted.`);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
// src/commands/publication.ts
|
|
980
|
+
import { Command as Command8 } from "commander";
|
|
981
|
+
var publicationCommand = new Command8("publication").description("View posts that have been published to platforms (read-only)").addHelpText(
|
|
982
|
+
"after",
|
|
983
|
+
`
|
|
984
|
+
Publications are records of posts that have been successfully published
|
|
985
|
+
to social media platforms. This is a read-only view of your publishing history.
|
|
986
|
+
|
|
987
|
+
Subcommands:
|
|
988
|
+
list List all publications
|
|
989
|
+
|
|
990
|
+
Full documentation: https://wahlu.com/docs`
|
|
991
|
+
);
|
|
992
|
+
publicationCommand.command("list").description("List published posts").option("--page <n>", "Page number (default: 1)", parseInt).option("--limit <n>", "Items per page (default: 50, max: 100)", parseInt).option("--json", "Output as JSON").addHelpText(
|
|
993
|
+
"after",
|
|
994
|
+
`
|
|
995
|
+
Returns a paginated list of published posts.
|
|
996
|
+
|
|
997
|
+
Response fields:
|
|
998
|
+
id string Publication ID
|
|
999
|
+
platform string Platform: "instagram" | "tiktok" | "facebook" | "youtube" | "linkedin"
|
|
1000
|
+
post_id string Source post ID
|
|
1001
|
+
post_name string|null Post name
|
|
1002
|
+
post_type string|null Post type (e.g. "grid_post", "reel", "video")
|
|
1003
|
+
media_type string|null Media type
|
|
1004
|
+
status string "processing" | "published" | "failed"
|
|
1005
|
+
source string|null "calendar" (from schedule) or "queue" (from queue)
|
|
1006
|
+
failure_reason string|null Failure reason if publishing failed
|
|
1007
|
+
integration_id string Integration used for publishing
|
|
1008
|
+
publish_id string|null Platform-specific content ID
|
|
1009
|
+
published_at string When it was published (ISO 8601)
|
|
1010
|
+
created_at string ISO 8601 timestamp
|
|
1011
|
+
updated_at string ISO 8601 timestamp
|
|
1012
|
+
|
|
1013
|
+
Examples:
|
|
1014
|
+
wahlu publication list
|
|
1015
|
+
wahlu publication list --limit 10 --json
|
|
1016
|
+
wahlu publication list --json | jq '.[] | select(.status == "failed")'`
|
|
1017
|
+
).action(async function(opts) {
|
|
1018
|
+
const brandId = resolveBrandId(this);
|
|
1019
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
1020
|
+
const res = await client.list(
|
|
1021
|
+
`/brands/${brandId}/publications`,
|
|
1022
|
+
opts.page,
|
|
1023
|
+
opts.limit
|
|
1024
|
+
);
|
|
1025
|
+
output(res.data, {
|
|
1026
|
+
json: opts.json,
|
|
1027
|
+
columns: [
|
|
1028
|
+
{ key: "id", header: "ID", width: 24 },
|
|
1029
|
+
{ key: "platform", header: "Platform", width: 12 },
|
|
1030
|
+
{ key: "post_name", header: "Post", width: 30 },
|
|
1031
|
+
{ key: "status", header: "Status", width: 12 },
|
|
1032
|
+
{
|
|
1033
|
+
key: "published_at",
|
|
1034
|
+
header: "Published",
|
|
1035
|
+
width: 20,
|
|
1036
|
+
transform: (v) => v ? new Date(v).toLocaleString() : "-"
|
|
1037
|
+
}
|
|
1038
|
+
]
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// src/commands/queue.ts
|
|
1043
|
+
import { Command as Command9 } from "commander";
|
|
1044
|
+
var queueCommand = new Command9("queue").description("View publishing queues and add posts to them").addHelpText(
|
|
1045
|
+
"after",
|
|
1046
|
+
`
|
|
1047
|
+
Queues define recurring time slots for automatic post publishing.
|
|
1048
|
+
Posts added to a queue are published at the next available slot.
|
|
1049
|
+
|
|
1050
|
+
Subcommands:
|
|
1051
|
+
list List all queues and their status
|
|
1052
|
+
add <queue-id> <post-id> Add a post to a queue
|
|
1053
|
+
|
|
1054
|
+
Queues are created and configured in the Wahlu web app.
|
|
1055
|
+
|
|
1056
|
+
Full documentation: https://wahlu.com/docs`
|
|
1057
|
+
);
|
|
1058
|
+
queueCommand.command("list").description("List all queues").option("--json", "Output as JSON").addHelpText(
|
|
1059
|
+
"after",
|
|
1060
|
+
`
|
|
1061
|
+
Returns all queues for the brand (no pagination \u2014 returns all at once).
|
|
1062
|
+
|
|
1063
|
+
Response fields:
|
|
1064
|
+
id string Queue ID
|
|
1065
|
+
name string Queue name
|
|
1066
|
+
brand_id string Brand ID
|
|
1067
|
+
active boolean Whether the queue is active
|
|
1068
|
+
mode string Queue mode
|
|
1069
|
+
interval number|null Interval between posts
|
|
1070
|
+
interval_unit string|null Interval unit
|
|
1071
|
+
times_of_day string[] Scheduled times (e.g. ["09:00", "17:00"])
|
|
1072
|
+
timezone string|null IANA timezone
|
|
1073
|
+
valid_from string|null ISO 8601 start date
|
|
1074
|
+
valid_until string|null ISO 8601 end date
|
|
1075
|
+
next_run_at string|null Next scheduled publishing time
|
|
1076
|
+
loop boolean Whether to loop through posts
|
|
1077
|
+
post_ids string[] Ordered list of post IDs in the queue
|
|
1078
|
+
integration_ids string[] Integration IDs to publish to
|
|
1079
|
+
skip_count number Number of posts skipped
|
|
1080
|
+
created_at string ISO 8601 timestamp
|
|
1081
|
+
updated_at string ISO 8601 timestamp
|
|
1082
|
+
|
|
1083
|
+
Examples:
|
|
1084
|
+
wahlu queue list
|
|
1085
|
+
wahlu queue list --json`
|
|
1086
|
+
).action(async function(opts) {
|
|
1087
|
+
const brandId = resolveBrandId(this);
|
|
1088
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
1089
|
+
const res = await client.get(`/brands/${brandId}/queues`);
|
|
1090
|
+
output(res.data, {
|
|
1091
|
+
json: opts.json,
|
|
1092
|
+
columns: [
|
|
1093
|
+
{ key: "id", header: "ID", width: 24 },
|
|
1094
|
+
{ key: "name", header: "Name", width: 30 },
|
|
1095
|
+
{
|
|
1096
|
+
key: "active",
|
|
1097
|
+
header: "Active",
|
|
1098
|
+
width: 8,
|
|
1099
|
+
transform: (v) => v ? "yes" : "no"
|
|
1100
|
+
},
|
|
1101
|
+
{ key: "mode", header: "Mode", width: 12 },
|
|
1102
|
+
{
|
|
1103
|
+
key: "next_run_at",
|
|
1104
|
+
header: "Next Run",
|
|
1105
|
+
width: 20,
|
|
1106
|
+
transform: (v) => v ? new Date(v).toLocaleString() : "-"
|
|
1107
|
+
}
|
|
1108
|
+
]
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
queueCommand.command("add").description("Add a post to a queue").argument("<queue-id>", "Queue ID").argument("<post-id>", "Post ID to add").option("--json", "Output as JSON").addHelpText(
|
|
1112
|
+
"after",
|
|
1113
|
+
`
|
|
1114
|
+
Adds a post to a queue. The post will be published at the queue's
|
|
1115
|
+
next available time slot.
|
|
1116
|
+
|
|
1117
|
+
Examples:
|
|
1118
|
+
wahlu queue add queue-abc post-xyz
|
|
1119
|
+
wahlu queue add queue-abc post-xyz --json`
|
|
1120
|
+
).action(async function(queueId, postId, opts) {
|
|
1121
|
+
const brandId = resolveBrandId(this);
|
|
1122
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
1123
|
+
const res = await client.patch(
|
|
1124
|
+
`/brands/${brandId}/queues/${queueId}`,
|
|
1125
|
+
{
|
|
1126
|
+
post_ids: [postId]
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
output(res.data, { json: opts.json });
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
// src/commands/schedule.ts
|
|
1133
|
+
import { Command as Command10 } from "commander";
|
|
1134
|
+
var scheduleCommand = new Command10("schedule").description("Schedule posts for future publishing to specific integrations").addHelpText(
|
|
1135
|
+
"after",
|
|
1136
|
+
`
|
|
1137
|
+
Scheduled posts are queued for publishing at a specific date and time
|
|
1138
|
+
to one or more connected social media integrations.
|
|
1139
|
+
|
|
1140
|
+
Subcommands:
|
|
1141
|
+
list List all scheduled posts
|
|
1142
|
+
create <post-id> Schedule a post for future publishing
|
|
1143
|
+
delete <id> Remove a post from the schedule (does not delete the post)
|
|
1144
|
+
|
|
1145
|
+
Typical workflow:
|
|
1146
|
+
1. Create a post: wahlu post create --name "My post" --instagram '...'
|
|
1147
|
+
2. Find integration IDs: wahlu integration list
|
|
1148
|
+
3. Schedule it: wahlu schedule create <post-id> --at <datetime> --integrations <id>
|
|
1149
|
+
|
|
1150
|
+
Full documentation: https://wahlu.com/docs`
|
|
1151
|
+
);
|
|
1152
|
+
scheduleCommand.command("list").description("List scheduled posts").option("--page <n>", "Page number (default: 1)", parseInt).option("--limit <n>", "Items per page (default: 50, max: 100)", parseInt).option("--json", "Output as JSON").addHelpText(
|
|
1153
|
+
"after",
|
|
1154
|
+
`
|
|
1155
|
+
Returns a paginated list of scheduled posts, sorted by scheduled_at.
|
|
1156
|
+
|
|
1157
|
+
Response fields:
|
|
1158
|
+
id string Scheduled post ID
|
|
1159
|
+
brand_id string Brand ID
|
|
1160
|
+
post_id string Referenced post ID
|
|
1161
|
+
scheduled_at string ISO 8601 datetime for publishing
|
|
1162
|
+
integration_ids string[] Integration IDs to publish to
|
|
1163
|
+
status string Status (e.g. "ready_for_publishing", "published", "failed")
|
|
1164
|
+
approval_status string|null Approval status
|
|
1165
|
+
source string|null "api" for API-created entries
|
|
1166
|
+
failure_reason string|null Failure reason if publishing failed
|
|
1167
|
+
thumbnail_url string|null Post thumbnail URL
|
|
1168
|
+
created_at string ISO 8601 timestamp
|
|
1169
|
+
updated_at string ISO 8601 timestamp
|
|
1170
|
+
|
|
1171
|
+
Examples:
|
|
1172
|
+
wahlu schedule list
|
|
1173
|
+
wahlu schedule list --limit 5 --json`
|
|
1174
|
+
).action(async function(opts) {
|
|
1175
|
+
const brandId = resolveBrandId(this);
|
|
1176
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
1177
|
+
const res = await client.list(
|
|
1178
|
+
`/brands/${brandId}/scheduled-posts`,
|
|
1179
|
+
opts.page,
|
|
1180
|
+
opts.limit
|
|
1181
|
+
);
|
|
1182
|
+
output(res.data, {
|
|
1183
|
+
json: opts.json,
|
|
1184
|
+
columns: [
|
|
1185
|
+
{ key: "id", header: "ID", width: 24 },
|
|
1186
|
+
{ key: "post_id", header: "Post ID", width: 24 },
|
|
1187
|
+
{ key: "status", header: "Status", width: 12 },
|
|
1188
|
+
{
|
|
1189
|
+
key: "scheduled_at",
|
|
1190
|
+
header: "Scheduled At",
|
|
1191
|
+
width: 20,
|
|
1192
|
+
transform: (v) => v ? new Date(v).toLocaleString() : "-"
|
|
1193
|
+
}
|
|
1194
|
+
]
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
scheduleCommand.command("create").description("Schedule a post for future publishing").argument("<post-id>", "Post ID to schedule (must exist in the brand)").requiredOption(
|
|
1198
|
+
"--at <datetime>",
|
|
1199
|
+
"ISO 8601 datetime (e.g. 2026-03-15T14:00:00Z)"
|
|
1200
|
+
).requiredOption(
|
|
1201
|
+
"--integrations <ids...>",
|
|
1202
|
+
"Integration IDs to publish to (max 20)"
|
|
1203
|
+
).option("--json", "Output as JSON").addHelpText(
|
|
1204
|
+
"after",
|
|
1205
|
+
`
|
|
1206
|
+
Schedules an existing post for future publishing. The post must already
|
|
1207
|
+
exist in the brand, and the integration IDs must be connected accounts.
|
|
1208
|
+
|
|
1209
|
+
Required fields:
|
|
1210
|
+
<post-id> The ID of an existing post in this brand
|
|
1211
|
+
--at <datetime> ISO 8601 datetime (e.g. "2026-03-15T14:00:00Z")
|
|
1212
|
+
--integrations <ids> Space-separated integration IDs
|
|
1213
|
+
|
|
1214
|
+
Find integration IDs with: wahlu integration list
|
|
1215
|
+
|
|
1216
|
+
Examples:
|
|
1217
|
+
wahlu schedule create post-abc \\
|
|
1218
|
+
--at 2026-03-15T14:00:00Z \\
|
|
1219
|
+
--integrations int-123
|
|
1220
|
+
|
|
1221
|
+
wahlu schedule create post-abc \\
|
|
1222
|
+
--at 2026-03-15T14:00:00Z \\
|
|
1223
|
+
--integrations int-123 int-456 int-789 \\
|
|
1224
|
+
--json`
|
|
1225
|
+
).action(async function(postId, opts) {
|
|
1226
|
+
const brandId = resolveBrandId(this);
|
|
1227
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
1228
|
+
const res = await client.post(`/brands/${brandId}/scheduled-posts`, {
|
|
1229
|
+
post_id: postId,
|
|
1230
|
+
scheduled_at: opts.at,
|
|
1231
|
+
integration_ids: opts.integrations
|
|
1232
|
+
});
|
|
1233
|
+
output(res.data, { json: opts.json });
|
|
1234
|
+
});
|
|
1235
|
+
scheduleCommand.command("delete").description("Remove a post from the schedule").argument("<scheduled-post-id>", "Scheduled post ID (not the post ID)").addHelpText(
|
|
1236
|
+
"after",
|
|
1237
|
+
`
|
|
1238
|
+
Removes a scheduled post from the publishing queue. The post itself is
|
|
1239
|
+
not deleted \u2014 only the schedule entry is removed.
|
|
1240
|
+
|
|
1241
|
+
Examples:
|
|
1242
|
+
wahlu schedule delete sched-abc123`
|
|
1243
|
+
).action(async function(id) {
|
|
1244
|
+
const brandId = resolveBrandId(this);
|
|
1245
|
+
const client = new WahluClient(getApiKey(), getApiUrl());
|
|
1246
|
+
await client.delete(`/brands/${brandId}/scheduled-posts/${id}`);
|
|
1247
|
+
console.log(`Scheduled post ${id} removed.`);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
// src/index.ts
|
|
1251
|
+
var program = new Command11().name("wahlu").description("Wahlu CLI \u2014 manage your social media from the terminal").version("0.1.0").option("--brand <id>", "Brand ID (overrides default)").configureHelp({ sortSubcommands: true }).addHelpText(
|
|
1252
|
+
"after",
|
|
1253
|
+
`
|
|
1254
|
+
Getting started:
|
|
1255
|
+
1. Generate an API key at wahlu.com under Settings > API Keys
|
|
1256
|
+
2. wahlu auth login <your-api-key>
|
|
1257
|
+
3. wahlu brand list
|
|
1258
|
+
4. wahlu brand switch <brand-id>
|
|
1259
|
+
5. wahlu post list
|
|
1260
|
+
|
|
1261
|
+
All list/get/create/update commands support --json for machine-readable output.
|
|
1262
|
+
Run 'wahlu <command> --help' for detailed usage, fields, and examples.
|
|
1263
|
+
|
|
1264
|
+
Full documentation: https://wahlu.com/docs`
|
|
1265
|
+
);
|
|
1266
|
+
program.addCommand(authCommand);
|
|
1267
|
+
program.addCommand(brandCommand);
|
|
1268
|
+
program.addCommand(ideaCommand);
|
|
1269
|
+
program.addCommand(integrationCommand);
|
|
1270
|
+
program.addCommand(labelCommand);
|
|
1271
|
+
program.addCommand(mediaCommand);
|
|
1272
|
+
program.addCommand(postCommand);
|
|
1273
|
+
program.addCommand(publicationCommand);
|
|
1274
|
+
program.addCommand(queueCommand);
|
|
1275
|
+
program.addCommand(scheduleCommand);
|
|
1276
|
+
program.exitOverride();
|
|
1277
|
+
async function main() {
|
|
1278
|
+
try {
|
|
1279
|
+
await program.parseAsync(process.argv);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
if (err instanceof CliError) {
|
|
1282
|
+
console.error(`Error: ${err.message}`);
|
|
1283
|
+
process.exit(1);
|
|
1284
|
+
}
|
|
1285
|
+
if (err && typeof err === "object" && "code" in err && err.code === "commander.helpDisplayed") {
|
|
1286
|
+
process.exit(0);
|
|
1287
|
+
}
|
|
1288
|
+
if (err && typeof err === "object" && "code" in err && err.code === "commander.version") {
|
|
1289
|
+
process.exit(0);
|
|
1290
|
+
}
|
|
1291
|
+
throw err;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
main();
|