careervivid 1.3.0 → 1.4.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/api.js +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +146 -88
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { getApiKey, getApiUrl } from "./config.js";
|
|
8
8
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
-
const CLI_VERSION = "1.
|
|
9
|
+
const CLI_VERSION = "1.4.0";
|
|
10
10
|
function requireApiKey() {
|
|
11
11
|
const key = getApiKey();
|
|
12
12
|
if (!key) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkIpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8F7D"}
|
package/dist/commands/publish.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* cv publish - --dry-run < article.md Validate without publishing
|
|
10
10
|
* cv publish - --json < article.md Agent-friendly JSON output
|
|
11
11
|
*/
|
|
12
|
-
import { readFileSync } from "fs";
|
|
13
|
-
import { extname } from "path";
|
|
12
|
+
import { readFileSync, lstatSync, readdirSync } from "fs";
|
|
13
|
+
import { extname, join } from "path";
|
|
14
14
|
import chalk from "chalk";
|
|
15
15
|
import ora from "ora";
|
|
16
16
|
import { publishPost, isApiError } from "../api.js";
|
|
17
|
-
import { printError,
|
|
17
|
+
import { printError, handleApiError } from "../output.js";
|
|
18
18
|
function inferFormat(filePath) {
|
|
19
19
|
const ext = extname(filePath).toLowerCase();
|
|
20
20
|
if ([".mmd", ".mermaid"].includes(ext))
|
|
@@ -31,113 +31,171 @@ async function readStdin() {
|
|
|
31
31
|
}
|
|
32
32
|
return Buffer.concat(chunks).toString("utf-8");
|
|
33
33
|
}
|
|
34
|
+
function getFiles(dir, recursive) {
|
|
35
|
+
let results = [];
|
|
36
|
+
const list = readdirSync(dir);
|
|
37
|
+
for (const file of list) {
|
|
38
|
+
const path = join(dir, file);
|
|
39
|
+
const stat = lstatSync(path);
|
|
40
|
+
if (stat && stat.isDirectory()) {
|
|
41
|
+
if (recursive) {
|
|
42
|
+
results = results.concat(getFiles(path, recursive));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const ext = extname(path).toLowerCase();
|
|
47
|
+
if ([".md", ".mmd", ".mermaid"].includes(ext)) {
|
|
48
|
+
results.push(path);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
54
|
+
async function publishSingleFile(filePath, content, opts, jsonMode) {
|
|
55
|
+
const dryRun = !!opts.dryRun;
|
|
56
|
+
const format = opts.format ||
|
|
57
|
+
(filePath !== "stdin" ? inferFormat(filePath) : "markdown");
|
|
58
|
+
const type = opts.type || inferType(format);
|
|
59
|
+
let title = opts.title || "";
|
|
60
|
+
if (!title && format === "markdown") {
|
|
61
|
+
const firstHeading = content.match(/^#\s+(.+)$/m);
|
|
62
|
+
if (firstHeading) {
|
|
63
|
+
title = firstHeading[1].trim();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!title) {
|
|
67
|
+
if (jsonMode || filePath === "stdin") {
|
|
68
|
+
// No interactive prompt for stdin or JSON mode
|
|
69
|
+
title = filePath === "stdin" ? "Untitled Post" : filePath;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const enquirer = (await import("enquirer"));
|
|
73
|
+
const prompt = enquirer.default?.prompt || enquirer.prompt;
|
|
74
|
+
const answers = await prompt({
|
|
75
|
+
type: "input",
|
|
76
|
+
name: "title",
|
|
77
|
+
message: `Post title for ${chalk.cyan(filePath)}`,
|
|
78
|
+
});
|
|
79
|
+
title = answers.title.trim();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const tags = opts.tags
|
|
83
|
+
? opts.tags.split(",").map((t) => t.trim()).filter(Boolean)
|
|
84
|
+
: [];
|
|
85
|
+
const payload = {
|
|
86
|
+
type,
|
|
87
|
+
dataFormat: format,
|
|
88
|
+
title,
|
|
89
|
+
content,
|
|
90
|
+
tags,
|
|
91
|
+
...(opts.cover ? { coverImage: opts.cover } : {}),
|
|
92
|
+
...(opts.official ? { isOfficialPost: true } : {}),
|
|
93
|
+
};
|
|
94
|
+
const result = await publishPost(payload, dryRun);
|
|
95
|
+
if (isApiError(result)) {
|
|
96
|
+
if (jsonMode) {
|
|
97
|
+
handleApiError(result, true);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
console.error(chalk.red(`\n ✖ Failed to publish ${filePath}: ${result.message}`));
|
|
101
|
+
}
|
|
102
|
+
return { success: false };
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
url: result.url,
|
|
107
|
+
postId: result.postId,
|
|
108
|
+
title: title
|
|
109
|
+
};
|
|
110
|
+
}
|
|
34
111
|
export function registerPublishCommand(program) {
|
|
35
112
|
program
|
|
36
|
-
.command("publish [
|
|
113
|
+
.command("publish [files...]")
|
|
37
114
|
.description([
|
|
38
|
-
"Publish
|
|
115
|
+
"Publish files or directories to CareerVivid",
|
|
39
116
|
"",
|
|
40
117
|
" cv publish article.md",
|
|
41
|
-
" cv publish
|
|
42
|
-
"
|
|
118
|
+
" cv publish docs/ --recursive",
|
|
119
|
+
" cv publish part1.md part2.md --tags series",
|
|
120
|
+
" cat article.md | cv publish - --title \"My Article\"",
|
|
43
121
|
].join("\n"))
|
|
44
|
-
.option("-t, --title <title>", "Post title (
|
|
45
|
-
.option("--type <type>", "Post type: article | whiteboard
|
|
46
|
-
.option("--format <format>", "Content format: markdown | mermaid
|
|
47
|
-
.option("--tags <tags>", "Comma-separated tags
|
|
122
|
+
.option("-t, --title <title>", "Post title (per file, inferred if omitted)")
|
|
123
|
+
.option("--type <type>", "Post type: article | whiteboard")
|
|
124
|
+
.option("--format <format>", "Content format: markdown | mermaid")
|
|
125
|
+
.option("--tags <tags>", "Comma-separated tags (max 5)")
|
|
48
126
|
.option("--cover <url>", "URL to a cover image")
|
|
49
127
|
.option("--official", "Publish as CareerVivid Community (admin only)")
|
|
50
|
-
.option("--
|
|
128
|
+
.option("-r, --recursive", "Recursively scan directories")
|
|
129
|
+
.option("--dry-run", "Validate without publishing")
|
|
51
130
|
.option("--json", "Machine-readable JSON output")
|
|
52
|
-
.action(async (
|
|
131
|
+
.action(async (files, opts) => {
|
|
53
132
|
const jsonMode = !!opts.json;
|
|
54
133
|
const dryRun = !!opts.dryRun;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!fileArg || fileArg === "-") {
|
|
59
|
-
if (!jsonMode) {
|
|
60
|
-
console.log(` ${chalk.dim("Reading from stdin... (Ctrl+D to finish)")}`);
|
|
61
|
-
}
|
|
62
|
-
content = await readStdin();
|
|
63
|
-
filePath = "stdin";
|
|
134
|
+
let fileList = [];
|
|
135
|
+
if (files.length === 0 || files.includes("-")) {
|
|
136
|
+
fileList = ["stdin"];
|
|
64
137
|
}
|
|
65
138
|
else {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
139
|
+
for (const arg of files) {
|
|
140
|
+
try {
|
|
141
|
+
const stat = lstatSync(arg);
|
|
142
|
+
if (stat.isDirectory()) {
|
|
143
|
+
fileList = fileList.concat(getFiles(arg, !!opts.recursive));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
fileList.push(arg);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
printError(`Cannot find path: ${arg}`, undefined, jsonMode);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
73
153
|
}
|
|
74
154
|
}
|
|
75
|
-
if (
|
|
76
|
-
printError("
|
|
155
|
+
if (fileList.length === 0) {
|
|
156
|
+
printError("No files found to publish.", undefined, jsonMode);
|
|
77
157
|
process.exit(1);
|
|
78
158
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
(filePath !== "stdin" ? inferFormat(filePath) : "markdown");
|
|
82
|
-
const type = opts.type || inferType(format);
|
|
83
|
-
// ── Infer title from first heading if not provided ─────────────────────
|
|
84
|
-
let title = opts.title || "";
|
|
85
|
-
if (!title && format === "markdown") {
|
|
86
|
-
const firstHeading = content.match(/^#\s+(.+)$/m);
|
|
87
|
-
if (firstHeading) {
|
|
88
|
-
title = firstHeading[1].trim();
|
|
89
|
-
}
|
|
159
|
+
if (!jsonMode && !dryRun) {
|
|
160
|
+
console.log(`\n ${chalk.bold("Preparing to publish")} ${chalk.cyan(fileList.length)} ${fileList.length === 1 ? "file" : "files"}...`);
|
|
90
161
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
162
|
+
const results = [];
|
|
163
|
+
let successCount = 0;
|
|
164
|
+
for (const filePath of fileList) {
|
|
165
|
+
let content;
|
|
166
|
+
if (filePath === "stdin") {
|
|
167
|
+
if (!jsonMode)
|
|
168
|
+
console.log(` ${chalk.dim("Reading from stdin... (Ctrl+D to finish)")}`);
|
|
169
|
+
content = await readStdin();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
content = readFileSync(filePath, "utf-8");
|
|
173
|
+
}
|
|
174
|
+
if (!content.trim()) {
|
|
175
|
+
if (!jsonMode)
|
|
176
|
+
console.log(chalk.yellow(` ⚠ Skipping empty file: ${filePath}`));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const spinner = (!jsonMode && !dryRun) ? ora(`Publishing ${chalk.cyan(filePath)}...`).start() : null;
|
|
180
|
+
const res = await publishSingleFile(filePath, content, opts, jsonMode);
|
|
181
|
+
spinner?.stop();
|
|
182
|
+
if (res.success) {
|
|
183
|
+
successCount++;
|
|
184
|
+
results.push({ file: filePath, ...res });
|
|
185
|
+
if (!jsonMode && !dryRun) {
|
|
186
|
+
console.log(` ${chalk.green("✔")} Published: ${chalk.bold(res.title)}`);
|
|
187
|
+
console.log(` ${chalk.dim(res.url)}`);
|
|
188
|
+
}
|
|
95
189
|
}
|
|
96
|
-
// Interactive prompt fallback
|
|
97
|
-
const enquirer = (await import("enquirer"));
|
|
98
|
-
const prompt = enquirer.default?.prompt || enquirer.prompt;
|
|
99
|
-
const answers = await prompt({
|
|
100
|
-
type: "input",
|
|
101
|
-
name: "title",
|
|
102
|
-
message: "Post title",
|
|
103
|
-
});
|
|
104
|
-
title = answers.title.trim();
|
|
105
190
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
? opts.tags.split(",").map((t) => t.trim()).filter(Boolean)
|
|
109
|
-
: [];
|
|
110
|
-
if (tags.length > 5) {
|
|
111
|
-
printError("Maximum 5 tags allowed.", undefined, jsonMode);
|
|
112
|
-
process.exit(1);
|
|
191
|
+
if (jsonMode) {
|
|
192
|
+
console.log(JSON.stringify(results, null, 2));
|
|
113
193
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
console.log(`\n ${chalk.yellow("★")} ${chalk.bold("Publishing as")} ${chalk.cyan("CareerVivid Community")} ${chalk.dim("(official post)")}`);
|
|
194
|
+
else if (!dryRun) {
|
|
195
|
+
console.log(`\n ${chalk.green("Done!")} Successfully published ${chalk.bold(successCount)} of ${chalk.bold(fileList.length)} files.\n`);
|
|
117
196
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
dataFormat: format,
|
|
121
|
-
title,
|
|
122
|
-
content,
|
|
123
|
-
tags,
|
|
124
|
-
...(opts.cover ? { coverImage: opts.cover } : {}),
|
|
125
|
-
...(isOfficialPost ? { isOfficialPost: true } : {}),
|
|
126
|
-
};
|
|
127
|
-
// ── Publish ────────────────────────────────────────────────────────────
|
|
128
|
-
const spinner = jsonMode || dryRun
|
|
129
|
-
? null
|
|
130
|
-
: ora(`${dryRun ? "Validating" : "Publishing"} ${type}...`).start();
|
|
131
|
-
const result = await publishPost(payload, dryRun);
|
|
132
|
-
spinner?.stop();
|
|
133
|
-
if (isApiError(result)) {
|
|
134
|
-
handleApiError(result, jsonMode);
|
|
197
|
+
else {
|
|
198
|
+
console.log(`\n ${chalk.yellow("Dry run complete.")} Validated ${chalk.bold(fileList.length)} files.\n`);
|
|
135
199
|
}
|
|
136
|
-
printSuccess({
|
|
137
|
-
Title: title,
|
|
138
|
-
URL: result.url,
|
|
139
|
-
"Post ID": result.postId,
|
|
140
|
-
...(dryRun ? { Note: "Dry run — not published" } : {}),
|
|
141
|
-
}, jsonMode);
|
|
142
200
|
});
|
|
143
201
|
}
|