careervivid 1.3.0 → 1.5.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 CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { getApiKey, getApiUrl } from "./config.js";
8
8
  // ── Helpers ───────────────────────────────────────────────────────────────────
9
- const CLI_VERSION = "1.3.0";
9
+ const CLI_VERSION = "1.5.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;AA2BpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8J7D"}
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"}
@@ -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, printSuccess, handleApiError } from "../output.js";
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 [file]")
113
+ .command("publish [files...]")
37
114
  .description([
38
- "Publish a markdown or mermaid file to CareerVivid",
115
+ "Publish files or directories to CareerVivid",
39
116
  "",
40
117
  " cv publish article.md",
41
- " cv publish diagram.mmd --type whiteboard",
42
- " cat article.md | cv publish - --title \"My Article\" --json",
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 (inferred from first heading if omitted)")
45
- .option("--type <type>", "Post type: article | whiteboard (default: inferred from format)")
46
- .option("--format <format>", "Content format: markdown | mermaid (default: inferred from file extension)")
47
- .option("--tags <tags>", "Comma-separated tags, e.g. typescript,firebase,react")
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("--dry-run", "Validate payload without publishing")
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 (fileArg, opts) => {
131
+ .action(async (files, opts) => {
53
132
  const jsonMode = !!opts.json;
54
133
  const dryRun = !!opts.dryRun;
55
- // ── Read content ────────────────────────────────────────────────────────
56
- let content;
57
- let filePath;
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
- try {
67
- content = readFileSync(fileArg, "utf-8");
68
- filePath = fileArg;
69
- }
70
- catch (err) {
71
- printError(`Cannot read file: ${err.message}`, undefined, jsonMode);
72
- process.exit(1);
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 (!content.trim()) {
76
- printError("Content is empty.", undefined, jsonMode);
155
+ if (fileList.length === 0) {
156
+ printError("No files found to publish.", undefined, jsonMode);
77
157
  process.exit(1);
78
158
  }
79
- // ── Infer format and type ───────────────────────────────────────────────
80
- const format = opts.format ||
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
- if (!title) {
92
- if (jsonMode) {
93
- printError("Title is required. Use --title <title> or add a # heading in your markdown.", undefined, true);
94
- process.exit(1);
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
- // ── Build payload ──────────────────────────────────────────────────────
107
- const tags = opts.tags
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
- const isOfficialPost = !!opts.official;
115
- if (isOfficialPost && !jsonMode) {
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
- const payload = {
119
- type,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Official CLI for CareerVivid — publish articles, diagrams, and portfolio updates from your terminal or AI agent",
5
5
  "type": "module",
6
6
  "bin": {