careervivid 1.2.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/README.md CHANGED
@@ -14,6 +14,9 @@
14
14
  - [Commands](#commands)
15
15
  - [cv publish](#cv-publish)
16
16
  - [cv whiteboard](#cv-whiteboard)
17
+ - [cv workspace](#cv-workspace)
18
+ - [cv profile](#cv-profile)
19
+ - [cv jobs](#cv-jobs)
17
20
  - [cv auth](#cv-auth)
18
21
  - [cv config](#cv-config)
19
22
  - [Whiteboard Templates](#whiteboard-templates)
@@ -116,6 +119,46 @@ Print all available built-in Mermaid templates. (Also available as `cv whiteboar
116
119
  cv list-templates
117
120
  ```
118
121
 
122
+ ### `cv workspace` (or `cv gws`)
123
+
124
+ Manage your Google Workspace integration.
125
+
126
+ | Subcommand | Description |
127
+ |---|---|
128
+ | `cv workspace check` | Verify GWS CLI installation and OAuth status |
129
+
130
+ ---
131
+
132
+ ### `cv profile`
133
+
134
+ Manage your CareerVivid profile.
135
+
136
+ | Subcommand | Description |
137
+ |---|---|
138
+ | `cv profile export` | Export resume data to other formats (e.g. `gdoc`) |
139
+
140
+ ```bash
141
+ # Export to Google Docs
142
+ cv profile export --format gdoc
143
+ ```
144
+
145
+ ---
146
+
147
+ ### `cv jobs`
148
+
149
+ Automate your job application tracking.
150
+
151
+ | Subcommand | Description |
152
+ |---|---|
153
+ | `cv jobs sync-gmail` | Scan Gmail for applications and sync to a Google Sheet |
154
+
155
+ ```bash
156
+ # Sync recent applications to Google Sheets
157
+ cv jobs sync-gmail
158
+ ```
159
+
160
+ ---
161
+
119
162
  ### `cv whiteboard`
120
163
 
121
164
  Grouped commands for Mermaid architecture diagrams.
@@ -211,7 +254,8 @@ The CLI is designed to be **AI-agent-friendly**. Use `--json` for structured out
211
254
  You are an expert technical writer and senior software architect.
212
255
  Your secondary directive — after writing and fixing code — is to
213
256
  proactively help the user document their work and build their
214
- CareerVivid portfolio using the local `cv` CLI.
257
+ CareerVivid portfolio using the local `cv` CLI. The platform supports
258
+ the latest Gemini 2.5 and 3.0 models for high-quality content generation.
215
259
 
216
260
  ## 1. Triggers (When to Propose a Post)
217
261
  Do not wait for the user to ask. Propose a CareerVivid post when:
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.1.13";
9
+ const CLI_VERSION = "1.4.0";
10
10
  function requireApiKey() {
11
11
  const key = getApiKey();
12
12
  if (!key) {
@@ -71,7 +71,7 @@ export async function publishPost(payload, dryRun = false) {
71
71
  message: "Dry run passed. No post was created.",
72
72
  };
73
73
  }
74
- return apiRequest("POST", "", payload);
74
+ return apiRequest("POST", "publish", payload);
75
75
  }
76
76
  /**
77
77
  * Verify an API key against the /verifyAuth endpoint.
@@ -131,16 +131,16 @@ export async function pingAuth() {
131
131
  return { ok: true };
132
132
  }
133
133
  export async function initPortfolio(title, templateId) {
134
- return apiRequest("POST", "initPortfolio", { title, templateId });
134
+ return apiRequest("POST", "portfolio/init", { title, templateId });
135
135
  }
136
136
  export async function updatePortfolioProjects(portfolioId, projects, techStack) {
137
- return apiRequest("PATCH", "updatePortfolioProjects", { portfolioId, projects, techStack });
137
+ return apiRequest("PATCH", "portfolio/projects", { portfolioId, projects, techStack });
138
138
  }
139
139
  export async function updatePortfolioHero(portfolioId, hero, theme, seoMetadata) {
140
- return apiRequest("PATCH", "updatePortfolioHero", { portfolioId, hero, theme, seoMetadata });
140
+ return apiRequest("PATCH", "portfolio/hero", { portfolioId, hero, theme, seoMetadata });
141
141
  }
142
142
  export async function uploadPortfolioAsset(image, path, mimeType) {
143
- return apiRequest("POST", "uploadPortfolioAsset", { image, path, mimeType });
143
+ return apiRequest("POST", "portfolio/assets", { image, path, mimeType });
144
144
  }
145
145
  export function isApiError(v) {
146
146
  return typeof v === "object" && v !== null && v.isError === true;
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerJobsCommand(program: Command): void;
3
+ //# sourceMappingURL=jobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/commands/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,QA6GnD"}
@@ -0,0 +1,91 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import boxen from "boxen";
4
+ import { checkGwsReady, runGwsCommand } from "../utils/gws-runner.js";
5
+ import { printError } from "../output.js";
6
+ export function registerJobsCommand(program) {
7
+ const jobsCmd = program
8
+ .command("jobs")
9
+ .description("Automate and track your job applications");
10
+ jobsCmd
11
+ .command("sync-gmail")
12
+ .description("Scan your Gmail for job applications and generate a tracking Sheet")
13
+ .action(async () => {
14
+ const isJson = process.argv.includes("--json");
15
+ if (!isJson) {
16
+ console.log(`\n ${chalk.bold("Job Tracker: Syncing Gmail to Google Sheets")}\n`);
17
+ }
18
+ // 1. Verify GWS CLI is available
19
+ const isReady = await checkGwsReady();
20
+ if (!isReady) {
21
+ printError("Google Workspace CLI is not configured. Run 'cv workspace check'.", undefined, isJson);
22
+ process.exit(1);
23
+ }
24
+ // 2. Search Gmail for Application Emails
25
+ const gmailSpinner = ora("Scanning Gmail for recent applications...").start();
26
+ // Note: In a real app we would paginate, but for the demo we'll fetch the top 5
27
+ const query = encodeURIComponent("subject:application OR subject:applied OR subject:\"thank you for applying\"");
28
+ const listRes = await runGwsCommand(`gmail users messages list --params '{"userId": "me", "maxResults": 5, "q": "${query}"}'`);
29
+ if (!listRes.success) {
30
+ gmailSpinner.fail("Failed to read Gmail.");
31
+ printError(listRes.error || "Unknown error", undefined, isJson);
32
+ process.exit(1);
33
+ }
34
+ const messages = listRes.data?.messages || [];
35
+ if (messages.length === 0) {
36
+ gmailSpinner.info("No recent application emails found.");
37
+ process.exit(0);
38
+ }
39
+ // Fetch snippets for these messages (mocking AI extraction)
40
+ const extractedJobs = [];
41
+ for (const msg of messages) {
42
+ const msgRes = await runGwsCommand(`gmail users messages get --params '{"userId": "me", "id": "${msg.id}", "format": "metadata"}'`);
43
+ if (msgRes.success && msgRes.data) {
44
+ const snippet = msgRes.data.snippet || "";
45
+ // Mocking AI parse
46
+ extractedJobs.push({
47
+ company: snippet.split(" ")[0] || "Unknown Corp",
48
+ role: "Software Engineer",
49
+ date: new Date().toISOString().split('T')[0]
50
+ });
51
+ }
52
+ }
53
+ gmailSpinner.succeed(`Found ${extractedJobs.length} applications via Gmail.`);
54
+ // 3. Create the Google Sheet Tracker
55
+ const sheetSpinner = ora("Creating Tracker inside Google Sheets...").start();
56
+ const createRes = await runGwsCommand(`sheets spreadsheets create --json '{"properties": {"title": "CareerVivid Job Tracker ${new Date().getFullYear()}"}}'`);
57
+ if (!createRes.success || !createRes.data?.spreadsheetId) {
58
+ sheetSpinner.fail("Failed to create Google Sheet.");
59
+ printError(createRes.error || "Unknown error", undefined, isJson);
60
+ process.exit(1);
61
+ }
62
+ const sheetId = createRes.data.spreadsheetId;
63
+ sheetSpinner.succeed(`Created new spreadsheet: ${sheetId}`);
64
+ // 4. Append Data to the Sheet
65
+ const appendSpinner = ora("Writing data to Sheet...").start();
66
+ const values = [
67
+ ["Company", "Role", "Date Applied", "Status"], // Header
68
+ ...extractedJobs.map(job => [job.company, job.role, job.date, "Applied"]) // Rows
69
+ ];
70
+ const payload = JSON.stringify({ values }).replace(/'/g, "");
71
+ const updateRes = await runGwsCommand(`sheets spreadsheets values append \
72
+ --params '{"spreadsheetId": "${sheetId}", "range": "Sheet1!A1", "valueInputOption": "USER_ENTERED"}' \
73
+ --json '${payload}'`);
74
+ if (!updateRes.success) {
75
+ appendSpinner.fail("Failed to write to Google Sheet.");
76
+ printError(updateRes.error || "Unknown validation issue with Sheets API payload.", undefined, isJson);
77
+ process.exit(1);
78
+ }
79
+ appendSpinner.succeed("Tracker updated successfully!");
80
+ // 5. Output Result
81
+ const sheetUrl = `https://docs.google.com/spreadsheets/d/${sheetId}/edit`;
82
+ if (isJson) {
83
+ console.log(JSON.stringify({ success: true, url: sheetUrl, spreadsheetId: sheetId, jobsFound: extractedJobs.length }));
84
+ }
85
+ else {
86
+ console.log(boxen(`${chalk.bold.green("✔ Sync Complete!")}\n\n` +
87
+ `Your applications have been synced to Google Sheets:\n` +
88
+ `${chalk.cyan.underline(sheetUrl)}`, { padding: 1, borderStyle: "round" }));
89
+ }
90
+ });
91
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerProfileCommand(program: Command): void;
3
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/commands/profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QAiHtD"}
@@ -0,0 +1,101 @@
1
+ import * as fs from "fs";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import boxen from "boxen";
5
+ import { checkGwsReady, runGwsCommand } from "../utils/gws-runner.js";
6
+ import { printError } from "../output.js";
7
+ export function registerProfileCommand(program) {
8
+ const profileCmd = program
9
+ .command("profile")
10
+ .description("Manage your CareerVivid developer profile and resume");
11
+ profileCmd
12
+ .command("export")
13
+ .description("Export your resume data to external formats")
14
+ .argument("[file]", "Path to local resume.json (uses mock data if omitted)")
15
+ .requiredOption("--format <fmt>", "Format to export (e.g., 'gdoc')")
16
+ .action(async (file, options) => {
17
+ const isJson = process.argv.includes("--json");
18
+ if (options.format.toLowerCase() !== "gdoc") {
19
+ printError("Currently only '--format gdoc' is supported by this integration.", undefined, isJson);
20
+ process.exit(1);
21
+ }
22
+ if (!isJson) {
23
+ console.log(`\n ${chalk.bold("Exporting CareerVivid Resume to Google Docs")}\n`);
24
+ }
25
+ // 1. Verify GWS CLI is available
26
+ const isReady = await checkGwsReady();
27
+ if (!isReady) {
28
+ printError("Google Workspace CLI is not configured. Run 'cv workspace check'.", undefined, isJson);
29
+ process.exit(1);
30
+ }
31
+ // 2. Load Resume Data
32
+ let resumeData = {
33
+ name: "Alex Dev",
34
+ title: "Senior Full Stack Engineer",
35
+ summary: "Passionate developer building AI-first tools.",
36
+ experience: [
37
+ { role: "Software Engineer", company: "Tech Corp", years: "2022 - Present" }
38
+ ]
39
+ };
40
+ if (file && fs.existsSync(file)) {
41
+ try {
42
+ resumeData = JSON.parse(fs.readFileSync(file, "utf8"));
43
+ }
44
+ catch (err) {
45
+ printError(`Failed to parse resume file: ${err.message}`, undefined, isJson);
46
+ process.exit(1);
47
+ }
48
+ }
49
+ else if (file) {
50
+ printError(`File not found: ${file}`, undefined, isJson);
51
+ process.exit(1);
52
+ }
53
+ const title = `${resumeData.name.replace(/ /g, "_")}_Resume_Export`;
54
+ // 3. Create the Google Doc
55
+ const createSpinner = ora("Creating new Google Doc...").start();
56
+ const createRes = await runGwsCommand(`docs documents create --json '{"title": "${title}"}'`);
57
+ if (!createRes.success || !createRes.data?.documentId) {
58
+ createSpinner.fail("Failed to create Google Doc.");
59
+ printError(createRes.error || "Unknown error", undefined, isJson);
60
+ process.exit(1);
61
+ }
62
+ createSpinner.succeed("Created base document.");
63
+ const docId = createRes.data.documentId;
64
+ // 4. Construct Content (Batch Update)
65
+ const contentSpinner = ora("Formatting resume content...").start();
66
+ // Text to insert (plain text for simplicity in this demo)
67
+ const textToInsert = `${resumeData.name}\n${resumeData.title}\n\nSUMMARY:\n${resumeData.summary}\n\nEXPERIENCE:\n` +
68
+ resumeData.experience.map(e => `- ${e.role} at ${e.company} (${e.years})`).join("\n");
69
+ // Google Docs API uses a specific JSON structure for insertions
70
+ const batchPayload = {
71
+ requests: [
72
+ {
73
+ insertText: {
74
+ location: { index: 1 },
75
+ text: textToInsert
76
+ }
77
+ }
78
+ ]
79
+ };
80
+ // Needs precise escaping for bash if passing directly, or we save to tmp file and pass path.
81
+ // For simplicity, we'll strip single quotes from the payload if any exist.
82
+ const cleanPayload = JSON.stringify(batchPayload).replace(/'/g, "");
83
+ const updateRes = await runGwsCommand(`docs documents batchUpdate --params '{"documentId": "${docId}"}' --json '${cleanPayload}'`);
84
+ if (!updateRes.success) {
85
+ contentSpinner.fail("Failed to insert content.");
86
+ printError(updateRes.error || "Unknown error", undefined, isJson);
87
+ process.exit(1);
88
+ }
89
+ contentSpinner.succeed("Resume content injected.");
90
+ // 5. Output Result
91
+ const docUrl = `https://docs.google.com/document/d/${docId}/edit`;
92
+ if (isJson) {
93
+ console.log(JSON.stringify({ success: true, url: docUrl, documentId: docId }));
94
+ }
95
+ else {
96
+ console.log(boxen(`${chalk.bold.green("✔ Export Complete!")}\n\n` +
97
+ `Your resume has been successfully generated in Google Docs:\n` +
98
+ `${chalk.cyan.underline(docUrl)}`, { padding: 1, borderStyle: "round" }));
99
+ }
100
+ });
101
+ }
@@ -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
  }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function registerWorkspaceCommand(program: Command): void;
3
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/commands/workspace.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAMpC,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA8C/D"}
@@ -0,0 +1,39 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { checkGwsReady } from "../utils/gws-runner.js";
4
+ import { COLORS } from "../branding.js";
5
+ export function registerWorkspaceCommand(program) {
6
+ const workspace = program
7
+ .command("workspace")
8
+ .alias("gws")
9
+ .description("Manage Google Workspace integrations (Google Docs, Gmail, Sheets)");
10
+ workspace
11
+ .command("check")
12
+ .description("Verify that the Google Workspace CLI is installed and authenticated")
13
+ .action(async () => {
14
+ console.log(`\n ${chalk.bold("CareerVivid Google Workspace Integration")}\n`);
15
+ const isReady = await checkGwsReady();
16
+ if (isReady) {
17
+ console.log(boxen(`${chalk.green("✔ Google Workspace integration is fully configured.")}\n\n` +
18
+ `You can now use CareerVivid commands that export to Google Docs,\n` +
19
+ `sync with Gmail, and manage Calendar invites.`, {
20
+ padding: 1,
21
+ margin: { top: 1, bottom: 1 },
22
+ borderStyle: "round",
23
+ borderColor: COLORS.success,
24
+ }));
25
+ }
26
+ else {
27
+ console.log(boxen(`${chalk.red("✖ Google Workspace integration is not ready.")}\n\n` +
28
+ `Please ensure you have installed \`gws\` and authenticated:\n\n` +
29
+ `${chalk.cyan("npm install -g @googleworkspace/cli")}\n` +
30
+ `${chalk.cyan("gws auth setup")}`, {
31
+ padding: 1,
32
+ margin: { top: 1, bottom: 1 },
33
+ borderStyle: "round",
34
+ borderColor: COLORS.error,
35
+ }));
36
+ process.exit(1);
37
+ }
38
+ });
39
+ }
package/dist/config.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * apiUrl — override for the publish endpoint (default: prod)
7
7
  */
8
8
  export declare const CONFIG_FILE: string;
9
- export declare const DEFAULT_API_URL = "https://careervivid.app/api/publish";
9
+ export declare const DEFAULT_API_URL = "https://careervivid.app/api";
10
10
  export interface CareerVividConfig {
11
11
  apiKey?: string;
12
12
  apiUrl?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,eAAO,MAAM,WAAW,QAAyC,CAAC;AAElE,eAAO,MAAM,eAAe,wCAAwC,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,UAAU,IAAI,iBAAiB,CAQ9C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1D;AAED,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAG9C;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIhF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,eAAO,MAAM,WAAW,QAAyC,CAAC;AAElE,eAAO,MAAM,eAAe,gCAAgC,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,UAAU,IAAI,iBAAiB,CAQ9C;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAE1D;AAED,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAG9C;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIhF"}
package/dist/config.js CHANGED
@@ -9,7 +9,7 @@ import { homedir } from "os";
9
9
  import { join } from "path";
10
10
  import { readFileSync, writeFileSync, existsSync } from "fs";
11
11
  export const CONFIG_FILE = join(homedir(), ".careervividrc.json");
12
- export const DEFAULT_API_URL = "https://careervivid.app/api/publish";
12
+ export const DEFAULT_API_URL = "https://careervivid.app/api";
13
13
  export function loadConfig() {
14
14
  if (!existsSync(CONFIG_FILE))
15
15
  return {};
package/dist/index.js CHANGED
@@ -29,13 +29,16 @@ import { registerPublishCommand } from "./commands/publish.js";
29
29
  import { registerConfigCommand } from "./commands/config.js";
30
30
  import { registerUpdateCommand } from "./commands/update.js";
31
31
  import { checkForUpdates } from "./updates.js";
32
- import { registerWhiteboardCommand, registerNewCommand, registerListTemplatesCommand, } from "./commands/whiteboard.js";
32
+ import { registerListTemplatesCommand, registerNewCommand, registerWhiteboardCommand } from "./commands/whiteboard.js";
33
33
  import { registerPortfolioCommand } from "./commands/portfolio.js";
34
+ import { registerWorkspaceCommand } from "./commands/workspace.js";
35
+ import { registerProfileCommand } from "./commands/profile.js";
36
+ import { registerJobsCommand } from "./commands/jobs.js";
34
37
  const program = new Command();
35
38
  program
36
39
  .name("cv")
37
40
  .description("CareerVivid CLI — publish articles, diagrams, and portfolio updates from your terminal or AI agent")
38
- .version("1.1.13", "-v, --version", "Print CLI version")
41
+ .version("1.3.0", "-v, --version", "Print CLI version")
39
42
  .addHelpText("before", getHelpHeader())
40
43
  .helpOption("-h, --help", "Show help");
41
44
  registerAuthCommand(program);
@@ -45,6 +48,9 @@ registerConfigCommand(program);
45
48
  registerUpdateCommand(program);
46
49
  registerWhiteboardCommand(program);
47
50
  registerPortfolioCommand(program);
51
+ registerWorkspaceCommand(program);
52
+ registerProfileCommand(program);
53
+ registerJobsCommand(program);
48
54
  // Shortcuts for whiteboard creation
49
55
  registerNewCommand(program);
50
56
  registerListTemplatesCommand(program);
@@ -0,0 +1,18 @@
1
+ export type GwsResponse<T = any> = {
2
+ success: boolean;
3
+ data?: T;
4
+ error?: string;
5
+ };
6
+ /**
7
+ * Executes a GWS CLI command, preferring a globally installed binary,
8
+ * falling back to npx if not found.
9
+ *
10
+ * @param command - The GWS command string (e.g., 'drive files list --params "..."')
11
+ * @returns Parsed JSON response from GWS
12
+ */
13
+ export declare function runGwsCommand<T = any>(command: string): Promise<GwsResponse<T>>;
14
+ /**
15
+ * Interactive check to ensure GWS is installed and authenticated.
16
+ */
17
+ export declare function checkGwsReady(): Promise<boolean>;
18
+ //# sourceMappingURL=gws-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gws-runner.d.ts","sourceRoot":"","sources":["../../src/utils/gws-runner.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,CAAC,CAAC,GAAG,GAAG,IAAI;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAqCrF;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CA2BtD"}
@@ -0,0 +1,80 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import ora from "ora";
4
+ const execAsync = promisify(exec);
5
+ /**
6
+ * Executes a GWS CLI command, preferring a globally installed binary,
7
+ * falling back to npx if not found.
8
+ *
9
+ * @param command - The GWS command string (e.g., 'drive files list --params "..."')
10
+ * @returns Parsed JSON response from GWS
11
+ */
12
+ export async function runGwsCommand(command) {
13
+ let baseCmd = 'gws';
14
+ // Check if gws is available globally
15
+ try {
16
+ await execAsync('command -v gws');
17
+ }
18
+ catch {
19
+ // Fallback to npx if gws is not in PATH
20
+ baseCmd = 'npx --yes @googleworkspace/cli';
21
+ }
22
+ try {
23
+ const fullCmd = `${baseCmd} ${command}`;
24
+ const { stdout, stderr } = await execAsync(fullCmd);
25
+ // GWS outputs JSON on stdout
26
+ try {
27
+ const data = JSON.parse(stdout.trim());
28
+ return { success: true, data };
29
+ }
30
+ catch (parseError) {
31
+ // Sometimes there's non-JSON output before the JSON
32
+ // Try to extract JSON if possible, or just return raw
33
+ return { success: true, data: stdout.trim() };
34
+ }
35
+ }
36
+ catch (error) {
37
+ // GWS errors usually contain stderr
38
+ let errorMsg = error.stderr || error.message;
39
+ try {
40
+ // Attempt to parse structured error from output if present
41
+ const parsedErr = JSON.parse(error.stdout || "{}");
42
+ if (parsedErr.error && parsedErr.error.message) {
43
+ errorMsg = parsedErr.error.message;
44
+ }
45
+ }
46
+ catch (_) { }
47
+ return { success: false, error: errorMsg };
48
+ }
49
+ }
50
+ /**
51
+ * Interactive check to ensure GWS is installed and authenticated.
52
+ */
53
+ export async function checkGwsReady() {
54
+ const spinner = ora("Checking Google Workspace CLI (gws) connection...").start();
55
+ // Check if installed
56
+ try {
57
+ await execAsync('command -v gws');
58
+ }
59
+ catch {
60
+ // If not installed globally, check if we can run it via npx
61
+ try {
62
+ await execAsync('npx --yes @googleworkspace/cli --version');
63
+ }
64
+ catch {
65
+ spinner.fail("Google Workspace CLI not found.");
66
+ return false;
67
+ }
68
+ }
69
+ // Check auth by doing a simple safe call
70
+ // getting the user's gmail profile is a good token check
71
+ const result = await runGwsCommand("gmail users getProfile --params '{\"userId\": \"me\"}'");
72
+ if (result.success && result.data && result.data.emailAddress) {
73
+ spinner.succeed(`GWS CLI is ready! Authenticated as ${result.data.emailAddress}`);
74
+ return true;
75
+ }
76
+ else {
77
+ spinner.fail("GWS CLI is installed but not authenticated, or missing Gmail scopes.");
78
+ return false;
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "1.2.0",
3
+ "version": "1.4.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": {
@@ -26,6 +26,7 @@
26
26
  "commander": "^12.1.0",
27
27
  "enquirer": "^2.4.1",
28
28
  "gradient-string": "^3.0.0",
29
+ "mermaid": "^11.12.3",
29
30
  "open": "^10.1.0",
30
31
  "ora": "^8.1.0",
31
32
  "semver": "^7.6.3",