@inblog/cli 0.2.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/dist/bin/inblog.js +2416 -0
- package/dist/bin/inblog.js.map +1 -0
- package/dist/sdk/index.d.mts +514 -0
- package/dist/sdk/index.d.ts +514 -0
- package/dist/sdk/index.js +584 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/index.mjs +545 -0
- package/dist/sdk/index.mjs.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,2416 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// bin/inblog.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/commands/auth.ts
|
|
30
|
+
var import_prompts = require("@inquirer/prompts");
|
|
31
|
+
var import_open = __toESM(require("open"));
|
|
32
|
+
|
|
33
|
+
// src/utils/config.ts
|
|
34
|
+
var fs = __toESM(require("fs"));
|
|
35
|
+
var path = __toESM(require("path"));
|
|
36
|
+
var os = __toESM(require("os"));
|
|
37
|
+
var CONFIG_DIR = path.join(os.homedir(), ".config", "inblog");
|
|
38
|
+
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
39
|
+
var LOCAL_CONFIG_FILE = ".inblogrc.json";
|
|
40
|
+
function ensureConfigDir() {
|
|
41
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
42
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function getGlobalConfigPath() {
|
|
46
|
+
return CONFIG_FILE;
|
|
47
|
+
}
|
|
48
|
+
function getLocalConfigPath() {
|
|
49
|
+
const localPath = path.resolve(LOCAL_CONFIG_FILE);
|
|
50
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
51
|
+
}
|
|
52
|
+
function readGlobalConfig() {
|
|
53
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
56
|
+
} catch {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function readLocalConfig() {
|
|
61
|
+
const localPath = path.resolve(LOCAL_CONFIG_FILE);
|
|
62
|
+
if (!fs.existsSync(localPath)) return {};
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(localPath, "utf-8"));
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function readConfig() {
|
|
70
|
+
const global = readGlobalConfig();
|
|
71
|
+
const local = readLocalConfig();
|
|
72
|
+
return { ...global, ...local };
|
|
73
|
+
}
|
|
74
|
+
function writeGlobalConfig(config) {
|
|
75
|
+
ensureConfigDir();
|
|
76
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
77
|
+
}
|
|
78
|
+
function setGlobalConfigValue(key, value) {
|
|
79
|
+
const config = readGlobalConfig();
|
|
80
|
+
config[key] = value;
|
|
81
|
+
writeGlobalConfig(config);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/utils/output.ts
|
|
85
|
+
var import_chalk = __toESM(require("chalk"));
|
|
86
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
87
|
+
function printJson(data) {
|
|
88
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
89
|
+
}
|
|
90
|
+
function printTable(headers, rows) {
|
|
91
|
+
const table = new import_cli_table3.default({
|
|
92
|
+
head: headers.map((h) => import_chalk.default.cyan(h)),
|
|
93
|
+
style: { head: [], border: [] }
|
|
94
|
+
});
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
table.push(row.map((cell) => cell == null ? import_chalk.default.dim("\u2014") : String(cell)));
|
|
97
|
+
}
|
|
98
|
+
console.log(table.toString());
|
|
99
|
+
}
|
|
100
|
+
function printDetail(entries) {
|
|
101
|
+
const maxKeyLen = Math.max(...entries.map(([k]) => k.length));
|
|
102
|
+
for (const [key, value] of entries) {
|
|
103
|
+
const label = import_chalk.default.cyan(key.padEnd(maxKeyLen));
|
|
104
|
+
const val = value == null ? import_chalk.default.dim("\u2014") : String(value);
|
|
105
|
+
console.log(` ${label} ${val}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function printSuccess(message) {
|
|
109
|
+
console.log(import_chalk.default.green(message));
|
|
110
|
+
}
|
|
111
|
+
function printWarning(message) {
|
|
112
|
+
console.error(import_chalk.default.yellow(message));
|
|
113
|
+
}
|
|
114
|
+
function printError(message) {
|
|
115
|
+
console.error(import_chalk.default.red(message));
|
|
116
|
+
}
|
|
117
|
+
function truncate(str, maxLen = 50) {
|
|
118
|
+
if (!str) return "";
|
|
119
|
+
return str.length > maxLen ? str.slice(0, maxLen - 1) + "\u2026" : str;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/sdk/deserialize.ts
|
|
123
|
+
function resolveRelationship(rel, included) {
|
|
124
|
+
if (!rel.data) return null;
|
|
125
|
+
if (Array.isArray(rel.data)) {
|
|
126
|
+
return rel.data.map((ref2) => {
|
|
127
|
+
const found2 = included.find((r) => r.type === ref2.type && r.id === ref2.id);
|
|
128
|
+
return found2 ? flattenResource(found2, included) : { id: ref2.id, type: ref2.type };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const ref = rel.data;
|
|
132
|
+
const found = included.find((r) => r.type === ref.type && r.id === ref.id);
|
|
133
|
+
return found ? flattenResource(found, included) : { id: ref.id, type: ref.type };
|
|
134
|
+
}
|
|
135
|
+
function flattenResource(resource, included = []) {
|
|
136
|
+
const result = {
|
|
137
|
+
id: resource.id,
|
|
138
|
+
...resource.attributes
|
|
139
|
+
};
|
|
140
|
+
if (resource.relationships) {
|
|
141
|
+
for (const [key, rel] of Object.entries(resource.relationships)) {
|
|
142
|
+
result[key] = resolveRelationship(rel, included);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function deserialize(response) {
|
|
148
|
+
const included = response.included ?? [];
|
|
149
|
+
if (Array.isArray(response.data)) {
|
|
150
|
+
return response.data.map((r) => flattenResource(r, included));
|
|
151
|
+
}
|
|
152
|
+
return flattenResource(response.data, included);
|
|
153
|
+
}
|
|
154
|
+
function extractMeta(response) {
|
|
155
|
+
return {
|
|
156
|
+
total: response.meta?.total,
|
|
157
|
+
page: response.meta?.page,
|
|
158
|
+
limit: response.meta?.limit,
|
|
159
|
+
hasNext: !!response.links?.next
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/sdk/serialize.ts
|
|
164
|
+
function serialize(type, attributes, id) {
|
|
165
|
+
const resource = {
|
|
166
|
+
type,
|
|
167
|
+
attributes
|
|
168
|
+
};
|
|
169
|
+
if (id !== void 0) {
|
|
170
|
+
resource.id = id;
|
|
171
|
+
}
|
|
172
|
+
return { data: resource };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/sdk/client.ts
|
|
176
|
+
var InblogApiError = class extends Error {
|
|
177
|
+
status;
|
|
178
|
+
code;
|
|
179
|
+
title;
|
|
180
|
+
detail;
|
|
181
|
+
constructor(status, code, title, detail) {
|
|
182
|
+
super(detail || title);
|
|
183
|
+
this.name = "InblogApiError";
|
|
184
|
+
this.status = status;
|
|
185
|
+
this.code = code;
|
|
186
|
+
this.title = title;
|
|
187
|
+
this.detail = detail;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var InblogClient = class {
|
|
191
|
+
apiKey;
|
|
192
|
+
accessToken;
|
|
193
|
+
baseUrl;
|
|
194
|
+
blogId;
|
|
195
|
+
blogSubdomain;
|
|
196
|
+
tokenRefresher;
|
|
197
|
+
constructor(options) {
|
|
198
|
+
this.apiKey = options.apiKey;
|
|
199
|
+
this.accessToken = options.accessToken;
|
|
200
|
+
this.baseUrl = (options.baseUrl ?? "https://inblog.ai").replace(/\/$/, "");
|
|
201
|
+
this.blogId = options.blogId;
|
|
202
|
+
this.blogSubdomain = options.blogSubdomain;
|
|
203
|
+
if (!this.apiKey && !this.accessToken) {
|
|
204
|
+
throw new Error("Either apiKey or accessToken must be provided");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
setTokenRefresher(fn) {
|
|
208
|
+
this.tokenRefresher = fn;
|
|
209
|
+
}
|
|
210
|
+
async readHeaders() {
|
|
211
|
+
const token = await this.resolveToken();
|
|
212
|
+
const headers = {
|
|
213
|
+
Authorization: `Bearer ${token}`,
|
|
214
|
+
Accept: "application/vnd.api+json"
|
|
215
|
+
};
|
|
216
|
+
if (this.accessToken && this.blogId) {
|
|
217
|
+
headers["X-Blog-Id"] = String(this.blogId);
|
|
218
|
+
}
|
|
219
|
+
return headers;
|
|
220
|
+
}
|
|
221
|
+
async writeHeaders() {
|
|
222
|
+
const token = await this.resolveToken();
|
|
223
|
+
const headers = {
|
|
224
|
+
Authorization: `Bearer ${token}`,
|
|
225
|
+
"Content-Type": "application/vnd.api+json",
|
|
226
|
+
Accept: "application/vnd.api+json"
|
|
227
|
+
};
|
|
228
|
+
if (this.accessToken && this.blogId) {
|
|
229
|
+
headers["X-Blog-Id"] = String(this.blogId);
|
|
230
|
+
}
|
|
231
|
+
return headers;
|
|
232
|
+
}
|
|
233
|
+
async resolveToken() {
|
|
234
|
+
if (this.apiKey) return this.apiKey;
|
|
235
|
+
if (this.tokenRefresher) {
|
|
236
|
+
const refreshed = await this.tokenRefresher();
|
|
237
|
+
if (refreshed) {
|
|
238
|
+
this.accessToken = refreshed;
|
|
239
|
+
return refreshed;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (this.accessToken) return this.accessToken;
|
|
243
|
+
throw new Error("No valid authentication token available");
|
|
244
|
+
}
|
|
245
|
+
buildUrl(path3, params) {
|
|
246
|
+
const url = new URL(`/api${path3}`, this.baseUrl);
|
|
247
|
+
if (params) {
|
|
248
|
+
for (const [key, value] of Object.entries(params)) {
|
|
249
|
+
if (value === void 0 || value === null) continue;
|
|
250
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
251
|
+
for (const [subKey, subValue] of Object.entries(value)) {
|
|
252
|
+
if (subValue !== void 0 && subValue !== null) {
|
|
253
|
+
url.searchParams.set(`${key}[${subKey}]`, String(subValue));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
url.searchParams.set(key, String(value));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return url.toString();
|
|
262
|
+
}
|
|
263
|
+
async handleResponse(response) {
|
|
264
|
+
const text = await response.text();
|
|
265
|
+
let body;
|
|
266
|
+
try {
|
|
267
|
+
body = text ? JSON.parse(text) : null;
|
|
268
|
+
} catch {
|
|
269
|
+
throw new InblogApiError(response.status, "PARSE_ERROR", "Failed to parse API response");
|
|
270
|
+
}
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
if (body?.errors?.[0]) {
|
|
273
|
+
const err = body.errors[0];
|
|
274
|
+
throw new InblogApiError(
|
|
275
|
+
parseInt(err.status, 10) || response.status,
|
|
276
|
+
err.code || "UNKNOWN_ERROR",
|
|
277
|
+
err.title || "API Error",
|
|
278
|
+
err.detail
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
throw new InblogApiError(response.status, "UNKNOWN_ERROR", `HTTP ${response.status}`);
|
|
282
|
+
}
|
|
283
|
+
return body;
|
|
284
|
+
}
|
|
285
|
+
async get(path3, params) {
|
|
286
|
+
const url = this.buildUrl(path3, params);
|
|
287
|
+
const response = await fetch(url, { method: "GET", headers: await this.readHeaders() });
|
|
288
|
+
const json = await this.handleResponse(response);
|
|
289
|
+
return {
|
|
290
|
+
data: deserialize(json),
|
|
291
|
+
meta: extractMeta(json)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async list(path3, params) {
|
|
295
|
+
const url = this.buildUrl(path3, params);
|
|
296
|
+
const response = await fetch(url, { method: "GET", headers: await this.readHeaders() });
|
|
297
|
+
const json = await this.handleResponse(response);
|
|
298
|
+
return {
|
|
299
|
+
data: deserialize(json),
|
|
300
|
+
meta: extractMeta(json)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async create(path3, type, attributes, params) {
|
|
304
|
+
const url = this.buildUrl(path3, params);
|
|
305
|
+
const body = serialize(type, attributes);
|
|
306
|
+
const response = await fetch(url, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: await this.writeHeaders(),
|
|
309
|
+
body: JSON.stringify(body)
|
|
310
|
+
});
|
|
311
|
+
const json = await this.handleResponse(response);
|
|
312
|
+
return {
|
|
313
|
+
data: deserialize(json),
|
|
314
|
+
meta: extractMeta(json)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
async update(path3, type, id, attributes) {
|
|
318
|
+
const url = this.buildUrl(path3);
|
|
319
|
+
const body = serialize(type, attributes, id);
|
|
320
|
+
const response = await fetch(url, {
|
|
321
|
+
method: "PATCH",
|
|
322
|
+
headers: await this.writeHeaders(),
|
|
323
|
+
body: JSON.stringify(body)
|
|
324
|
+
});
|
|
325
|
+
const json = await this.handleResponse(response);
|
|
326
|
+
return {
|
|
327
|
+
data: deserialize(json),
|
|
328
|
+
meta: extractMeta(json)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async delete(path3) {
|
|
332
|
+
const url = this.buildUrl(path3);
|
|
333
|
+
const response = await fetch(url, { method: "DELETE", headers: await this.readHeaders() });
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
await this.handleResponse(response);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async post(path3, body) {
|
|
339
|
+
const url = this.buildUrl(path3);
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
method: "POST",
|
|
342
|
+
headers: await this.writeHeaders(),
|
|
343
|
+
body: body ? JSON.stringify(body) : void 0
|
|
344
|
+
});
|
|
345
|
+
const json = await this.handleResponse(response);
|
|
346
|
+
return {
|
|
347
|
+
data: deserialize(json),
|
|
348
|
+
meta: extractMeta(json)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async patch(path3, body) {
|
|
352
|
+
const url = this.buildUrl(path3);
|
|
353
|
+
const response = await fetch(url, {
|
|
354
|
+
method: "PATCH",
|
|
355
|
+
headers: await this.writeHeaders(),
|
|
356
|
+
body: body ? JSON.stringify(body) : void 0
|
|
357
|
+
});
|
|
358
|
+
const json = await this.handleResponse(response);
|
|
359
|
+
return {
|
|
360
|
+
data: deserialize(json),
|
|
361
|
+
meta: extractMeta(json)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Raw GET — for non-JSON:API endpoints that return plain JSON.
|
|
366
|
+
*/
|
|
367
|
+
async rawGet(path3, params) {
|
|
368
|
+
const url = this.buildUrl(path3, params);
|
|
369
|
+
const response = await fetch(url, { method: "GET", headers: await this.readHeaders() });
|
|
370
|
+
return this.handleRawResponse(response);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Raw POST — for non-JSON:API endpoints.
|
|
374
|
+
*/
|
|
375
|
+
async rawPost(path3, body) {
|
|
376
|
+
const url = this.buildUrl(path3);
|
|
377
|
+
const headers = await this.readHeaders();
|
|
378
|
+
headers["Content-Type"] = "application/json";
|
|
379
|
+
const response = await fetch(url, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers,
|
|
382
|
+
body: body ? JSON.stringify(body) : void 0
|
|
383
|
+
});
|
|
384
|
+
return this.handleRawResponse(response);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Raw PATCH — for non-JSON:API endpoints.
|
|
388
|
+
*/
|
|
389
|
+
async rawPatch(path3, body) {
|
|
390
|
+
const url = this.buildUrl(path3);
|
|
391
|
+
const headers = await this.readHeaders();
|
|
392
|
+
headers["Content-Type"] = "application/json";
|
|
393
|
+
const response = await fetch(url, {
|
|
394
|
+
method: "PATCH",
|
|
395
|
+
headers,
|
|
396
|
+
body: body ? JSON.stringify(body) : void 0
|
|
397
|
+
});
|
|
398
|
+
return this.handleRawResponse(response);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Raw DELETE — for non-JSON:API endpoints.
|
|
402
|
+
*/
|
|
403
|
+
async rawDelete(path3) {
|
|
404
|
+
const url = this.buildUrl(path3);
|
|
405
|
+
const response = await fetch(url, { method: "DELETE", headers: await this.readHeaders() });
|
|
406
|
+
if (response.status === 204) return { success: true };
|
|
407
|
+
return this.handleRawResponse(response);
|
|
408
|
+
}
|
|
409
|
+
async handleRawResponse(response) {
|
|
410
|
+
const text = await response.text();
|
|
411
|
+
let body;
|
|
412
|
+
try {
|
|
413
|
+
body = text ? JSON.parse(text) : null;
|
|
414
|
+
} catch {
|
|
415
|
+
if (!response.ok) {
|
|
416
|
+
throw new InblogApiError(response.status, "PARSE_ERROR", "Failed to parse API response");
|
|
417
|
+
}
|
|
418
|
+
return text;
|
|
419
|
+
}
|
|
420
|
+
if (!response.ok) {
|
|
421
|
+
if (body?.errors?.[0]) {
|
|
422
|
+
const err = body.errors[0];
|
|
423
|
+
throw new InblogApiError(
|
|
424
|
+
parseInt(err.status, 10) || response.status,
|
|
425
|
+
err.code || "UNKNOWN_ERROR",
|
|
426
|
+
err.title || "API Error",
|
|
427
|
+
err.detail
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
throw new InblogApiError(response.status, "UNKNOWN_ERROR", body?.message || `HTTP ${response.status}`);
|
|
431
|
+
}
|
|
432
|
+
return body;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/utils/token-store.ts
|
|
437
|
+
var fs2 = __toESM(require("fs"));
|
|
438
|
+
var path2 = __toESM(require("path"));
|
|
439
|
+
var os2 = __toESM(require("os"));
|
|
440
|
+
var CONFIG_DIR2 = path2.join(os2.homedir(), ".config", "inblog");
|
|
441
|
+
var TOKENS_FILE = path2.join(CONFIG_DIR2, "tokens.json");
|
|
442
|
+
function ensureConfigDir2() {
|
|
443
|
+
if (!fs2.existsSync(CONFIG_DIR2)) {
|
|
444
|
+
fs2.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function readSession() {
|
|
448
|
+
if (!fs2.existsSync(TOKENS_FILE)) return null;
|
|
449
|
+
try {
|
|
450
|
+
return JSON.parse(fs2.readFileSync(TOKENS_FILE, "utf-8"));
|
|
451
|
+
} catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function writeSession(session) {
|
|
456
|
+
ensureConfigDir2();
|
|
457
|
+
fs2.writeFileSync(TOKENS_FILE, JSON.stringify(session, null, 2) + "\n", {
|
|
458
|
+
encoding: "utf-8",
|
|
459
|
+
mode: 384
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function clearSession() {
|
|
463
|
+
if (fs2.existsSync(TOKENS_FILE)) {
|
|
464
|
+
fs2.unlinkSync(TOKENS_FILE);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function setActiveBlog(blogId, subdomain, plan) {
|
|
468
|
+
const session = readSession();
|
|
469
|
+
if (!session) {
|
|
470
|
+
throw new Error("Not logged in. Run `inblog auth login` first.");
|
|
471
|
+
}
|
|
472
|
+
session.activeBlogId = blogId;
|
|
473
|
+
session.activeBlogSubdomain = subdomain;
|
|
474
|
+
if (plan !== void 0) session.activeBlogPlan = plan;
|
|
475
|
+
writeSession(session);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/utils/token-refresh.ts
|
|
479
|
+
var SUPABASE_URL = process.env.INBLOG_SUPABASE_URL || "https://fgobbnslcbjgothosvni.supabase.co";
|
|
480
|
+
var SUPABASE_ANON_KEY = process.env.INBLOG_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZnb2JibnNsY2JqZ290aG9zdm5pIiwicm9sZSI6ImFub24iLCJpYXQiOjE2Nzc5OTc1MzYsImV4cCI6MTk5MzU3MzUzNn0.cFlx12_PLf42IHJseAxiYOw7MFiS2FOSRgbSZQNDiEo";
|
|
481
|
+
var REFRESH_BUFFER_SECONDS = 60;
|
|
482
|
+
function isTokenExpiringSoon(session) {
|
|
483
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
484
|
+
return session.tokens.expires_at - now < REFRESH_BUFFER_SECONDS;
|
|
485
|
+
}
|
|
486
|
+
async function refreshTokens(refreshToken) {
|
|
487
|
+
const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: {
|
|
490
|
+
"Content-Type": "application/json",
|
|
491
|
+
apikey: SUPABASE_ANON_KEY
|
|
492
|
+
},
|
|
493
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
494
|
+
});
|
|
495
|
+
if (!response.ok) {
|
|
496
|
+
throw new Error("Session expired. Run `inblog auth login` to re-authenticate.");
|
|
497
|
+
}
|
|
498
|
+
const data = await response.json();
|
|
499
|
+
return {
|
|
500
|
+
access_token: data.access_token,
|
|
501
|
+
refresh_token: data.refresh_token,
|
|
502
|
+
expires_at: Math.floor(Date.now() / 1e3) + data.expires_in,
|
|
503
|
+
user_id: data.user?.id || ""
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
async function getValidAccessToken() {
|
|
507
|
+
const session = readSession();
|
|
508
|
+
if (!session) return null;
|
|
509
|
+
if (!isTokenExpiringSoon(session)) {
|
|
510
|
+
return session.tokens.access_token;
|
|
511
|
+
}
|
|
512
|
+
const newTokens = await refreshTokens(session.tokens.refresh_token);
|
|
513
|
+
session.tokens = newTokens;
|
|
514
|
+
writeSession(session);
|
|
515
|
+
return newTokens.access_token;
|
|
516
|
+
}
|
|
517
|
+
async function exchangeCodeForTokens(code, codeVerifier) {
|
|
518
|
+
const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: {
|
|
521
|
+
"Content-Type": "application/json",
|
|
522
|
+
apikey: SUPABASE_ANON_KEY
|
|
523
|
+
},
|
|
524
|
+
body: JSON.stringify({
|
|
525
|
+
auth_code: code,
|
|
526
|
+
code_verifier: codeVerifier
|
|
527
|
+
})
|
|
528
|
+
});
|
|
529
|
+
if (!response.ok) {
|
|
530
|
+
const errorData = await response.text();
|
|
531
|
+
throw new Error(`Token exchange failed: ${errorData}`);
|
|
532
|
+
}
|
|
533
|
+
const data = await response.json();
|
|
534
|
+
return {
|
|
535
|
+
access_token: data.access_token,
|
|
536
|
+
refresh_token: data.refresh_token,
|
|
537
|
+
expires_at: Math.floor(Date.now() / 1e3) + data.expires_in,
|
|
538
|
+
user_id: data.user?.id || ""
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/utils/errors.ts
|
|
543
|
+
function checkPlanOrExit() {
|
|
544
|
+
const session = readSession();
|
|
545
|
+
if (!session) return;
|
|
546
|
+
const plan = session.activeBlogPlan;
|
|
547
|
+
if (plan === "team" || plan === "enterprise") return;
|
|
548
|
+
if (!plan) return;
|
|
549
|
+
printError(`CLI features require a Team plan or above. Current plan: ${plan}`);
|
|
550
|
+
printError(`Upgrade: https://inblog.ai/dashboard/${session.activeBlogSubdomain || ""}/settings/billing`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
function handleError(error, json = false) {
|
|
554
|
+
if (error instanceof InblogApiError) {
|
|
555
|
+
if (json) {
|
|
556
|
+
const errorObj = {
|
|
557
|
+
error: true,
|
|
558
|
+
status: error.status,
|
|
559
|
+
code: error.code,
|
|
560
|
+
title: error.title,
|
|
561
|
+
detail: error.detail
|
|
562
|
+
};
|
|
563
|
+
process.stderr.write(JSON.stringify(errorObj, null, 2) + "\n");
|
|
564
|
+
} else {
|
|
565
|
+
printError(`API Error (${error.status}): ${error.title}`);
|
|
566
|
+
if (error.detail) {
|
|
567
|
+
printError(` ${error.detail}`);
|
|
568
|
+
}
|
|
569
|
+
if (error.code === "SUBSCRIPTION_REQUIRED") {
|
|
570
|
+
printError(" API access requires a paid plan. Upgrade at https://inblog.ai/dashboard/settings/billing");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
process.exit(2);
|
|
574
|
+
}
|
|
575
|
+
if (error instanceof Error) {
|
|
576
|
+
if (json) {
|
|
577
|
+
process.stderr.write(JSON.stringify({ error: true, message: error.message }, null, 2) + "\n");
|
|
578
|
+
} else {
|
|
579
|
+
printError(error.message);
|
|
580
|
+
}
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
printError("An unexpected error occurred");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/sdk/endpoints/posts.ts
|
|
588
|
+
var PostsEndpoint = class {
|
|
589
|
+
constructor(client) {
|
|
590
|
+
this.client = client;
|
|
591
|
+
}
|
|
592
|
+
async list(options = {}) {
|
|
593
|
+
const params = {};
|
|
594
|
+
if (options.page) params.page = options.page;
|
|
595
|
+
if (options.limit) params.limit = options.limit;
|
|
596
|
+
if (options.sort) params.sort = options.sort;
|
|
597
|
+
if (options.order) params.order = options.order;
|
|
598
|
+
if (options.include?.length) params.include = options.include.join(",");
|
|
599
|
+
if (options.filter) params.filter = options.filter;
|
|
600
|
+
return this.client.list("/v1/posts", params);
|
|
601
|
+
}
|
|
602
|
+
async get(id, include) {
|
|
603
|
+
const params = {};
|
|
604
|
+
if (include?.length) params.include = include.join(",");
|
|
605
|
+
return this.client.get(`/v1/posts/${id}`, params);
|
|
606
|
+
}
|
|
607
|
+
async create(input) {
|
|
608
|
+
const { tag_ids, author_ids, ...attributes } = input;
|
|
609
|
+
const attrs = { ...attributes };
|
|
610
|
+
if (tag_ids) attrs.tag_ids = tag_ids;
|
|
611
|
+
if (author_ids) attrs.author_ids = author_ids;
|
|
612
|
+
return this.client.create("/v1/posts", "posts", attrs, {
|
|
613
|
+
include: "tags,authors"
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async update(id, input) {
|
|
617
|
+
return this.client.update(`/v1/posts/${id}`, "posts", id, input);
|
|
618
|
+
}
|
|
619
|
+
async delete(id) {
|
|
620
|
+
return this.client.delete(`/v1/posts/${id}`);
|
|
621
|
+
}
|
|
622
|
+
async publish(id) {
|
|
623
|
+
return this.client.post(`/v1/posts/${id}/publish`);
|
|
624
|
+
}
|
|
625
|
+
async unpublish(id) {
|
|
626
|
+
return this.client.post(`/v1/posts/${id}/unpublish`);
|
|
627
|
+
}
|
|
628
|
+
async schedule(id, scheduledAt) {
|
|
629
|
+
return this.client.post(`/v1/posts/${id}/schedule`, {
|
|
630
|
+
data: { type: "posts", attributes: { published_at: scheduledAt } }
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
// ── Relationship management ──
|
|
634
|
+
async listTags(postId) {
|
|
635
|
+
return this.client.list(`/v1/posts/${postId}/tags`);
|
|
636
|
+
}
|
|
637
|
+
async addTags(postId, tagIds) {
|
|
638
|
+
return this.client.post(`/v1/posts/${postId}/tags`, {
|
|
639
|
+
data: tagIds.map((id) => ({ id }))
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
async removeTag(postId, tagId) {
|
|
643
|
+
return this.client.delete(`/v1/posts/${postId}/tags/${tagId}`);
|
|
644
|
+
}
|
|
645
|
+
async listAuthors(postId) {
|
|
646
|
+
return this.client.list(`/v1/posts/${postId}/authors`);
|
|
647
|
+
}
|
|
648
|
+
async addAuthors(postId, authorIds) {
|
|
649
|
+
return this.client.post(`/v1/posts/${postId}/authors`, {
|
|
650
|
+
data: authorIds.map((id) => ({ id }))
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async removeAuthor(postId, authorId) {
|
|
654
|
+
return this.client.delete(`/v1/posts/${postId}/authors/${authorId}`);
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// src/sdk/endpoints/tags.ts
|
|
659
|
+
var TagsEndpoint = class {
|
|
660
|
+
constructor(client) {
|
|
661
|
+
this.client = client;
|
|
662
|
+
}
|
|
663
|
+
async list(options = {}) {
|
|
664
|
+
const params = {};
|
|
665
|
+
if (options.include?.length) params.include = options.include.join(",");
|
|
666
|
+
return this.client.list("/v1/tags", params);
|
|
667
|
+
}
|
|
668
|
+
async get(id) {
|
|
669
|
+
return this.client.get(`/v1/tags/${id}`);
|
|
670
|
+
}
|
|
671
|
+
async create(input) {
|
|
672
|
+
return this.client.create("/v1/tags", "tags", input);
|
|
673
|
+
}
|
|
674
|
+
async update(id, input) {
|
|
675
|
+
return this.client.update(`/v1/tags/${id}`, "tags", id, input);
|
|
676
|
+
}
|
|
677
|
+
async delete(id) {
|
|
678
|
+
return this.client.delete(`/v1/tags/${id}`);
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/sdk/endpoints/authors.ts
|
|
683
|
+
var AuthorsEndpoint = class {
|
|
684
|
+
constructor(client) {
|
|
685
|
+
this.client = client;
|
|
686
|
+
}
|
|
687
|
+
async list(options = {}) {
|
|
688
|
+
const params = {};
|
|
689
|
+
if (options.page) params.page = options.page;
|
|
690
|
+
if (options.limit) params.limit = options.limit;
|
|
691
|
+
if (options.include?.length) params.include = options.include.join(",");
|
|
692
|
+
return this.client.list("/v1/authors", params);
|
|
693
|
+
}
|
|
694
|
+
async get(id) {
|
|
695
|
+
return this.client.get(`/v1/authors/${id}`);
|
|
696
|
+
}
|
|
697
|
+
async update(id, input) {
|
|
698
|
+
return this.client.update(
|
|
699
|
+
`/v1/authors/${id}`,
|
|
700
|
+
"authors",
|
|
701
|
+
id,
|
|
702
|
+
input
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/sdk/endpoints/blogs.ts
|
|
708
|
+
var BlogsEndpoint = class {
|
|
709
|
+
constructor(client) {
|
|
710
|
+
this.client = client;
|
|
711
|
+
}
|
|
712
|
+
async me() {
|
|
713
|
+
return this.client.get("/v1/blogs/me");
|
|
714
|
+
}
|
|
715
|
+
async update(subdomain, input) {
|
|
716
|
+
return this.client.update(
|
|
717
|
+
`/v1/blogs/${subdomain}`,
|
|
718
|
+
"blogs",
|
|
719
|
+
subdomain,
|
|
720
|
+
input
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
// Domain management (plain JSON endpoints, not JSON:API)
|
|
724
|
+
async domainConnect(domain) {
|
|
725
|
+
return this.client.rawPost("/v1/blogs/domain", { custom_domain: domain });
|
|
726
|
+
}
|
|
727
|
+
async domainStatus() {
|
|
728
|
+
return this.client.rawGet("/v1/blogs/domain");
|
|
729
|
+
}
|
|
730
|
+
async domainDisconnect() {
|
|
731
|
+
return this.client.rawDelete("/v1/blogs/domain");
|
|
732
|
+
}
|
|
733
|
+
// Custom UI / Banner (plain JSON endpoints)
|
|
734
|
+
async getCustomUi() {
|
|
735
|
+
return this.client.rawGet("/v1/blogs/custom-ui");
|
|
736
|
+
}
|
|
737
|
+
async updateCustomUi(input) {
|
|
738
|
+
return this.client.rawPatch("/v1/blogs/custom-ui", input);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/sdk/endpoints/redirects.ts
|
|
743
|
+
var RedirectsEndpoint = class {
|
|
744
|
+
constructor(client) {
|
|
745
|
+
this.client = client;
|
|
746
|
+
}
|
|
747
|
+
async list(options = {}) {
|
|
748
|
+
const params = {};
|
|
749
|
+
if (options.page) params.page = options.page;
|
|
750
|
+
if (options.limit) params.limit = options.limit;
|
|
751
|
+
return this.client.list("/v1/redirects", params);
|
|
752
|
+
}
|
|
753
|
+
async get(id) {
|
|
754
|
+
return this.client.get(`/v1/redirects/${id}`);
|
|
755
|
+
}
|
|
756
|
+
async create(input) {
|
|
757
|
+
return this.client.create("/v1/redirects", "redirects", input);
|
|
758
|
+
}
|
|
759
|
+
async update(id, input) {
|
|
760
|
+
return this.client.update(
|
|
761
|
+
`/v1/redirects/${id}`,
|
|
762
|
+
"redirects",
|
|
763
|
+
id,
|
|
764
|
+
input
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
async delete(id) {
|
|
768
|
+
return this.client.delete(`/v1/redirects/${id}`);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/sdk/endpoints/forms.ts
|
|
773
|
+
var FormsEndpoint = class {
|
|
774
|
+
constructor(client) {
|
|
775
|
+
this.client = client;
|
|
776
|
+
}
|
|
777
|
+
async list(options = {}) {
|
|
778
|
+
const params = {};
|
|
779
|
+
if (options.page) params.page = options.page;
|
|
780
|
+
if (options.limit) params.limit = options.limit;
|
|
781
|
+
return this.client.list("/v1/forms", params);
|
|
782
|
+
}
|
|
783
|
+
async get(id) {
|
|
784
|
+
return this.client.get(`/v1/forms/${id}`);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
var FormResponsesEndpoint = class {
|
|
788
|
+
constructor(client) {
|
|
789
|
+
this.client = client;
|
|
790
|
+
}
|
|
791
|
+
async list(options = {}) {
|
|
792
|
+
const params = {};
|
|
793
|
+
if (options.page) params.page = options.page;
|
|
794
|
+
if (options.limit) params.limit = options.limit;
|
|
795
|
+
if (options.filter) params.filter = options.filter;
|
|
796
|
+
return this.client.list("/v1/form-responses", params);
|
|
797
|
+
}
|
|
798
|
+
async get(id) {
|
|
799
|
+
return this.client.get(`/v1/form-responses/${id}`);
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// src/sdk/endpoints/search-console.ts
|
|
804
|
+
var SearchConsoleEndpoint = class {
|
|
805
|
+
constructor(client) {
|
|
806
|
+
this.client = client;
|
|
807
|
+
}
|
|
808
|
+
async oauthUrl(redirectUri) {
|
|
809
|
+
const params = {};
|
|
810
|
+
if (redirectUri) params.redirect_uri = redirectUri;
|
|
811
|
+
return this.client.rawGet("/v1/blogs/search-console/oauth-url", params);
|
|
812
|
+
}
|
|
813
|
+
async connect(code, redirectUri) {
|
|
814
|
+
return this.client.rawPost("/v1/blogs/search-console/connect", { code, redirect_uri: redirectUri });
|
|
815
|
+
}
|
|
816
|
+
async status() {
|
|
817
|
+
return this.client.rawGet("/v1/blogs/search-console/status");
|
|
818
|
+
}
|
|
819
|
+
async disconnect() {
|
|
820
|
+
return this.client.rawDelete("/v1/blogs/search-console/disconnect");
|
|
821
|
+
}
|
|
822
|
+
async keywords(options = {}) {
|
|
823
|
+
return this.client.rawGet("/v1/blogs/search-console/keywords", options);
|
|
824
|
+
}
|
|
825
|
+
async pages(options = {}) {
|
|
826
|
+
return this.client.rawGet("/v1/blogs/search-console/pages", options);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
// src/sdk/endpoints/analytics.ts
|
|
831
|
+
var AnalyticsEndpoint = class {
|
|
832
|
+
constructor(client) {
|
|
833
|
+
this.client = client;
|
|
834
|
+
}
|
|
835
|
+
async traffic(options = {}) {
|
|
836
|
+
return this.client.rawGet("/v1/blogs/analytics/traffic", options);
|
|
837
|
+
}
|
|
838
|
+
async posts(options = {}) {
|
|
839
|
+
return this.client.rawGet("/v1/blogs/analytics/posts", options);
|
|
840
|
+
}
|
|
841
|
+
async sources(options = {}) {
|
|
842
|
+
return this.client.rawGet("/v1/blogs/analytics/sources", options);
|
|
843
|
+
}
|
|
844
|
+
async postTraffic(postId, options = {}) {
|
|
845
|
+
return this.client.rawGet(`/v1/blogs/analytics/posts/${postId}`, options);
|
|
846
|
+
}
|
|
847
|
+
async postSources(postId, options = {}) {
|
|
848
|
+
return this.client.rawGet(`/v1/blogs/analytics/posts/${postId}/sources`, options);
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// src/utils/client-factory.ts
|
|
853
|
+
function createClientFromCommand(cmd) {
|
|
854
|
+
checkPlanOrExit();
|
|
855
|
+
const opts = cmd.optsWithGlobals();
|
|
856
|
+
const config = readConfig();
|
|
857
|
+
const baseUrl = opts.baseUrl || config.baseUrl || "https://inblog.ai";
|
|
858
|
+
const session = readSession();
|
|
859
|
+
if (!session) {
|
|
860
|
+
throw new Error("Not logged in. Run `inblog auth login` to authenticate.");
|
|
861
|
+
}
|
|
862
|
+
if (!session.activeBlogId || !session.activeBlogSubdomain) {
|
|
863
|
+
throw new Error(
|
|
864
|
+
"No active blog selected. Run `inblog blogs switch` to select a blog."
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
const client = new InblogClient({
|
|
868
|
+
accessToken: session.tokens.access_token,
|
|
869
|
+
baseUrl,
|
|
870
|
+
blogId: session.activeBlogId,
|
|
871
|
+
blogSubdomain: session.activeBlogSubdomain
|
|
872
|
+
});
|
|
873
|
+
client.setTokenRefresher(getValidAccessToken);
|
|
874
|
+
return {
|
|
875
|
+
client,
|
|
876
|
+
posts: new PostsEndpoint(client),
|
|
877
|
+
tags: new TagsEndpoint(client),
|
|
878
|
+
authors: new AuthorsEndpoint(client),
|
|
879
|
+
blogs: new BlogsEndpoint(client),
|
|
880
|
+
redirects: new RedirectsEndpoint(client),
|
|
881
|
+
forms: new FormsEndpoint(client),
|
|
882
|
+
formResponses: new FormResponsesEndpoint(client),
|
|
883
|
+
searchConsole: new SearchConsoleEndpoint(client),
|
|
884
|
+
analytics: new AnalyticsEndpoint(client)
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function isJsonMode(cmd) {
|
|
888
|
+
return cmd.optsWithGlobals().json === true;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/utils/pkce.ts
|
|
892
|
+
var import_node_crypto = require("crypto");
|
|
893
|
+
function generateCodeVerifier() {
|
|
894
|
+
return (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
895
|
+
}
|
|
896
|
+
function generateCodeChallenge(verifier) {
|
|
897
|
+
return (0, import_node_crypto.createHash)("sha256").update(verifier).digest("base64url");
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// src/utils/callback-server.ts
|
|
901
|
+
var import_node_http = require("http");
|
|
902
|
+
var SUCCESS_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0"><div style="text-align:center"><h1>Login successful!</h1><p>You can close this window and return to the terminal.</p></div></body></html>`;
|
|
903
|
+
var ERROR_HTML = `<!DOCTYPE html><html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0"><div style="text-align:center"><h1>Login failed</h1><p>Please try again.</p></div></body></html>`;
|
|
904
|
+
function startCallbackServer(options = {}) {
|
|
905
|
+
const { port = 54321, timeout = 3e5, callbackPath = "/auth/callback" } = options;
|
|
906
|
+
return new Promise((resolve2, reject) => {
|
|
907
|
+
let server;
|
|
908
|
+
let timer;
|
|
909
|
+
const cleanup = () => {
|
|
910
|
+
clearTimeout(timer);
|
|
911
|
+
server.close();
|
|
912
|
+
};
|
|
913
|
+
server = (0, import_node_http.createServer)((req, res) => {
|
|
914
|
+
const url = new URL(req.url || "/", `http://127.0.0.1`);
|
|
915
|
+
if (url.pathname !== callbackPath) {
|
|
916
|
+
res.writeHead(404);
|
|
917
|
+
res.end("Not found");
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const code = url.searchParams.get("code");
|
|
921
|
+
const error = url.searchParams.get("error");
|
|
922
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
923
|
+
if (error) {
|
|
924
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
925
|
+
res.end(ERROR_HTML);
|
|
926
|
+
cleanup();
|
|
927
|
+
reject(new Error(`OAuth error: ${errorDescription || error}`));
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (!code) {
|
|
931
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
932
|
+
res.end(ERROR_HTML);
|
|
933
|
+
cleanup();
|
|
934
|
+
reject(new Error("Missing authorization code in callback"));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
938
|
+
res.end(SUCCESS_HTML);
|
|
939
|
+
cleanup();
|
|
940
|
+
resolve2({ code, port: actualPort });
|
|
941
|
+
});
|
|
942
|
+
let actualPort = port;
|
|
943
|
+
server.on("error", (err) => {
|
|
944
|
+
if (err.code === "EADDRINUSE") {
|
|
945
|
+
server.listen(0, "127.0.0.1");
|
|
946
|
+
} else {
|
|
947
|
+
reject(err);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
server.on("listening", () => {
|
|
951
|
+
const addr = server.address();
|
|
952
|
+
if (addr && typeof addr === "object") {
|
|
953
|
+
actualPort = addr.port;
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
server.listen(port, "127.0.0.1");
|
|
957
|
+
timer = setTimeout(() => {
|
|
958
|
+
cleanup();
|
|
959
|
+
reject(new Error("Login timed out. Please try again."));
|
|
960
|
+
}, timeout);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/commands/auth.ts
|
|
965
|
+
function registerAuthCommands(program2) {
|
|
966
|
+
const auth = program2.command("auth").description("Manage authentication");
|
|
967
|
+
auth.command("login").description("Log in with your Google account").action(async function() {
|
|
968
|
+
const json = isJsonMode(this);
|
|
969
|
+
try {
|
|
970
|
+
if (json) {
|
|
971
|
+
throw new Error("Interactive login is not available in --json mode.");
|
|
972
|
+
}
|
|
973
|
+
const existingSession = readSession();
|
|
974
|
+
if (existingSession) {
|
|
975
|
+
printWarning("Already logged in. Run `inblog auth logout` first to switch accounts.");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const codeVerifier = generateCodeVerifier();
|
|
979
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
980
|
+
const serverPromise = startCallbackServer();
|
|
981
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
982
|
+
const redirectUri = `http://127.0.0.1:54321/auth/callback`;
|
|
983
|
+
const authUrl = new URL(`${SUPABASE_URL}/auth/v1/authorize`);
|
|
984
|
+
authUrl.searchParams.set("provider", "google");
|
|
985
|
+
authUrl.searchParams.set("redirect_to", redirectUri);
|
|
986
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
987
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
988
|
+
console.log("Opening browser for login...");
|
|
989
|
+
await (0, import_open.default)(authUrl.toString());
|
|
990
|
+
console.log("Waiting for authentication...");
|
|
991
|
+
const { code } = await serverPromise;
|
|
992
|
+
const tokens = await exchangeCodeForTokens(code, codeVerifier);
|
|
993
|
+
const config = readConfig();
|
|
994
|
+
const baseUrl = this.optsWithGlobals().baseUrl || config.baseUrl || "https://inblog.ai";
|
|
995
|
+
const response = await fetch(`${baseUrl}/api/v1/user/blogs`, {
|
|
996
|
+
headers: {
|
|
997
|
+
Authorization: `Bearer ${tokens.access_token}`,
|
|
998
|
+
"X-Blog-Id": "0",
|
|
999
|
+
Accept: "application/vnd.api+json"
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
if (!response.ok) {
|
|
1003
|
+
writeSession({ tokens });
|
|
1004
|
+
printSuccess("Logged in successfully.");
|
|
1005
|
+
printWarning("Could not fetch blog list. Run `inblog blogs list` to select a blog.");
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const blogsData = await response.json();
|
|
1009
|
+
const blogs = blogsData.data || [];
|
|
1010
|
+
if (blogs.length === 0) {
|
|
1011
|
+
writeSession({ tokens });
|
|
1012
|
+
printSuccess("Logged in, but you have no blogs yet.");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
let activeBlogId;
|
|
1016
|
+
let activeBlogSubdomain;
|
|
1017
|
+
let activeBlogPlan;
|
|
1018
|
+
if (blogs.length === 1) {
|
|
1019
|
+
activeBlogId = parseInt(blogs[0].id, 10);
|
|
1020
|
+
activeBlogSubdomain = blogs[0].attributes.subdomain;
|
|
1021
|
+
activeBlogPlan = blogs[0].attributes.plan;
|
|
1022
|
+
} else {
|
|
1023
|
+
const choices = blogs.map((b) => ({
|
|
1024
|
+
name: `${b.attributes.title} (${b.attributes.subdomain}) [${b.attributes.permission}]`,
|
|
1025
|
+
value: { id: parseInt(b.id, 10), subdomain: b.attributes.subdomain }
|
|
1026
|
+
}));
|
|
1027
|
+
const selected = await (0, import_prompts.select)({
|
|
1028
|
+
message: "Select a blog to use:",
|
|
1029
|
+
choices
|
|
1030
|
+
});
|
|
1031
|
+
activeBlogId = selected.id;
|
|
1032
|
+
activeBlogSubdomain = selected.subdomain;
|
|
1033
|
+
const selectedBlog = blogs.find((b) => parseInt(b.id, 10) === activeBlogId);
|
|
1034
|
+
activeBlogPlan = selectedBlog?.attributes.plan;
|
|
1035
|
+
}
|
|
1036
|
+
writeSession({
|
|
1037
|
+
tokens,
|
|
1038
|
+
activeBlogId,
|
|
1039
|
+
activeBlogSubdomain,
|
|
1040
|
+
activeBlogPlan
|
|
1041
|
+
});
|
|
1042
|
+
printSuccess(`Logged in. Active blog: ${activeBlogSubdomain}`);
|
|
1043
|
+
if (activeBlogPlan !== "team" && activeBlogPlan !== "enterprise") {
|
|
1044
|
+
printWarning(`Blog "${activeBlogSubdomain}" is on the ${activeBlogPlan || "free"} plan.`);
|
|
1045
|
+
printWarning(" CLI features require a Team plan or above.");
|
|
1046
|
+
printWarning(` Upgrade: https://inblog.ai/dashboard/${activeBlogSubdomain}/settings/billing`);
|
|
1047
|
+
}
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
handleError(error, json);
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
auth.command("logout").description("Log out and clear stored session").action(async function() {
|
|
1053
|
+
const json = isJsonMode(this);
|
|
1054
|
+
try {
|
|
1055
|
+
const session = readSession();
|
|
1056
|
+
if (!session) {
|
|
1057
|
+
if (json) {
|
|
1058
|
+
printJson({ success: true, message: "Already logged out" });
|
|
1059
|
+
} else {
|
|
1060
|
+
printWarning("Already logged out.");
|
|
1061
|
+
}
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
clearSession();
|
|
1065
|
+
if (json) {
|
|
1066
|
+
printJson({ success: true, message: "Logged out" });
|
|
1067
|
+
} else {
|
|
1068
|
+
printSuccess("Logged out.");
|
|
1069
|
+
}
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
handleError(error, json);
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
auth.command("status").description("Show current login status").action(async function() {
|
|
1075
|
+
const json = isJsonMode(this);
|
|
1076
|
+
try {
|
|
1077
|
+
const session = readSession();
|
|
1078
|
+
if (!session) {
|
|
1079
|
+
if (json) {
|
|
1080
|
+
printJson({ loggedIn: false });
|
|
1081
|
+
} else {
|
|
1082
|
+
console.log("Not logged in. Run `inblog auth login` to authenticate.");
|
|
1083
|
+
}
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
const expiresAt = new Date(session.tokens.expires_at * 1e3).toISOString();
|
|
1087
|
+
if (json) {
|
|
1088
|
+
printJson({
|
|
1089
|
+
loggedIn: true,
|
|
1090
|
+
userId: session.tokens.user_id,
|
|
1091
|
+
activeBlogId: session.activeBlogId,
|
|
1092
|
+
activeBlogSubdomain: session.activeBlogSubdomain,
|
|
1093
|
+
tokenExpiresAt: expiresAt
|
|
1094
|
+
});
|
|
1095
|
+
} else {
|
|
1096
|
+
printDetail([
|
|
1097
|
+
["User ID", session.tokens.user_id],
|
|
1098
|
+
["Active Blog", session.activeBlogSubdomain || "\u2014"],
|
|
1099
|
+
["Active Blog ID", session.activeBlogId || "\u2014"],
|
|
1100
|
+
["Token Expires", expiresAt]
|
|
1101
|
+
]);
|
|
1102
|
+
}
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
handleError(error, json);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/commands/posts.ts
|
|
1110
|
+
var fs3 = __toESM(require("fs"));
|
|
1111
|
+
function formatPost(post) {
|
|
1112
|
+
return [
|
|
1113
|
+
["ID", post.id],
|
|
1114
|
+
["Title", post.title],
|
|
1115
|
+
["Slug", post.slug],
|
|
1116
|
+
["Published", post.published ? "Yes" : "No"],
|
|
1117
|
+
["Published At", post.published_at],
|
|
1118
|
+
["Content Type", post.content_type],
|
|
1119
|
+
["Description", post.description],
|
|
1120
|
+
["Image", post.image?.url],
|
|
1121
|
+
["Canonical URL", post.canonical_url],
|
|
1122
|
+
["Meta Title", post.meta_title],
|
|
1123
|
+
["Meta Description", post.meta_description],
|
|
1124
|
+
["Tags", post.tags?.map((t) => t.name).join(", ")],
|
|
1125
|
+
["Authors", post.authors?.map((a) => a.author_name).join(", ")]
|
|
1126
|
+
];
|
|
1127
|
+
}
|
|
1128
|
+
function registerPostsCommands(program2) {
|
|
1129
|
+
const posts = program2.command("posts").description("CRUD, publish, schedule posts + manage tags/authors");
|
|
1130
|
+
posts.command("list").description("List posts with pagination, filters, and sorting").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "10").option("-s, --sort <field>", "Sort field (published_at, created_at, title)").option("-o, --order <dir>", "Sort order (asc, desc)").option("--published", "Only published posts").option("--draft", "Only draft posts").option("--tag-id <id>", "Filter by tag ID").option("--author-id <id>", "Filter by author ID").option("--include <rels>", "Include relationships (tags,authors)", "tags,authors").action(async function() {
|
|
1131
|
+
const json = isJsonMode(this);
|
|
1132
|
+
try {
|
|
1133
|
+
const opts = this.opts();
|
|
1134
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1135
|
+
const filter = {};
|
|
1136
|
+
if (opts.published) filter.published = true;
|
|
1137
|
+
if (opts.draft) filter.published = false;
|
|
1138
|
+
if (opts.tagId) filter.tag_id = parseInt(opts.tagId, 10);
|
|
1139
|
+
if (opts.authorId) filter.author_id = opts.authorId;
|
|
1140
|
+
const { data, meta } = await endpoint.list({
|
|
1141
|
+
page: parseInt(opts.page, 10),
|
|
1142
|
+
limit: parseInt(opts.limit, 10),
|
|
1143
|
+
sort: opts.sort,
|
|
1144
|
+
order: opts.order,
|
|
1145
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0,
|
|
1146
|
+
include: opts.include ? opts.include.split(",") : void 0
|
|
1147
|
+
});
|
|
1148
|
+
if (json) {
|
|
1149
|
+
printJson({ data, meta });
|
|
1150
|
+
} else {
|
|
1151
|
+
printTable(
|
|
1152
|
+
["ID", "Title", "Slug", "Published", "Published At", "Tags"],
|
|
1153
|
+
data.map((p) => [
|
|
1154
|
+
p.id,
|
|
1155
|
+
truncate(p.title, 40),
|
|
1156
|
+
truncate(p.slug, 30),
|
|
1157
|
+
p.published ? "Yes" : "No",
|
|
1158
|
+
p.published_at ? new Date(p.published_at).toLocaleDateString() : "\u2014",
|
|
1159
|
+
p.tags?.map((t) => t.name).join(", ") ?? ""
|
|
1160
|
+
])
|
|
1161
|
+
);
|
|
1162
|
+
if (meta.total) {
|
|
1163
|
+
console.log(`
|
|
1164
|
+
Showing page ${meta.page ?? 1} (${data.length} of ${meta.total} posts)`);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
} catch (error) {
|
|
1168
|
+
handleError(error, json);
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
posts.command("get <id>").description("Get post details by ID (integer)").option("--include <rels>", "Include relationships", "tags,authors").action(async function(id) {
|
|
1172
|
+
const json = isJsonMode(this);
|
|
1173
|
+
try {
|
|
1174
|
+
const opts = this.opts();
|
|
1175
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1176
|
+
const { data } = await endpoint.get(id, opts.include?.split(","));
|
|
1177
|
+
if (json) {
|
|
1178
|
+
printJson(data);
|
|
1179
|
+
} else {
|
|
1180
|
+
printDetail(formatPost(data));
|
|
1181
|
+
}
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
handleError(error, json);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
posts.command("create").description("Create post (draft by default, use --published to publish)").requiredOption("-t, --title <title>", "Post title").option("-s, --slug <slug>", "Post slug").option("-d, --description <desc>", "Post description").option("--content <html>", "HTML content").option("--content-file <path>", "Read HTML content from file").option("--notion-url <url>", "Notion page URL").option("--published", "Publish immediately").option("--tag-ids <ids>", "Comma-separated tag IDs").option("--author-ids <ids>", "Comma-separated author IDs").option("--canonical-url <url>", "Canonical URL").option("--meta-title <title>", "Meta title").option("--meta-description <desc>", "Meta description").action(async function() {
|
|
1187
|
+
const json = isJsonMode(this);
|
|
1188
|
+
try {
|
|
1189
|
+
const opts = this.opts();
|
|
1190
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1191
|
+
let contentHtml = opts.content;
|
|
1192
|
+
if (opts.contentFile) {
|
|
1193
|
+
contentHtml = fs3.readFileSync(opts.contentFile, "utf-8");
|
|
1194
|
+
}
|
|
1195
|
+
const input = { title: opts.title };
|
|
1196
|
+
if (opts.slug) input.slug = opts.slug;
|
|
1197
|
+
if (opts.description) input.description = opts.description;
|
|
1198
|
+
if (contentHtml) input.content_html = contentHtml;
|
|
1199
|
+
if (opts.notionUrl) input.notion_url = opts.notionUrl;
|
|
1200
|
+
if (opts.published) input.published = true;
|
|
1201
|
+
if (opts.canonicalUrl) input.canonical_url = opts.canonicalUrl;
|
|
1202
|
+
if (opts.metaTitle) input.meta_title = opts.metaTitle;
|
|
1203
|
+
if (opts.metaDescription) input.meta_description = opts.metaDescription;
|
|
1204
|
+
if (opts.tagIds) input.tag_ids = opts.tagIds.split(",").map(Number);
|
|
1205
|
+
if (opts.authorIds) input.author_ids = opts.authorIds.split(",");
|
|
1206
|
+
const { data } = await endpoint.create(input);
|
|
1207
|
+
if (json) {
|
|
1208
|
+
printJson(data);
|
|
1209
|
+
} else {
|
|
1210
|
+
printSuccess(`Post created: "${data.title}" (ID: ${data.id})`);
|
|
1211
|
+
printDetail(formatPost(data));
|
|
1212
|
+
}
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
handleError(error, json);
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
posts.command("update <id>").description("Update post fields (title, slug, content, SEO metadata)").option("-t, --title <title>", "Post title").option("-s, --slug <slug>", "Post slug").option("-d, --description <desc>", "Post description").option("--content <html>", "HTML content").option("--content-file <path>", "Read HTML content from file").option("--canonical-url <url>", "Canonical URL").option("--meta-title <title>", "Meta title").option("--meta-description <desc>", "Meta description").action(async function(id) {
|
|
1218
|
+
const json = isJsonMode(this);
|
|
1219
|
+
try {
|
|
1220
|
+
const opts = this.opts();
|
|
1221
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1222
|
+
let contentHtml = opts.content;
|
|
1223
|
+
if (opts.contentFile) {
|
|
1224
|
+
contentHtml = fs3.readFileSync(opts.contentFile, "utf-8");
|
|
1225
|
+
}
|
|
1226
|
+
const input = {};
|
|
1227
|
+
if (opts.title) input.title = opts.title;
|
|
1228
|
+
if (opts.slug) input.slug = opts.slug;
|
|
1229
|
+
if (opts.description) input.description = opts.description;
|
|
1230
|
+
if (contentHtml) input.content_html = contentHtml;
|
|
1231
|
+
if (opts.canonicalUrl !== void 0) input.canonical_url = opts.canonicalUrl || null;
|
|
1232
|
+
if (opts.metaTitle !== void 0) input.meta_title = opts.metaTitle || null;
|
|
1233
|
+
if (opts.metaDescription !== void 0) input.meta_description = opts.metaDescription || null;
|
|
1234
|
+
if (Object.keys(input).length === 0) {
|
|
1235
|
+
throw new Error("No fields to update. Provide at least one option.");
|
|
1236
|
+
}
|
|
1237
|
+
const { data } = await endpoint.update(id, input);
|
|
1238
|
+
if (json) {
|
|
1239
|
+
printJson(data);
|
|
1240
|
+
} else {
|
|
1241
|
+
printSuccess(`Post updated: "${data.title}" (ID: ${data.id})`);
|
|
1242
|
+
}
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
handleError(error, json);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
posts.command("delete <id>").description("Permanently delete a post").action(async function(id) {
|
|
1248
|
+
const json = isJsonMode(this);
|
|
1249
|
+
try {
|
|
1250
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1251
|
+
await endpoint.delete(id);
|
|
1252
|
+
if (json) {
|
|
1253
|
+
printJson({ success: true, id });
|
|
1254
|
+
} else {
|
|
1255
|
+
printSuccess(`Post ${id} deleted.`);
|
|
1256
|
+
}
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
handleError(error, json);
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
posts.command("publish <id>").description("Set post as published (immediately visible)").action(async function(id) {
|
|
1262
|
+
const json = isJsonMode(this);
|
|
1263
|
+
try {
|
|
1264
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1265
|
+
const { data } = await endpoint.publish(id);
|
|
1266
|
+
if (json) {
|
|
1267
|
+
printJson(data);
|
|
1268
|
+
} else {
|
|
1269
|
+
printSuccess(`Post "${data.title}" published.`);
|
|
1270
|
+
}
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
handleError(error, json);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
posts.command("unpublish <id>").description("Revert published post to draft").action(async function(id) {
|
|
1276
|
+
const json = isJsonMode(this);
|
|
1277
|
+
try {
|
|
1278
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1279
|
+
const { data } = await endpoint.unpublish(id);
|
|
1280
|
+
if (json) {
|
|
1281
|
+
printJson(data);
|
|
1282
|
+
} else {
|
|
1283
|
+
printSuccess(`Post "${data.title}" unpublished.`);
|
|
1284
|
+
}
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
handleError(error, json);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
posts.command("schedule <id>").description("Schedule post for future publishing (ISO 8601 date)").requiredOption("--at <iso-date>", "ISO 8601 date for scheduled publish").action(async function(id) {
|
|
1290
|
+
const json = isJsonMode(this);
|
|
1291
|
+
try {
|
|
1292
|
+
const opts = this.opts();
|
|
1293
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1294
|
+
const { data } = await endpoint.schedule(id, opts.at);
|
|
1295
|
+
if (json) {
|
|
1296
|
+
printJson(data);
|
|
1297
|
+
} else {
|
|
1298
|
+
printSuccess(`Post "${data.title}" scheduled for ${opts.at}.`);
|
|
1299
|
+
}
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
handleError(error, json);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
posts.command("tags <id>").description("List tags for a post").action(async function(id) {
|
|
1305
|
+
const json = isJsonMode(this);
|
|
1306
|
+
try {
|
|
1307
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1308
|
+
const { data } = await endpoint.listTags(id);
|
|
1309
|
+
if (json) {
|
|
1310
|
+
printJson(data);
|
|
1311
|
+
} else {
|
|
1312
|
+
printTable(["ID", "Name", "Slug"], data.map((t) => [t.id, t.name, t.slug]));
|
|
1313
|
+
}
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
handleError(error, json);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
posts.command("add-tags <id>").description("Add tags to a post").requiredOption("--tag-ids <ids>", "Comma-separated tag IDs").action(async function(id) {
|
|
1319
|
+
const json = isJsonMode(this);
|
|
1320
|
+
try {
|
|
1321
|
+
const opts = this.opts();
|
|
1322
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1323
|
+
const tagIds = opts.tagIds.split(",").map(Number);
|
|
1324
|
+
const { data } = await endpoint.addTags(id, tagIds);
|
|
1325
|
+
if (json) {
|
|
1326
|
+
printJson(data);
|
|
1327
|
+
} else {
|
|
1328
|
+
printSuccess(`Tags added to post ${id}.`);
|
|
1329
|
+
}
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
handleError(error, json);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
posts.command("remove-tag <postId> <tagId>").description("Remove a tag from a post").action(async function(postId, tagId) {
|
|
1335
|
+
const json = isJsonMode(this);
|
|
1336
|
+
try {
|
|
1337
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1338
|
+
await endpoint.removeTag(postId, tagId);
|
|
1339
|
+
if (json) {
|
|
1340
|
+
printJson({ success: true, postId, tagId });
|
|
1341
|
+
} else {
|
|
1342
|
+
printSuccess(`Tag ${tagId} removed from post ${postId}.`);
|
|
1343
|
+
}
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
handleError(error, json);
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
posts.command("authors <id>").description("List authors for a post").action(async function(id) {
|
|
1349
|
+
const json = isJsonMode(this);
|
|
1350
|
+
try {
|
|
1351
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1352
|
+
const { data } = await endpoint.listAuthors(id);
|
|
1353
|
+
if (json) {
|
|
1354
|
+
printJson(data);
|
|
1355
|
+
} else {
|
|
1356
|
+
printTable(
|
|
1357
|
+
["ID", "Name", "Avatar URL"],
|
|
1358
|
+
data.map((a) => [a.id, a.author_name, truncate(a.avatar_url, 40)])
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
handleError(error, json);
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
posts.command("add-authors <id>").description("Add authors to a post").requiredOption("--author-ids <ids>", "Comma-separated author IDs").action(async function(id) {
|
|
1366
|
+
const json = isJsonMode(this);
|
|
1367
|
+
try {
|
|
1368
|
+
const opts = this.opts();
|
|
1369
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1370
|
+
const authorIds = opts.authorIds.split(",");
|
|
1371
|
+
const { data } = await endpoint.addAuthors(id, authorIds);
|
|
1372
|
+
if (json) {
|
|
1373
|
+
printJson(data);
|
|
1374
|
+
} else {
|
|
1375
|
+
printSuccess(`Authors added to post ${id}.`);
|
|
1376
|
+
}
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
handleError(error, json);
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
posts.command("remove-author <postId> <authorId>").description("Remove an author from a post").action(async function(postId, authorId) {
|
|
1382
|
+
const json = isJsonMode(this);
|
|
1383
|
+
try {
|
|
1384
|
+
const { posts: endpoint } = createClientFromCommand(this);
|
|
1385
|
+
await endpoint.removeAuthor(postId, authorId);
|
|
1386
|
+
if (json) {
|
|
1387
|
+
printJson({ success: true, postId, authorId });
|
|
1388
|
+
} else {
|
|
1389
|
+
printSuccess(`Author ${authorId} removed from post ${postId}.`);
|
|
1390
|
+
}
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
handleError(error, json);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/commands/tags.ts
|
|
1398
|
+
function registerTagsCommands(program2) {
|
|
1399
|
+
const tags = program2.command("tags").description("CRUD tags (no pagination, sorted by priority)");
|
|
1400
|
+
tags.command("list").description("List all tags (returns all, sorted by priority)").action(async function() {
|
|
1401
|
+
const json = isJsonMode(this);
|
|
1402
|
+
try {
|
|
1403
|
+
const { tags: endpoint } = createClientFromCommand(this);
|
|
1404
|
+
const { data } = await endpoint.list();
|
|
1405
|
+
if (json) {
|
|
1406
|
+
printJson(data);
|
|
1407
|
+
} else {
|
|
1408
|
+
printTable(
|
|
1409
|
+
["ID", "Name", "Slug", "Priority"],
|
|
1410
|
+
data.map((t) => [t.id, t.name, t.slug, t.priority])
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
} catch (error) {
|
|
1414
|
+
handleError(error, json);
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
tags.command("get <id>").description("Get a tag by ID").action(async function(id) {
|
|
1418
|
+
const json = isJsonMode(this);
|
|
1419
|
+
try {
|
|
1420
|
+
const { tags: endpoint } = createClientFromCommand(this);
|
|
1421
|
+
const { data } = await endpoint.get(id);
|
|
1422
|
+
if (json) {
|
|
1423
|
+
printJson(data);
|
|
1424
|
+
} else {
|
|
1425
|
+
printDetail([
|
|
1426
|
+
["ID", data.id],
|
|
1427
|
+
["Name", data.name],
|
|
1428
|
+
["Slug", data.slug],
|
|
1429
|
+
["Priority", data.priority]
|
|
1430
|
+
]);
|
|
1431
|
+
}
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
handleError(error, json);
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
tags.command("create").description("Create a new tag").requiredOption("-n, --name <name>", "Tag name").option("-s, --slug <slug>", "Tag slug").option("-p, --priority <number>", "Tag priority").action(async function() {
|
|
1437
|
+
const json = isJsonMode(this);
|
|
1438
|
+
try {
|
|
1439
|
+
const opts = this.opts();
|
|
1440
|
+
const { tags: endpoint } = createClientFromCommand(this);
|
|
1441
|
+
const input = { name: opts.name };
|
|
1442
|
+
if (opts.slug) input.slug = opts.slug;
|
|
1443
|
+
if (opts.priority !== void 0) input.priority = parseInt(opts.priority, 10);
|
|
1444
|
+
const { data } = await endpoint.create(input);
|
|
1445
|
+
if (json) {
|
|
1446
|
+
printJson(data);
|
|
1447
|
+
} else {
|
|
1448
|
+
printSuccess(`Tag created: "${data.name}" (ID: ${data.id})`);
|
|
1449
|
+
}
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
handleError(error, json);
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
tags.command("update <id>").description("Update a tag").option("-n, --name <name>", "Tag name").option("-s, --slug <slug>", "Tag slug").option("-p, --priority <number>", "Tag priority").action(async function(id) {
|
|
1455
|
+
const json = isJsonMode(this);
|
|
1456
|
+
try {
|
|
1457
|
+
const opts = this.opts();
|
|
1458
|
+
const { tags: endpoint } = createClientFromCommand(this);
|
|
1459
|
+
const input = {};
|
|
1460
|
+
if (opts.name) input.name = opts.name;
|
|
1461
|
+
if (opts.slug) input.slug = opts.slug;
|
|
1462
|
+
if (opts.priority !== void 0) input.priority = parseInt(opts.priority, 10);
|
|
1463
|
+
if (Object.keys(input).length === 0) {
|
|
1464
|
+
throw new Error("No fields to update. Provide at least one option.");
|
|
1465
|
+
}
|
|
1466
|
+
const { data } = await endpoint.update(id, input);
|
|
1467
|
+
if (json) {
|
|
1468
|
+
printJson(data);
|
|
1469
|
+
} else {
|
|
1470
|
+
printSuccess(`Tag updated: "${data.name}" (ID: ${data.id})`);
|
|
1471
|
+
}
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
handleError(error, json);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
tags.command("delete <id>").description("Delete a tag").action(async function(id) {
|
|
1477
|
+
const json = isJsonMode(this);
|
|
1478
|
+
try {
|
|
1479
|
+
const { tags: endpoint } = createClientFromCommand(this);
|
|
1480
|
+
await endpoint.delete(id);
|
|
1481
|
+
if (json) {
|
|
1482
|
+
printJson({ success: true, id });
|
|
1483
|
+
} else {
|
|
1484
|
+
printSuccess(`Tag ${id} deleted.`);
|
|
1485
|
+
}
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
handleError(error, json);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// src/commands/authors.ts
|
|
1493
|
+
function registerAuthorsCommands(program2) {
|
|
1494
|
+
const authors = program2.command("authors").description("List, view, update authors (create not available)");
|
|
1495
|
+
authors.command("list").description("List authors (only profiles with posts, UUID IDs)").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "10").action(async function() {
|
|
1496
|
+
const json = isJsonMode(this);
|
|
1497
|
+
try {
|
|
1498
|
+
const opts = this.opts();
|
|
1499
|
+
const { authors: endpoint } = createClientFromCommand(this);
|
|
1500
|
+
const { data, meta } = await endpoint.list({
|
|
1501
|
+
page: parseInt(opts.page, 10),
|
|
1502
|
+
limit: parseInt(opts.limit, 10)
|
|
1503
|
+
});
|
|
1504
|
+
if (json) {
|
|
1505
|
+
printJson({ data, meta });
|
|
1506
|
+
} else {
|
|
1507
|
+
printTable(
|
|
1508
|
+
["ID", "Name", "Avatar"],
|
|
1509
|
+
data.map((a) => [a.id, a.author_name, truncate(a.avatar_url, 40)])
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
handleError(error, json);
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
authors.command("get <id>").description("Get an author by ID").action(async function(id) {
|
|
1517
|
+
const json = isJsonMode(this);
|
|
1518
|
+
try {
|
|
1519
|
+
const { authors: endpoint } = createClientFromCommand(this);
|
|
1520
|
+
const { data } = await endpoint.get(id);
|
|
1521
|
+
if (json) {
|
|
1522
|
+
printJson(data);
|
|
1523
|
+
} else {
|
|
1524
|
+
printDetail([
|
|
1525
|
+
["ID", data.id],
|
|
1526
|
+
["Name", data.author_name],
|
|
1527
|
+
["Avatar URL", data.avatar_url]
|
|
1528
|
+
]);
|
|
1529
|
+
}
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
handleError(error, json);
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
authors.command("update <id>").description("Update an author").option("-n, --name <name>", "Author name").option("--avatar-url <url>", "Avatar URL").action(async function(id) {
|
|
1535
|
+
const json = isJsonMode(this);
|
|
1536
|
+
try {
|
|
1537
|
+
const opts = this.opts();
|
|
1538
|
+
const { authors: endpoint } = createClientFromCommand(this);
|
|
1539
|
+
const input = {};
|
|
1540
|
+
if (opts.name) input.author_name = opts.name;
|
|
1541
|
+
if (opts.avatarUrl !== void 0) input.avatar_url = opts.avatarUrl || null;
|
|
1542
|
+
if (Object.keys(input).length === 0) {
|
|
1543
|
+
throw new Error("No fields to update. Provide at least one option.");
|
|
1544
|
+
}
|
|
1545
|
+
const { data } = await endpoint.update(id, input);
|
|
1546
|
+
if (json) {
|
|
1547
|
+
printJson(data);
|
|
1548
|
+
} else {
|
|
1549
|
+
printSuccess(`Author updated: "${data.author_name}" (ID: ${data.id})`);
|
|
1550
|
+
}
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
handleError(error, json);
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// src/commands/blogs.ts
|
|
1558
|
+
var import_prompts2 = require("@inquirer/prompts");
|
|
1559
|
+
function registerBlogsCommands(program2) {
|
|
1560
|
+
const blogs = program2.command("blogs").description("Manage blogs \u2014 list, switch, view, and update blog settings");
|
|
1561
|
+
blogs.command("me").description("Show blog info (title, subdomain, plan, domain)").action(async function() {
|
|
1562
|
+
const json = isJsonMode(this);
|
|
1563
|
+
try {
|
|
1564
|
+
const { blogs: endpoint } = createClientFromCommand(this);
|
|
1565
|
+
const { data } = await endpoint.me();
|
|
1566
|
+
if (json) {
|
|
1567
|
+
printJson(data);
|
|
1568
|
+
} else {
|
|
1569
|
+
printDetail([
|
|
1570
|
+
["ID", data.id],
|
|
1571
|
+
["Title", data.title],
|
|
1572
|
+
["Subdomain", data.subdomain],
|
|
1573
|
+
["Description", data.description],
|
|
1574
|
+
["Plan", data.plan],
|
|
1575
|
+
["Language", data.blog_language],
|
|
1576
|
+
["Custom Domain", data.custom_domain],
|
|
1577
|
+
["Domain Verified", data.custom_domain_verified],
|
|
1578
|
+
["Logo", data.logo_url],
|
|
1579
|
+
["Favicon", data.favicon],
|
|
1580
|
+
["OG Image", data.og_image],
|
|
1581
|
+
["GA ID", data.ga_measurement_id],
|
|
1582
|
+
["Search Console", data.is_search_console_connected ? `Connected (${data.search_console_url})` : "Not connected"],
|
|
1583
|
+
["Created At", data.created_at]
|
|
1584
|
+
]);
|
|
1585
|
+
}
|
|
1586
|
+
} catch (error) {
|
|
1587
|
+
handleError(error, json);
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
blogs.command("list").description("List all blogs you have access to (OAuth only)").action(async function() {
|
|
1591
|
+
const json = isJsonMode(this);
|
|
1592
|
+
try {
|
|
1593
|
+
const session = readSession();
|
|
1594
|
+
if (!session) {
|
|
1595
|
+
throw new Error("OAuth session required. Run `inblog auth login` first.");
|
|
1596
|
+
}
|
|
1597
|
+
const accessToken = await getValidAccessToken();
|
|
1598
|
+
if (!accessToken) {
|
|
1599
|
+
throw new Error("Session expired. Run `inblog auth login` to re-authenticate.");
|
|
1600
|
+
}
|
|
1601
|
+
const config = readConfig();
|
|
1602
|
+
const baseUrl = this.optsWithGlobals().baseUrl || config.baseUrl || "https://inblog.ai";
|
|
1603
|
+
const response = await fetch(`${baseUrl}/api/v1/user/blogs`, {
|
|
1604
|
+
headers: {
|
|
1605
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1606
|
+
"X-Blog-Id": "0",
|
|
1607
|
+
Accept: "application/vnd.api+json"
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
if (!response.ok) {
|
|
1611
|
+
throw new Error(`Failed to fetch blogs: HTTP ${response.status}`);
|
|
1612
|
+
}
|
|
1613
|
+
const blogsData = await response.json();
|
|
1614
|
+
const blogsList = blogsData.data || [];
|
|
1615
|
+
if (json) {
|
|
1616
|
+
printJson(blogsList.map((b) => ({
|
|
1617
|
+
id: parseInt(b.id, 10),
|
|
1618
|
+
...b.attributes
|
|
1619
|
+
})));
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
if (blogsList.length === 0) {
|
|
1623
|
+
printWarning("No blogs found.");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const activeBlogId = session.activeBlogId;
|
|
1627
|
+
printTable(
|
|
1628
|
+
["", "ID", "Title", "Subdomain", "Permission", "Plan"],
|
|
1629
|
+
blogsList.map((b) => {
|
|
1630
|
+
const id = parseInt(b.id, 10);
|
|
1631
|
+
const active = id === activeBlogId ? "*" : "";
|
|
1632
|
+
return [active, id, b.attributes.title, b.attributes.subdomain, b.attributes.permission, b.attributes.plan];
|
|
1633
|
+
})
|
|
1634
|
+
);
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
handleError(error, json);
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
blogs.command("switch [blog-id]").description("Switch active blog (OAuth only)").action(async function(blogIdArg) {
|
|
1640
|
+
const json = isJsonMode(this);
|
|
1641
|
+
try {
|
|
1642
|
+
const session = readSession();
|
|
1643
|
+
if (!session) {
|
|
1644
|
+
throw new Error("OAuth session required. Run `inblog auth login` first.");
|
|
1645
|
+
}
|
|
1646
|
+
const accessToken = await getValidAccessToken();
|
|
1647
|
+
if (!accessToken) {
|
|
1648
|
+
throw new Error("Session expired. Run `inblog auth login` to re-authenticate.");
|
|
1649
|
+
}
|
|
1650
|
+
const config = readConfig();
|
|
1651
|
+
const baseUrl = this.optsWithGlobals().baseUrl || config.baseUrl || "https://inblog.ai";
|
|
1652
|
+
const response = await fetch(`${baseUrl}/api/v1/user/blogs`, {
|
|
1653
|
+
headers: {
|
|
1654
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1655
|
+
"X-Blog-Id": "0",
|
|
1656
|
+
Accept: "application/vnd.api+json"
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
if (!response.ok) {
|
|
1660
|
+
throw new Error(`Failed to fetch blogs: HTTP ${response.status}`);
|
|
1661
|
+
}
|
|
1662
|
+
const blogsData = await response.json();
|
|
1663
|
+
const blogsList = blogsData.data || [];
|
|
1664
|
+
if (blogsList.length === 0) {
|
|
1665
|
+
throw new Error("No blogs found for your account.");
|
|
1666
|
+
}
|
|
1667
|
+
let selectedBlogId;
|
|
1668
|
+
let selectedSubdomain;
|
|
1669
|
+
if (blogIdArg) {
|
|
1670
|
+
const target = blogsList.find((b) => b.id === blogIdArg || b.attributes.subdomain === blogIdArg);
|
|
1671
|
+
if (!target) {
|
|
1672
|
+
throw new Error(`Blog not found: ${blogIdArg}`);
|
|
1673
|
+
}
|
|
1674
|
+
selectedBlogId = parseInt(target.id, 10);
|
|
1675
|
+
selectedSubdomain = target.attributes.subdomain;
|
|
1676
|
+
} else {
|
|
1677
|
+
if (json) {
|
|
1678
|
+
throw new Error("Blog ID is required in --json mode. Usage: inblog blogs switch <blog-id>");
|
|
1679
|
+
}
|
|
1680
|
+
const choices = blogsList.map((b) => ({
|
|
1681
|
+
name: `${b.attributes.title} (${b.attributes.subdomain}) [${b.attributes.permission}]`,
|
|
1682
|
+
value: { id: parseInt(b.id, 10), subdomain: b.attributes.subdomain }
|
|
1683
|
+
}));
|
|
1684
|
+
const selected = await (0, import_prompts2.select)({
|
|
1685
|
+
message: "Select a blog:",
|
|
1686
|
+
choices
|
|
1687
|
+
});
|
|
1688
|
+
selectedBlogId = selected.id;
|
|
1689
|
+
selectedSubdomain = selected.subdomain;
|
|
1690
|
+
}
|
|
1691
|
+
const targetBlog = blogsList.find((b) => b.id === String(selectedBlogId));
|
|
1692
|
+
const activeBlogPlan = targetBlog?.attributes.plan;
|
|
1693
|
+
setActiveBlog(selectedBlogId, selectedSubdomain, activeBlogPlan);
|
|
1694
|
+
if (json) {
|
|
1695
|
+
printJson({ success: true, blogId: selectedBlogId, subdomain: selectedSubdomain });
|
|
1696
|
+
} else {
|
|
1697
|
+
printSuccess(`Switched to blog: ${selectedSubdomain}`);
|
|
1698
|
+
}
|
|
1699
|
+
if (activeBlogPlan !== "team" && activeBlogPlan !== "enterprise") {
|
|
1700
|
+
printWarning(`Blog "${selectedSubdomain}" is on the ${activeBlogPlan || "free"} plan.`);
|
|
1701
|
+
printWarning(" CLI features require a Team plan or above.");
|
|
1702
|
+
printWarning(` Upgrade: https://inblog.ai/dashboard/${selectedSubdomain}/settings/billing`);
|
|
1703
|
+
}
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
handleError(error, json);
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
blogs.command("update").description("Update blog title, description, language, or timezone").option("-t, --title <title>", "Blog title").option("-d, --description <desc>", "Blog description").option("--language <lang>", "Blog language").option("--timezone-diff <hours>", "Timezone offset in hours").option("--logo <url>", "Blog logo image URL").option("--favicon <url>", "Blog favicon image URL").option("--og-image <url>", "Blog OG image URL").option("--ga-id <id>", "Google Analytics measurement ID").action(async function() {
|
|
1709
|
+
const json = isJsonMode(this);
|
|
1710
|
+
try {
|
|
1711
|
+
const opts = this.opts();
|
|
1712
|
+
const ctx = createClientFromCommand(this);
|
|
1713
|
+
const { data: blog } = await ctx.blogs.me();
|
|
1714
|
+
const input = {};
|
|
1715
|
+
if (opts.title) input.title = opts.title;
|
|
1716
|
+
if (opts.description) input.description = opts.description;
|
|
1717
|
+
if (opts.language) input.blog_language = opts.language;
|
|
1718
|
+
if (opts.timezoneDiff !== void 0) input.timezone_diff = parseInt(opts.timezoneDiff, 10);
|
|
1719
|
+
if (opts.logo) input.logo = opts.logo;
|
|
1720
|
+
if (opts.favicon) input.favicon = opts.favicon;
|
|
1721
|
+
if (opts.ogImage) input.og_image = opts.ogImage;
|
|
1722
|
+
if (opts.gaId) input.ga_measurement_id = opts.gaId;
|
|
1723
|
+
if (Object.keys(input).length === 0) {
|
|
1724
|
+
throw new Error("No fields to update. Provide at least one option.");
|
|
1725
|
+
}
|
|
1726
|
+
const { data } = await ctx.blogs.update(blog.subdomain, input);
|
|
1727
|
+
if (json) {
|
|
1728
|
+
printJson(data);
|
|
1729
|
+
} else {
|
|
1730
|
+
printSuccess(`Blog updated: "${data.title}"`);
|
|
1731
|
+
}
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
handleError(error, json);
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
const domain = blogs.command("domain").description("Manage custom domain");
|
|
1737
|
+
domain.command("connect <domain>").description("Connect a custom domain").action(async function(domainArg) {
|
|
1738
|
+
const json = isJsonMode(this);
|
|
1739
|
+
try {
|
|
1740
|
+
const ctx = createClientFromCommand(this);
|
|
1741
|
+
const result = await ctx.blogs.domainConnect(domainArg);
|
|
1742
|
+
if (json) {
|
|
1743
|
+
printJson(result);
|
|
1744
|
+
} else {
|
|
1745
|
+
printSuccess(`Custom domain requested: ${domainArg}`);
|
|
1746
|
+
if (result.dns_records && result.dns_records.length > 0) {
|
|
1747
|
+
printTable(
|
|
1748
|
+
["Type", "Name", "Value"],
|
|
1749
|
+
result.dns_records.map((r) => [r.type, r.name, r.value])
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
printWarning("DNS \uC804\uD30C \uD6C4 `inblog blogs domain status`\uB85C \uD655\uC778\uD558\uC138\uC694.");
|
|
1753
|
+
}
|
|
1754
|
+
} catch (error) {
|
|
1755
|
+
handleError(error, json);
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
domain.command("status").description("Check custom domain verification status").action(async function() {
|
|
1759
|
+
const json = isJsonMode(this);
|
|
1760
|
+
try {
|
|
1761
|
+
const ctx = createClientFromCommand(this);
|
|
1762
|
+
const result = await ctx.blogs.domainStatus();
|
|
1763
|
+
if (json) {
|
|
1764
|
+
printJson(result);
|
|
1765
|
+
} else {
|
|
1766
|
+
if (!result.custom_domain) {
|
|
1767
|
+
printWarning("No custom domain configured.");
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
printDetail([
|
|
1771
|
+
["Domain", result.custom_domain],
|
|
1772
|
+
["Verified", result.verified],
|
|
1773
|
+
["SSL Status", result.ssl_status]
|
|
1774
|
+
]);
|
|
1775
|
+
if (result.dns_records && result.dns_records.length > 0) {
|
|
1776
|
+
printTable(
|
|
1777
|
+
["Type", "Name", "Value"],
|
|
1778
|
+
result.dns_records.map((r) => [r.type, r.name, r.value])
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
handleError(error, json);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
domain.command("disconnect").description("Disconnect custom domain").action(async function() {
|
|
1787
|
+
const json = isJsonMode(this);
|
|
1788
|
+
try {
|
|
1789
|
+
const ctx = createClientFromCommand(this);
|
|
1790
|
+
await ctx.blogs.domainDisconnect();
|
|
1791
|
+
if (json) {
|
|
1792
|
+
printJson({ success: true });
|
|
1793
|
+
} else {
|
|
1794
|
+
printSuccess("Custom domain disconnected.");
|
|
1795
|
+
}
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
handleError(error, json);
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
const banner = blogs.command("banner").description("Manage blog banner settings");
|
|
1801
|
+
banner.command("get").description("Show current banner settings").action(async function() {
|
|
1802
|
+
const json = isJsonMode(this);
|
|
1803
|
+
try {
|
|
1804
|
+
const ctx = createClientFromCommand(this);
|
|
1805
|
+
const result = await ctx.blogs.getCustomUi();
|
|
1806
|
+
if (json) {
|
|
1807
|
+
printJson(result);
|
|
1808
|
+
} else {
|
|
1809
|
+
printDetail([
|
|
1810
|
+
["Banner Image", result.banner_url],
|
|
1811
|
+
["Title", result.banner_title],
|
|
1812
|
+
["Subtext", result.banner_subtext],
|
|
1813
|
+
["Title Color", result.banner_title_color],
|
|
1814
|
+
["Background Color", result.banner_bg_color]
|
|
1815
|
+
]);
|
|
1816
|
+
}
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
handleError(error, json);
|
|
1819
|
+
}
|
|
1820
|
+
});
|
|
1821
|
+
banner.command("set").description("Update banner settings").option("--image <url>", "Banner image URL").option("--title <text>", "Banner title text").option("--subtext <text>", "Banner subtext").option("--title-color <hex>", "Banner title color (hex)").option("--bg-color <hex>", "Banner background color (hex)").action(async function() {
|
|
1822
|
+
const json = isJsonMode(this);
|
|
1823
|
+
try {
|
|
1824
|
+
const opts = this.opts();
|
|
1825
|
+
const input = {};
|
|
1826
|
+
if (opts.image) input.banner_url = opts.image;
|
|
1827
|
+
if (opts.title) input.banner_title = opts.title;
|
|
1828
|
+
if (opts.subtext) input.banner_subtext = opts.subtext;
|
|
1829
|
+
if (opts.titleColor) input.banner_title_color = opts.titleColor;
|
|
1830
|
+
if (opts.bgColor) input.banner_bg_color = opts.bgColor;
|
|
1831
|
+
if (Object.keys(input).length === 0) {
|
|
1832
|
+
throw new Error("No fields to update. Provide at least one option (--image, --title, --subtext, --title-color, --bg-color).");
|
|
1833
|
+
}
|
|
1834
|
+
const ctx = createClientFromCommand(this);
|
|
1835
|
+
const result = await ctx.blogs.updateCustomUi(input);
|
|
1836
|
+
if (json) {
|
|
1837
|
+
printJson(result);
|
|
1838
|
+
} else {
|
|
1839
|
+
printSuccess("Banner updated.");
|
|
1840
|
+
printDetail([
|
|
1841
|
+
["Banner Image", result.banner_url],
|
|
1842
|
+
["Title", result.banner_title],
|
|
1843
|
+
["Subtext", result.banner_subtext],
|
|
1844
|
+
["Title Color", result.banner_title_color],
|
|
1845
|
+
["Background Color", result.banner_bg_color]
|
|
1846
|
+
]);
|
|
1847
|
+
}
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
handleError(error, json);
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
banner.command("remove").description("Remove banner settings").action(async function() {
|
|
1853
|
+
const json = isJsonMode(this);
|
|
1854
|
+
try {
|
|
1855
|
+
const ctx = createClientFromCommand(this);
|
|
1856
|
+
await ctx.blogs.updateCustomUi({
|
|
1857
|
+
banner_url: null,
|
|
1858
|
+
banner_title: null,
|
|
1859
|
+
banner_subtext: null
|
|
1860
|
+
});
|
|
1861
|
+
if (json) {
|
|
1862
|
+
printJson({ success: true });
|
|
1863
|
+
} else {
|
|
1864
|
+
printSuccess("Banner removed.");
|
|
1865
|
+
}
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
handleError(error, json);
|
|
1868
|
+
}
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// src/commands/redirects.ts
|
|
1873
|
+
function registerRedirectsCommands(program2) {
|
|
1874
|
+
const redirects = program2.command("redirects").description("CRUD URL redirects (307 temporary, 308 permanent)");
|
|
1875
|
+
redirects.command("list").description("List redirects").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "50").action(async function() {
|
|
1876
|
+
const json = isJsonMode(this);
|
|
1877
|
+
try {
|
|
1878
|
+
const opts = this.opts();
|
|
1879
|
+
const { redirects: endpoint } = createClientFromCommand(this);
|
|
1880
|
+
const { data, meta } = await endpoint.list({
|
|
1881
|
+
page: parseInt(opts.page, 10),
|
|
1882
|
+
limit: parseInt(opts.limit, 10)
|
|
1883
|
+
});
|
|
1884
|
+
if (json) {
|
|
1885
|
+
printJson({ data, meta });
|
|
1886
|
+
} else {
|
|
1887
|
+
printTable(
|
|
1888
|
+
["ID", "From", "To", "Type"],
|
|
1889
|
+
data.map((r) => [r.id, r.from_path, r.to_path, r.redirect_type])
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
handleError(error, json);
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
redirects.command("get <id>").description("Get a redirect by ID").action(async function(id) {
|
|
1897
|
+
const json = isJsonMode(this);
|
|
1898
|
+
try {
|
|
1899
|
+
const { redirects: endpoint } = createClientFromCommand(this);
|
|
1900
|
+
const { data } = await endpoint.get(id);
|
|
1901
|
+
if (json) {
|
|
1902
|
+
printJson(data);
|
|
1903
|
+
} else {
|
|
1904
|
+
printDetail([
|
|
1905
|
+
["ID", data.id],
|
|
1906
|
+
["From", data.from_path],
|
|
1907
|
+
["To", data.to_path],
|
|
1908
|
+
["Type", data.redirect_type],
|
|
1909
|
+
["Created At", data.created_at],
|
|
1910
|
+
["Updated At", data.updated_at]
|
|
1911
|
+
]);
|
|
1912
|
+
}
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
handleError(error, json);
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
redirects.command("create").description("Create a redirect (default: 308 permanent)").requiredOption("--from <path>", "Source path (auto-normalized with /)").requiredOption("--to <path>", "Destination path").option("--type <type>", "Redirect type (307 or 308)", "308").action(async function() {
|
|
1918
|
+
const json = isJsonMode(this);
|
|
1919
|
+
try {
|
|
1920
|
+
const opts = this.opts();
|
|
1921
|
+
const { redirects: endpoint } = createClientFromCommand(this);
|
|
1922
|
+
const { data } = await endpoint.create({
|
|
1923
|
+
from_path: opts.from,
|
|
1924
|
+
to_path: opts.to,
|
|
1925
|
+
redirect_type: parseInt(opts.type, 10)
|
|
1926
|
+
});
|
|
1927
|
+
if (json) {
|
|
1928
|
+
printJson(data);
|
|
1929
|
+
} else {
|
|
1930
|
+
printSuccess(`Redirect created: ${data.from_path} \u2192 ${data.to_path} (${data.redirect_type})`);
|
|
1931
|
+
}
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
handleError(error, json);
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
redirects.command("update <id>").description("Update a redirect").option("--from <path>", "Source path").option("--to <path>", "Destination path").option("--type <type>", "Redirect type (307 or 308)").action(async function(id) {
|
|
1937
|
+
const json = isJsonMode(this);
|
|
1938
|
+
try {
|
|
1939
|
+
const opts = this.opts();
|
|
1940
|
+
const { redirects: endpoint } = createClientFromCommand(this);
|
|
1941
|
+
const input = {};
|
|
1942
|
+
if (opts.from) input.from_path = opts.from;
|
|
1943
|
+
if (opts.to) input.to_path = opts.to;
|
|
1944
|
+
if (opts.type) input.redirect_type = parseInt(opts.type, 10);
|
|
1945
|
+
if (Object.keys(input).length === 0) {
|
|
1946
|
+
throw new Error("No fields to update. Provide at least one option.");
|
|
1947
|
+
}
|
|
1948
|
+
const { data } = await endpoint.update(id, input);
|
|
1949
|
+
if (json) {
|
|
1950
|
+
printJson(data);
|
|
1951
|
+
} else {
|
|
1952
|
+
printSuccess(`Redirect updated: ${data.from_path} \u2192 ${data.to_path}`);
|
|
1953
|
+
}
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
handleError(error, json);
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
redirects.command("delete <id>").description("Delete a redirect").action(async function(id) {
|
|
1959
|
+
const json = isJsonMode(this);
|
|
1960
|
+
try {
|
|
1961
|
+
const { redirects: endpoint } = createClientFromCommand(this);
|
|
1962
|
+
await endpoint.delete(id);
|
|
1963
|
+
if (json) {
|
|
1964
|
+
printJson({ success: true, id });
|
|
1965
|
+
} else {
|
|
1966
|
+
printSuccess(`Redirect ${id} deleted.`);
|
|
1967
|
+
}
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
handleError(error, json);
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/commands/forms.ts
|
|
1975
|
+
function registerFormsCommands(program2) {
|
|
1976
|
+
const forms = program2.command("forms").description("View lead-capture forms (read-only)");
|
|
1977
|
+
forms.command("list").description("List forms").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "10").action(async function() {
|
|
1978
|
+
const json = isJsonMode(this);
|
|
1979
|
+
try {
|
|
1980
|
+
const opts = this.opts();
|
|
1981
|
+
const { forms: endpoint } = createClientFromCommand(this);
|
|
1982
|
+
const { data, meta } = await endpoint.list({
|
|
1983
|
+
page: parseInt(opts.page, 10),
|
|
1984
|
+
limit: parseInt(opts.limit, 10)
|
|
1985
|
+
});
|
|
1986
|
+
if (json) {
|
|
1987
|
+
printJson({ data, meta });
|
|
1988
|
+
} else {
|
|
1989
|
+
printTable(
|
|
1990
|
+
["ID", "Title", "Responses", "Created At"],
|
|
1991
|
+
data.map((f) => [f.id, truncate(f.title, 40), f.response_count, f.created_at])
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
} catch (error) {
|
|
1995
|
+
handleError(error, json);
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
forms.command("get <id>").description("Get a form by ID").action(async function(id) {
|
|
1999
|
+
const json = isJsonMode(this);
|
|
2000
|
+
try {
|
|
2001
|
+
const { forms: endpoint } = createClientFromCommand(this);
|
|
2002
|
+
const { data } = await endpoint.get(id);
|
|
2003
|
+
if (json) {
|
|
2004
|
+
printJson(data);
|
|
2005
|
+
} else {
|
|
2006
|
+
printDetail([
|
|
2007
|
+
["ID", data.id],
|
|
2008
|
+
["Title", data.title],
|
|
2009
|
+
["Magnet Type", data.magnet_type],
|
|
2010
|
+
["Responses", data.response_count],
|
|
2011
|
+
["Created At", data.created_at]
|
|
2012
|
+
]);
|
|
2013
|
+
}
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
handleError(error, json);
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
function registerFormResponsesCommands(program2) {
|
|
2020
|
+
const formResponses = program2.command("form-responses").description("View form submission data (read-only)");
|
|
2021
|
+
formResponses.command("list").description("List form responses").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "10").option("--form-id <id>", "Filter by form ID").action(async function() {
|
|
2022
|
+
const json = isJsonMode(this);
|
|
2023
|
+
try {
|
|
2024
|
+
const opts = this.opts();
|
|
2025
|
+
const { formResponses: endpoint } = createClientFromCommand(this);
|
|
2026
|
+
const filter = {};
|
|
2027
|
+
if (opts.formId) filter.form_id = opts.formId;
|
|
2028
|
+
const { data, meta } = await endpoint.list({
|
|
2029
|
+
page: parseInt(opts.page, 10),
|
|
2030
|
+
limit: parseInt(opts.limit, 10),
|
|
2031
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0
|
|
2032
|
+
});
|
|
2033
|
+
if (json) {
|
|
2034
|
+
printJson({ data, meta });
|
|
2035
|
+
} else {
|
|
2036
|
+
printTable(
|
|
2037
|
+
["ID", "Email", "Country", "Created At"],
|
|
2038
|
+
data.map((r) => [r.id, r.email, r.country, r.created_at])
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
handleError(error, json);
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
formResponses.command("get <id>").description("Get a form response by ID").action(async function(id) {
|
|
2046
|
+
const json = isJsonMode(this);
|
|
2047
|
+
try {
|
|
2048
|
+
const { formResponses: endpoint } = createClientFromCommand(this);
|
|
2049
|
+
const { data } = await endpoint.get(id);
|
|
2050
|
+
if (json) {
|
|
2051
|
+
printJson(data);
|
|
2052
|
+
} else {
|
|
2053
|
+
printDetail([
|
|
2054
|
+
["ID", data.id],
|
|
2055
|
+
["Email", data.email],
|
|
2056
|
+
["Country", data.country],
|
|
2057
|
+
["City", data.city],
|
|
2058
|
+
["Region", data.region],
|
|
2059
|
+
["Language", data.language],
|
|
2060
|
+
["Created At", data.created_at],
|
|
2061
|
+
["Response", JSON.stringify(data.response)]
|
|
2062
|
+
]);
|
|
2063
|
+
}
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
handleError(error, json);
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/commands/config.ts
|
|
2071
|
+
function registerConfigCommands(program2) {
|
|
2072
|
+
const config = program2.command("config").description("Manage CLI config (~/.config/inblog/config.json)");
|
|
2073
|
+
config.command("list").description("Show all configuration values").action(async function() {
|
|
2074
|
+
const json = isJsonMode(this);
|
|
2075
|
+
try {
|
|
2076
|
+
const cfg = readGlobalConfig();
|
|
2077
|
+
if (json) {
|
|
2078
|
+
printJson(cfg);
|
|
2079
|
+
} else {
|
|
2080
|
+
const entries = Object.entries(cfg);
|
|
2081
|
+
if (entries.length === 0) {
|
|
2082
|
+
console.log("No configuration set.");
|
|
2083
|
+
} else {
|
|
2084
|
+
printDetail(
|
|
2085
|
+
entries.map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : v])
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
} catch (error) {
|
|
2090
|
+
handleError(error, json);
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
config.command("get <key>").description("Get a configuration value").action(async function(key) {
|
|
2094
|
+
const json = isJsonMode(this);
|
|
2095
|
+
try {
|
|
2096
|
+
const cfg = readGlobalConfig();
|
|
2097
|
+
const value = cfg[key];
|
|
2098
|
+
if (json) {
|
|
2099
|
+
printJson({ [key]: value ?? null });
|
|
2100
|
+
} else if (value === void 0) {
|
|
2101
|
+
console.log(`"${key}" is not set.`);
|
|
2102
|
+
} else {
|
|
2103
|
+
console.log(value);
|
|
2104
|
+
}
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
handleError(error, json);
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
config.command("set <key> <value>").description("Set a configuration value").action(async function(key, value) {
|
|
2110
|
+
const json = isJsonMode(this);
|
|
2111
|
+
try {
|
|
2112
|
+
let parsed = value;
|
|
2113
|
+
if (value === "true") parsed = true;
|
|
2114
|
+
else if (value === "false") parsed = false;
|
|
2115
|
+
else if (!isNaN(Number(value)) && value !== "") parsed = Number(value);
|
|
2116
|
+
setGlobalConfigValue(key, parsed);
|
|
2117
|
+
if (json) {
|
|
2118
|
+
printJson({ [key]: parsed });
|
|
2119
|
+
} else {
|
|
2120
|
+
printSuccess(`Set ${key} = ${parsed}`);
|
|
2121
|
+
}
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
handleError(error, json);
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
config.command("path").description("Show configuration file paths").action(async function() {
|
|
2127
|
+
const json = isJsonMode(this);
|
|
2128
|
+
try {
|
|
2129
|
+
const globalPath = getGlobalConfigPath();
|
|
2130
|
+
const localPath = getLocalConfigPath();
|
|
2131
|
+
if (json) {
|
|
2132
|
+
printJson({ global: globalPath, local: localPath });
|
|
2133
|
+
} else {
|
|
2134
|
+
printDetail([
|
|
2135
|
+
["Global", globalPath],
|
|
2136
|
+
["Local", localPath ?? "(none)"]
|
|
2137
|
+
]);
|
|
2138
|
+
}
|
|
2139
|
+
} catch (error) {
|
|
2140
|
+
handleError(error, json);
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
// src/commands/search-console.ts
|
|
2146
|
+
var import_open2 = __toESM(require("open"));
|
|
2147
|
+
function registerSearchConsoleCommands(program2) {
|
|
2148
|
+
const sc = program2.command("search-console").description("Google Search Console integration");
|
|
2149
|
+
sc.command("connect").description("Connect Google Search Console via OAuth").action(async function() {
|
|
2150
|
+
const json = isJsonMode(this);
|
|
2151
|
+
try {
|
|
2152
|
+
if (json) {
|
|
2153
|
+
throw new Error("Interactive OAuth flow is not available in --json mode.");
|
|
2154
|
+
}
|
|
2155
|
+
const { searchConsole } = createClientFromCommand(this);
|
|
2156
|
+
const statusResult = await searchConsole.status();
|
|
2157
|
+
if (statusResult.connected) {
|
|
2158
|
+
printWarning("Search Console is already connected.");
|
|
2159
|
+
printDetail([
|
|
2160
|
+
["Status", "Connected"],
|
|
2161
|
+
["Property", statusResult.property || "\u2014"]
|
|
2162
|
+
]);
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const callbackPath = "/auth/gsc-callback";
|
|
2166
|
+
const serverPromise = startCallbackServer({ callbackPath });
|
|
2167
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2168
|
+
const redirectUri = `http://127.0.0.1:54321${callbackPath}`;
|
|
2169
|
+
const oauthResult = await searchConsole.oauthUrl(redirectUri);
|
|
2170
|
+
const url = oauthResult.url;
|
|
2171
|
+
if (!url) {
|
|
2172
|
+
throw new Error("Failed to retrieve OAuth URL from server.");
|
|
2173
|
+
}
|
|
2174
|
+
console.log("Opening browser for Google Search Console authorization...");
|
|
2175
|
+
await (0, import_open2.default)(url);
|
|
2176
|
+
console.log("Waiting for authorization...");
|
|
2177
|
+
const { code } = await serverPromise;
|
|
2178
|
+
await searchConsole.connect(code, redirectUri);
|
|
2179
|
+
printSuccess("Google Search Console connected successfully.");
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
handleError(error, json);
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
sc.command("status").description("Show Search Console connection status").action(async function() {
|
|
2185
|
+
const json = isJsonMode(this);
|
|
2186
|
+
try {
|
|
2187
|
+
const { searchConsole } = createClientFromCommand(this);
|
|
2188
|
+
const data = await searchConsole.status();
|
|
2189
|
+
if (json) {
|
|
2190
|
+
printJson(data);
|
|
2191
|
+
} else {
|
|
2192
|
+
printDetail([
|
|
2193
|
+
["Connected", data.connected ? "Yes" : "No"],
|
|
2194
|
+
["Property", data.property || "\u2014"]
|
|
2195
|
+
]);
|
|
2196
|
+
}
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
handleError(error, json);
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
sc.command("disconnect").description("Disconnect Google Search Console").action(async function() {
|
|
2202
|
+
const json = isJsonMode(this);
|
|
2203
|
+
try {
|
|
2204
|
+
const { searchConsole } = createClientFromCommand(this);
|
|
2205
|
+
await searchConsole.disconnect();
|
|
2206
|
+
if (json) {
|
|
2207
|
+
printJson({ success: true });
|
|
2208
|
+
} else {
|
|
2209
|
+
printSuccess("Google Search Console disconnected.");
|
|
2210
|
+
}
|
|
2211
|
+
} catch (error) {
|
|
2212
|
+
handleError(error, json);
|
|
2213
|
+
}
|
|
2214
|
+
});
|
|
2215
|
+
sc.command("keywords").description("Show keyword performance data").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--sort <field>", "Sort by: clicks, impressions, ctr, position", "clicks").option("--order <dir>", "Sort order: asc, desc", "desc").option("-l, --limit <number>", "Max results", "20").action(async function() {
|
|
2216
|
+
const json = isJsonMode(this);
|
|
2217
|
+
try {
|
|
2218
|
+
const opts = this.opts();
|
|
2219
|
+
const { searchConsole } = createClientFromCommand(this);
|
|
2220
|
+
const data = await searchConsole.keywords({
|
|
2221
|
+
start_date: opts.startDate,
|
|
2222
|
+
end_date: opts.endDate,
|
|
2223
|
+
sort: opts.sort,
|
|
2224
|
+
order: opts.order,
|
|
2225
|
+
limit: parseInt(opts.limit, 10)
|
|
2226
|
+
});
|
|
2227
|
+
if (json) {
|
|
2228
|
+
printJson(data);
|
|
2229
|
+
} else {
|
|
2230
|
+
const rows = (data.keywords || data.data || []).map((k) => [
|
|
2231
|
+
k.keyword || k.key,
|
|
2232
|
+
k.clicks,
|
|
2233
|
+
k.impressions,
|
|
2234
|
+
typeof k.ctr === "number" ? `${(k.ctr * 100).toFixed(1)}%` : k.ctr,
|
|
2235
|
+
typeof k.position === "number" ? k.position.toFixed(1) : k.position
|
|
2236
|
+
]);
|
|
2237
|
+
printTable(["Keyword", "Clicks", "Impressions", "CTR", "Position"], rows);
|
|
2238
|
+
}
|
|
2239
|
+
} catch (error) {
|
|
2240
|
+
handleError(error, json);
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
sc.command("pages").description("Show page performance data").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--sort <field>", "Sort by: clicks, impressions, ctr, position", "clicks").option("--order <dir>", "Sort order: asc, desc", "desc").option("-l, --limit <number>", "Max results", "20").action(async function() {
|
|
2244
|
+
const json = isJsonMode(this);
|
|
2245
|
+
try {
|
|
2246
|
+
const opts = this.opts();
|
|
2247
|
+
const { searchConsole } = createClientFromCommand(this);
|
|
2248
|
+
const data = await searchConsole.pages({
|
|
2249
|
+
start_date: opts.startDate,
|
|
2250
|
+
end_date: opts.endDate,
|
|
2251
|
+
sort: opts.sort,
|
|
2252
|
+
order: opts.order,
|
|
2253
|
+
limit: parseInt(opts.limit, 10)
|
|
2254
|
+
});
|
|
2255
|
+
if (json) {
|
|
2256
|
+
printJson(data);
|
|
2257
|
+
} else {
|
|
2258
|
+
const rows = (data.pages || data.data || []).map((p) => [
|
|
2259
|
+
p.page || p.key,
|
|
2260
|
+
p.clicks,
|
|
2261
|
+
p.impressions,
|
|
2262
|
+
typeof p.ctr === "number" ? `${(p.ctr * 100).toFixed(1)}%` : p.ctr,
|
|
2263
|
+
typeof p.position === "number" ? p.position.toFixed(1) : p.position
|
|
2264
|
+
]);
|
|
2265
|
+
printTable(["Page", "Clicks", "Impressions", "CTR", "Position"], rows);
|
|
2266
|
+
}
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
handleError(error, json);
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/commands/analytics.ts
|
|
2274
|
+
function registerAnalyticsCommands(program2) {
|
|
2275
|
+
const analytics = program2.command("analytics").description("Blog analytics: traffic, post metrics, referrer sources");
|
|
2276
|
+
analytics.command("traffic").description("Show blog overall traffic").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--interval <interval>", "Interval: day, hour", "day").option("--type <type>", "Page type: all, home, post, category, author", "all").action(async function() {
|
|
2277
|
+
const json = isJsonMode(this);
|
|
2278
|
+
try {
|
|
2279
|
+
const opts = this.opts();
|
|
2280
|
+
const { analytics: endpoint } = createClientFromCommand(this);
|
|
2281
|
+
const data = await endpoint.traffic({
|
|
2282
|
+
start_date: opts.startDate,
|
|
2283
|
+
end_date: opts.endDate,
|
|
2284
|
+
interval: opts.interval,
|
|
2285
|
+
type: opts.type
|
|
2286
|
+
});
|
|
2287
|
+
if (json) {
|
|
2288
|
+
printJson(data);
|
|
2289
|
+
} else {
|
|
2290
|
+
const rows = (data.data || []).map((r) => [
|
|
2291
|
+
r.date || r.period,
|
|
2292
|
+
r.visits,
|
|
2293
|
+
r.clicks,
|
|
2294
|
+
r.organic
|
|
2295
|
+
]);
|
|
2296
|
+
printTable(["Date", "Visits", "Clicks", "Organic"], rows);
|
|
2297
|
+
}
|
|
2298
|
+
} catch (error) {
|
|
2299
|
+
handleError(error, json);
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
analytics.command("posts").description("Show post-level metrics").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--sort <field>", "Sort by: visits, clicks, organic, cvr", "visits").option("--order <dir>", "Sort order: asc, desc", "desc").option("-l, --limit <number>", "Max results", "20").option("--include <fields>", "Include extra fields (e.g. title)").action(async function() {
|
|
2303
|
+
const json = isJsonMode(this);
|
|
2304
|
+
try {
|
|
2305
|
+
const opts = this.opts();
|
|
2306
|
+
const { analytics: endpoint } = createClientFromCommand(this);
|
|
2307
|
+
const data = await endpoint.posts({
|
|
2308
|
+
start_date: opts.startDate,
|
|
2309
|
+
end_date: opts.endDate,
|
|
2310
|
+
sort: opts.sort,
|
|
2311
|
+
order: opts.order,
|
|
2312
|
+
limit: parseInt(opts.limit, 10),
|
|
2313
|
+
include: opts.include
|
|
2314
|
+
});
|
|
2315
|
+
if (json) {
|
|
2316
|
+
printJson(data);
|
|
2317
|
+
} else {
|
|
2318
|
+
const hasTitle = opts.include?.includes("title");
|
|
2319
|
+
const headers = hasTitle ? ["Post ID", "Title", "Visits", "Clicks", "Organic", "CVR"] : ["Post ID", "Visits", "Clicks", "Organic", "CVR"];
|
|
2320
|
+
const rows = (data.data || []).map((r) => {
|
|
2321
|
+
const cvr = typeof r.cvr === "number" ? `${(r.cvr * 100).toFixed(1)}%` : r.cvr ?? "\u2014";
|
|
2322
|
+
if (hasTitle) {
|
|
2323
|
+
return [r.post_id ?? r.id, truncate(r.title, 40), r.visits, r.clicks, r.organic, cvr];
|
|
2324
|
+
}
|
|
2325
|
+
return [r.post_id ?? r.id, r.visits, r.clicks, r.organic, cvr];
|
|
2326
|
+
});
|
|
2327
|
+
printTable(headers, rows);
|
|
2328
|
+
}
|
|
2329
|
+
} catch (error) {
|
|
2330
|
+
handleError(error, json);
|
|
2331
|
+
}
|
|
2332
|
+
});
|
|
2333
|
+
analytics.command("sources").description("Show referrer sources").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("-l, --limit <number>", "Max results", "20").action(async function() {
|
|
2334
|
+
const json = isJsonMode(this);
|
|
2335
|
+
try {
|
|
2336
|
+
const opts = this.opts();
|
|
2337
|
+
const { analytics: endpoint } = createClientFromCommand(this);
|
|
2338
|
+
const data = await endpoint.sources({
|
|
2339
|
+
start_date: opts.startDate,
|
|
2340
|
+
end_date: opts.endDate,
|
|
2341
|
+
limit: parseInt(opts.limit, 10)
|
|
2342
|
+
});
|
|
2343
|
+
if (json) {
|
|
2344
|
+
printJson(data);
|
|
2345
|
+
} else {
|
|
2346
|
+
const rows = (data.data || []).map((r) => [
|
|
2347
|
+
r.referrer || r.source,
|
|
2348
|
+
r.count ?? r.visits
|
|
2349
|
+
]);
|
|
2350
|
+
printTable(["Referrer", "Count"], rows);
|
|
2351
|
+
}
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
handleError(error, json);
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
analytics.command("post <id>").description("Show traffic or sources for a single post").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--end-date <date>", "End date (YYYY-MM-DD)").option("--interval <interval>", "Interval: day, hour", "day").option("--sources", "Show referrer sources instead of traffic").option("-l, --limit <number>", "Max results (for --sources)", "20").action(async function(id) {
|
|
2357
|
+
const json = isJsonMode(this);
|
|
2358
|
+
try {
|
|
2359
|
+
const opts = this.opts();
|
|
2360
|
+
const { analytics: endpoint } = createClientFromCommand(this);
|
|
2361
|
+
const postId = parseInt(id, 10);
|
|
2362
|
+
if (opts.sources) {
|
|
2363
|
+
const data = await endpoint.postSources(postId, {
|
|
2364
|
+
start_date: opts.startDate,
|
|
2365
|
+
end_date: opts.endDate,
|
|
2366
|
+
limit: parseInt(opts.limit, 10)
|
|
2367
|
+
});
|
|
2368
|
+
if (json) {
|
|
2369
|
+
printJson(data);
|
|
2370
|
+
} else {
|
|
2371
|
+
const rows = (data.data || []).map((r) => [
|
|
2372
|
+
r.referrer || r.source,
|
|
2373
|
+
r.count ?? r.visits
|
|
2374
|
+
]);
|
|
2375
|
+
printTable(["Referrer", "Count"], rows);
|
|
2376
|
+
}
|
|
2377
|
+
} else {
|
|
2378
|
+
const data = await endpoint.postTraffic(postId, {
|
|
2379
|
+
start_date: opts.startDate,
|
|
2380
|
+
end_date: opts.endDate,
|
|
2381
|
+
interval: opts.interval
|
|
2382
|
+
});
|
|
2383
|
+
if (json) {
|
|
2384
|
+
printJson(data);
|
|
2385
|
+
} else {
|
|
2386
|
+
const rows = (data.data || []).map((r) => [
|
|
2387
|
+
r.date || r.period,
|
|
2388
|
+
r.visits,
|
|
2389
|
+
r.clicks,
|
|
2390
|
+
r.organic
|
|
2391
|
+
]);
|
|
2392
|
+
printTable(["Date", "Visits", "Clicks", "Organic"], rows);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
} catch (error) {
|
|
2396
|
+
handleError(error, json);
|
|
2397
|
+
}
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// bin/inblog.ts
|
|
2402
|
+
var program = new import_commander.Command();
|
|
2403
|
+
program.name("inblog").description("CLI for managing inblog.ai blog content (posts, tags, authors, redirects, forms)").version("0.2.0").option("--json", "Output as JSON (for programmatic use)").option("--base-url <url>", "API base URL").option("--no-color", "Disable colored output");
|
|
2404
|
+
registerAuthCommands(program);
|
|
2405
|
+
registerPostsCommands(program);
|
|
2406
|
+
registerTagsCommands(program);
|
|
2407
|
+
registerAuthorsCommands(program);
|
|
2408
|
+
registerBlogsCommands(program);
|
|
2409
|
+
registerRedirectsCommands(program);
|
|
2410
|
+
registerFormsCommands(program);
|
|
2411
|
+
registerFormResponsesCommands(program);
|
|
2412
|
+
registerConfigCommands(program);
|
|
2413
|
+
registerSearchConsoleCommands(program);
|
|
2414
|
+
registerAnalyticsCommands(program);
|
|
2415
|
+
program.parse();
|
|
2416
|
+
//# sourceMappingURL=inblog.js.map
|