careervivid 1.0.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 ADDED
@@ -0,0 +1,199 @@
1
+ # CareerVivid CLI
2
+
3
+ Official command-line interface for [CareerVivid](https://careervivid.app) — publish articles, architecture diagrams, and portfolio updates directly from your terminal or an AI coding agent.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # 1. Clone and build (one-time setup)
11
+ git clone https://github.com/Jastalk/CareerVivid
12
+ cd CareerVivid/cli
13
+ npm install && npm run build
14
+
15
+ # 2. Install globally
16
+ npm install -g .
17
+
18
+ # 3. Authenticate and publish
19
+ cv auth set-key
20
+ cv publish article.md
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ # Build from source and install globally
29
+ git clone https://github.com/Jastalk/CareerVivid
30
+ cd CareerVivid/cli && npm install && npm run build
31
+ npm install -g .
32
+
33
+ # If you already have the repo:
34
+ cd path/to/careervivid/cli
35
+ npm install && npm run build && npm install -g .
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Authentication
41
+
42
+ Get your API key at [careervivid.app/#/developer](https://careervivid.app/#/developer).
43
+
44
+ ```bash
45
+ # Interactive setup
46
+ cv auth set-key
47
+
48
+ # Or pass it directly (no prompts)
49
+ cv auth set-key cv_live_your_key_here
50
+
51
+ # Verify it works
52
+ cv auth check
53
+
54
+ # See current config
55
+ cv auth whoami
56
+ ```
57
+
58
+ Your key is stored in `~/.careervividrc.json` with `chmod 600` permissions. It is never printed to stdout.
59
+
60
+ **Alternatively**, set the `CV_API_KEY` environment variable (takes priority over the config file):
61
+ ```bash
62
+ export CV_API_KEY=cv_live_...
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Publishing Content
68
+
69
+ ### Publish a Markdown article
70
+
71
+ ```bash
72
+ cv publish article.md
73
+ cv publish article.md --title "My Custom Title" --tags "typescript,firebase,saas"
74
+ ```
75
+
76
+ The title is automatically inferred from the first `# Heading` in your file.
77
+
78
+ ### Publish a Mermaid diagram
79
+
80
+ ```bash
81
+ cv publish diagram.mmd --type whiteboard
82
+ ```
83
+
84
+ Files with `.mmd` or `.mermaid` extensions are automatically detected as Mermaid diagrams.
85
+
86
+ ### Publish from stdin (pipe-friendly for AI agents)
87
+
88
+ ```bash
89
+ cat article.md | cv publish - --title "Architecture Deep Dive"
90
+ echo "# My Post\n\nHello world!" | cv publish - --tags "ai,demo"
91
+ ```
92
+
93
+ ### Dry run (validate without publishing)
94
+
95
+ ```bash
96
+ cv publish article.md --dry-run
97
+ ```
98
+
99
+ ### Machine-readable JSON output (for AI agents and scripts)
100
+
101
+ Every command supports `--json`:
102
+
103
+ ```bash
104
+ cv publish article.md --json
105
+ # → {"success":true,"postId":"abc123","url":"https://careervivid.app/community/post/abc123"}
106
+
107
+ cat article.md | cv publish - --title "Post" --json
108
+ # → {"success":true,"postId":"abc123","url":"..."}
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Command Reference
114
+
115
+ ```
116
+ cv publish [file]
117
+ [file] Path to a .md / .mmd file, or "-" to read stdin
118
+ --title <title> Post title (required for stdin / if no # heading)
119
+ --type article | whiteboard (default: inferred)
120
+ --format markdown | mermaid (default: inferred from extension)
121
+ --tags <tags> Comma-separated tags, max 5
122
+ --cover <url> URL to a cover image
123
+ --dry-run Validate without publishing
124
+ --json Machine-readable output
125
+
126
+ cv auth set-key [apiKey]
127
+ cv auth check
128
+ cv auth whoami
129
+ cv auth revoke
130
+
131
+ cv config show
132
+ cv config get <key> (keys: apiKey, apiUrl)
133
+ cv config set <key> <val>
134
+ ```
135
+
136
+ ---
137
+
138
+ ## AI Agent & MCP Integration
139
+
140
+ ### With Cursor / Claude Desktop (MCP)
141
+
142
+ The [MCP server](../mcp-server/) is the recommended way to integrate with AI coding agents. See `mcp-server/README.md`.
143
+
144
+ ### Direct CLI from an AI agent
145
+
146
+ ```bash
147
+ # Agent publishes an article it just wrote:
148
+ echo "${ARTICLE_CONTENT}" | cv publish - \
149
+ --title "${TITLE}" \
150
+ --tags "${TAGS}" \
151
+ --json
152
+
153
+ # Agent parses the JSON response:
154
+ # {"success":true,"postId":"abc123","url":"https://careervivid.app/community/post/abc123"}
155
+ ```
156
+
157
+ For full agentic pipelines, prefer the MCP server — it integrates natively with AI tool calling.
158
+
159
+ ---
160
+
161
+ ## Environment Variables
162
+
163
+ | Variable | Description |
164
+ |----------|-------------|
165
+ | `CV_API_KEY` | API key (overrides config file) |
166
+ | `CV_API_URL` | Override the publish endpoint (for self-hosting or staging) |
167
+
168
+ ---
169
+
170
+ ## Configuration File
171
+
172
+ Location: `~/.careervividrc.json` (mode 600 — readable only by your user)
173
+
174
+ ```json
175
+ {
176
+ "apiKey": "cv_live_...",
177
+ "apiUrl": "https://careervivid.app/api/publish"
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Building from Source
184
+
185
+ ```bash
186
+ cd cli
187
+ npm install
188
+ npm run build # TypeScript → dist/
189
+ node dist/index.js --help
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Security
195
+
196
+ - API keys are **never** written to `stdout` — only `stderr` diagnostics and masked previews
197
+ - The config file is created with `chmod 600`
198
+ - Never commit `~/.careervividrc.json` or `CV_API_KEY` to version control
199
+ - Revoke a compromised key immediately at [careervivid.app/#/developer](https://careervivid.app/#/developer)
package/dist/api.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * API client — thin fetch wrapper around the CareerVivid API.
3
+ *
4
+ * All functions throw on network errors; on HTTP errors they return
5
+ * a structured ApiError for clean CLI error messages.
6
+ */
7
+ export type PostType = "article" | "whiteboard";
8
+ export type DataFormat = "markdown" | "mermaid";
9
+ export interface PublishPayload {
10
+ type: PostType;
11
+ dataFormat: DataFormat;
12
+ title: string;
13
+ content: string;
14
+ tags?: string[];
15
+ coverImage?: string;
16
+ }
17
+ export interface PublishResult {
18
+ success: boolean;
19
+ postId: string;
20
+ url: string;
21
+ message: string;
22
+ }
23
+ export interface ApiError {
24
+ isError: true;
25
+ statusCode: number;
26
+ message: string;
27
+ fields?: {
28
+ field: string;
29
+ message: string;
30
+ }[];
31
+ }
32
+ export declare function publishPost(payload: PublishPayload, dryRun?: boolean): Promise<PublishResult | ApiError>;
33
+ export declare function pingAuth(): Promise<{
34
+ ok: boolean;
35
+ error?: string;
36
+ }>;
37
+ export declare function isApiError(v: unknown): v is ApiError;
38
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,YAAY,CAAC;AAChD,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;AAEhD,MAAM,WAAW,cAAc;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAQ;IACrB,OAAO,EAAE,IAAI,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD;AAsDD,wBAAsB,WAAW,CAC7B,OAAO,EAAE,cAAc,EACvB,MAAM,UAAQ,GACf,OAAO,CAAC,aAAa,GAAG,QAAQ,CAAC,CAiBnC;AAED,wBAAsB,QAAQ,IAAI,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBzE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,QAAQ,CAEpD"}
package/dist/api.js ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * API client — thin fetch wrapper around the CareerVivid API.
3
+ *
4
+ * All functions throw on network errors; on HTTP errors they return
5
+ * a structured ApiError for clean CLI error messages.
6
+ */
7
+ import { getApiKey, getApiUrl } from "./config.js";
8
+ // ── Helpers ───────────────────────────────────────────────────────────────────
9
+ function requireApiKey() {
10
+ const key = getApiKey();
11
+ if (!key) {
12
+ throw new Error("No API key configured.\n\n" +
13
+ " Run: cv auth set-key\n" +
14
+ " Or: export CV_API_KEY=cv_live_...\n\n" +
15
+ " Get your key at: https://careervivid.app/#/developer");
16
+ }
17
+ return key;
18
+ }
19
+ async function apiRequest(method, path, body) {
20
+ const apiKey = requireApiKey();
21
+ const baseUrl = getApiUrl().replace(/\/+$/, "");
22
+ const url = `${baseUrl}${path === "" ? "" : "/" + path}`;
23
+ const response = await fetch(url, {
24
+ method,
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ "x-api-key": apiKey,
28
+ "User-Agent": "careervivid-cli/1.0.0",
29
+ },
30
+ body: body ? JSON.stringify(body) : undefined,
31
+ });
32
+ const text = await response.text();
33
+ let parsed = {};
34
+ try {
35
+ parsed = JSON.parse(text);
36
+ }
37
+ catch {
38
+ parsed = { message: text };
39
+ }
40
+ if (!response.ok) {
41
+ return {
42
+ isError: true,
43
+ statusCode: response.status,
44
+ message: parsed.error || parsed.message || `HTTP ${response.status}`,
45
+ fields: parsed.fields,
46
+ };
47
+ }
48
+ return parsed;
49
+ }
50
+ // ── Public API ────────────────────────────────────────────────────────────────
51
+ export async function publishPost(payload, dryRun = false) {
52
+ if (dryRun) {
53
+ // Basic local validation only — don't call the network
54
+ if (!payload.title || payload.title.trim() === "") {
55
+ return { isError: true, statusCode: 0, message: "Title is required." };
56
+ }
57
+ if (!payload.content || payload.content.trim() === "") {
58
+ return { isError: true, statusCode: 0, message: "Content is required." };
59
+ }
60
+ return {
61
+ success: true,
62
+ postId: "dry-run-no-id",
63
+ url: "https://careervivid.app/community (dry-run — not published)",
64
+ message: "Dry run passed. No post was created.",
65
+ };
66
+ }
67
+ return apiRequest("POST", "", payload);
68
+ }
69
+ export async function pingAuth() {
70
+ const apiKey = getApiKey();
71
+ if (!apiKey)
72
+ return { ok: false, error: "No API key configured." };
73
+ // We send a GET (HEAD-like) to the publish endpoint.
74
+ // The API returns 405 if auth succeeds and only POST is allowed — that's fine.
75
+ // It returns 401 if auth fails.
76
+ const apiUrl = getApiUrl().replace(/\/+$/, "");
77
+ try {
78
+ const res = await fetch(apiUrl, {
79
+ method: "GET",
80
+ headers: { "x-api-key": apiKey, "User-Agent": "careervivid-cli/1.0.0" },
81
+ });
82
+ // 405 = Method Not Allowed → the key is valid, but GET is not supported → auth OK
83
+ // 401 = Unauthorized → bad key
84
+ if (res.status === 401)
85
+ return { ok: false, error: "API key is invalid or has been revoked." };
86
+ return { ok: true };
87
+ }
88
+ catch (err) {
89
+ return { ok: false, error: `Network error: ${err.message}` };
90
+ }
91
+ }
92
+ export function isApiError(v) {
93
+ return typeof v === "object" && v !== null && v.isError === true;
94
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * cv auth — API key management
3
+ *
4
+ * Subcommands:
5
+ * cv auth set-key Interactively prompt for an API key and save it
6
+ * cv auth check Verify the saved key is accepted by the server
7
+ * cv auth whoami Print the identity associated with the key (if supported)
8
+ * cv auth revoke Remove saved API key from config
9
+ */
10
+ import { Command } from "commander";
11
+ export declare function registerAuthCommand(program: Command): void;
12
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoJ1D"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * cv auth — API key management
3
+ *
4
+ * Subcommands:
5
+ * cv auth set-key Interactively prompt for an API key and save it
6
+ * cv auth check Verify the saved key is accepted by the server
7
+ * cv auth whoami Print the identity associated with the key (if supported)
8
+ * cv auth revoke Remove saved API key from config
9
+ */
10
+ import chalk from "chalk";
11
+ import ora from "ora";
12
+ import { CONFIG_FILE, loadConfig, setConfigValue } from "../config.js";
13
+ import { pingAuth } from "../api.js";
14
+ import { getApiKey } from "../config.js";
15
+ import { printError, printSuccess, printInfo } from "../output.js";
16
+ export function registerAuthCommand(program) {
17
+ const auth = program
18
+ .command("auth")
19
+ .description("Manage API key authentication");
20
+ // ── set-key ──────────────────────────────────────────────────────────────────
21
+ auth
22
+ .command("set-key [apiKey]")
23
+ .description("Save your CareerVivid API key to ~/.careervividrc.json")
24
+ .option("--json", "Machine-readable output")
25
+ .action(async (apiKeyArg, opts) => {
26
+ const jsonMode = !!opts.json;
27
+ let apiKey = apiKeyArg;
28
+ if (!apiKey) {
29
+ // Interactive prompt
30
+ if (jsonMode) {
31
+ printError("In --json mode, provide the API key as an argument: cv auth set-key <key>", undefined, true);
32
+ process.exit(1);
33
+ }
34
+ console.log();
35
+ console.log(` ${chalk.bold("Get your API key at:")} ${chalk.cyan("https://careervivid.app/#/developer")}`);
36
+ console.log();
37
+ const { prompt } = await import("enquirer");
38
+ const answers = await prompt({
39
+ type: "password",
40
+ name: "key",
41
+ message: "Paste your API key",
42
+ });
43
+ apiKey = answers.key.trim();
44
+ }
45
+ if (!apiKey || !apiKey.startsWith("cv_live_")) {
46
+ printError("Invalid key format. CareerVivid API keys start with cv_live_", undefined, jsonMode);
47
+ process.exit(1);
48
+ }
49
+ setConfigValue("apiKey", apiKey);
50
+ if (!jsonMode) {
51
+ console.log();
52
+ console.log(` ${chalk.green("✔")} Key saved to ${chalk.dim(CONFIG_FILE)}`);
53
+ console.log(` ${chalk.dim("Run")} ${chalk.cyan("cv auth check")} ${chalk.dim("to verify it works.")}`);
54
+ console.log();
55
+ }
56
+ else {
57
+ console.log(JSON.stringify({ success: true, configFile: CONFIG_FILE }));
58
+ }
59
+ });
60
+ // ── check ─────────────────────────────────────────────────────────────────────
61
+ auth
62
+ .command("check")
63
+ .description("Verify your API key is valid")
64
+ .option("--json", "Machine-readable output")
65
+ .action(async (opts) => {
66
+ const jsonMode = !!opts.json;
67
+ const key = getApiKey();
68
+ if (!key) {
69
+ printError("No API key configured. Run: cv auth set-key", undefined, jsonMode);
70
+ process.exit(1);
71
+ }
72
+ const spinner = jsonMode ? null : ora("Checking API key...").start();
73
+ const result = await pingAuth();
74
+ spinner?.stop();
75
+ if (result.ok) {
76
+ printSuccess({ status: "Valid", key: `${key.slice(0, 16)}...` }, jsonMode);
77
+ }
78
+ else {
79
+ printError(result.error || "Key check failed", undefined, jsonMode);
80
+ process.exit(1);
81
+ }
82
+ });
83
+ // ── whoami ────────────────────────────────────────────────────────────────────
84
+ auth
85
+ .command("whoami")
86
+ .description("Print the current API key configuration")
87
+ .option("--json", "Machine-readable output")
88
+ .action(async (opts) => {
89
+ const jsonMode = !!opts.json;
90
+ const config = loadConfig();
91
+ const keyFromEnv = !!process.env.CV_API_KEY;
92
+ const key = getApiKey();
93
+ if (!key) {
94
+ printError("No API key configured. Run: cv auth set-key", undefined, jsonMode);
95
+ process.exit(1);
96
+ }
97
+ const masked = `${key.slice(0, 16)}${"•".repeat(Math.max(0, key.length - 16))}`;
98
+ printSuccess({
99
+ "API Key": masked,
100
+ "Key Source": keyFromEnv ? "Environment (CV_API_KEY)" : `Config file (${CONFIG_FILE})`,
101
+ "API URL": config.apiUrl || "https://careervivid.app/api/publish (default)",
102
+ }, jsonMode);
103
+ });
104
+ // ── revoke ────────────────────────────────────────────────────────────────────
105
+ auth
106
+ .command("revoke")
107
+ .description("Remove the saved API key from ~/.careervividrc.json")
108
+ .option("--json", "Machine-readable output")
109
+ .action(async (opts) => {
110
+ const jsonMode = !!opts.json;
111
+ const config = loadConfig();
112
+ if (!config.apiKey) {
113
+ printInfo("No API key in config file to remove.", jsonMode);
114
+ return;
115
+ }
116
+ delete config.apiKey;
117
+ const { saveConfig } = await import("../config.js");
118
+ saveConfig(config);
119
+ if (jsonMode) {
120
+ console.log(JSON.stringify({ success: true }));
121
+ }
122
+ else {
123
+ console.log();
124
+ console.log(` ${chalk.green("✔")} API key removed from ${chalk.dim(CONFIG_FILE)}`);
125
+ console.log();
126
+ }
127
+ });
128
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * cv config — view and modify CLI configuration
3
+ *
4
+ * Subcommands:
5
+ * cv config show Print entire configuration
6
+ * cv config get <key> Print single config value
7
+ * cv config set <key> <val> Set a config value
8
+ */
9
+ import { Command } from "commander";
10
+ export declare function registerConfigCommand(program: Command): void;
11
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoG5D"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * cv config — view and modify CLI configuration
3
+ *
4
+ * Subcommands:
5
+ * cv config show Print entire configuration
6
+ * cv config get <key> Print single config value
7
+ * cv config set <key> <val> Set a config value
8
+ */
9
+ import chalk from "chalk";
10
+ import { CONFIG_FILE, loadConfig, setConfigValue } from "../config.js";
11
+ import { printError, printSuccess } from "../output.js";
12
+ const VALID_KEYS = ["apiKey", "apiUrl"];
13
+ export function registerConfigCommand(program) {
14
+ const config = program
15
+ .command("config")
16
+ .description("View and modify CLI configuration (~/.careervividrc.json)");
17
+ // ── show ──────────────────────────────────────────────────────────────────────
18
+ config
19
+ .command("show")
20
+ .description("Print current configuration")
21
+ .option("--json", "Machine-readable output")
22
+ .action((opts) => {
23
+ const jsonMode = !!opts.json;
24
+ const cfg = loadConfig();
25
+ if (jsonMode) {
26
+ // Never leak the full key in JSON — mask it
27
+ const masked = { ...cfg };
28
+ if (masked.apiKey)
29
+ masked.apiKey = `${masked.apiKey.slice(0, 16)}...`;
30
+ console.log(JSON.stringify({ configFile: CONFIG_FILE, config: masked }));
31
+ return;
32
+ }
33
+ console.log();
34
+ console.log(` ${chalk.bold("Config file:")} ${chalk.dim(CONFIG_FILE)}`);
35
+ console.log();
36
+ if (!cfg.apiKey && !cfg.apiUrl) {
37
+ console.log(` ${chalk.dim("No configuration set. Run: cv auth set-key")}`);
38
+ console.log();
39
+ return;
40
+ }
41
+ if (cfg.apiKey) {
42
+ const masked = `${cfg.apiKey.slice(0, 16)}${"•".repeat(cfg.apiKey.length - 16)}`;
43
+ console.log(` ${chalk.dim("apiKey".padEnd(10))} ${masked}`);
44
+ }
45
+ if (cfg.apiUrl) {
46
+ console.log(` ${chalk.dim("apiUrl".padEnd(10))} ${cfg.apiUrl}`);
47
+ }
48
+ console.log();
49
+ });
50
+ // ── get ───────────────────────────────────────────────────────────────────────
51
+ config
52
+ .command("get <key>")
53
+ .description(`Get a config value (keys: ${VALID_KEYS.join(", ")})`)
54
+ .option("--json", "Machine-readable output")
55
+ .action((key, opts) => {
56
+ const jsonMode = !!opts.json;
57
+ if (!VALID_KEYS.includes(key)) {
58
+ printError(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`, undefined, jsonMode);
59
+ process.exit(1);
60
+ }
61
+ const cfg = loadConfig();
62
+ const value = cfg[key];
63
+ if (!value) {
64
+ if (jsonMode) {
65
+ console.log(JSON.stringify({ key, value: null }));
66
+ }
67
+ else {
68
+ console.log(` ${chalk.dim(key)} is not set`);
69
+ }
70
+ return;
71
+ }
72
+ // Mask apiKey
73
+ const displayValue = key === "apiKey"
74
+ ? `${value.slice(0, 16)}${"•".repeat(value.length - 16)}`
75
+ : value;
76
+ if (jsonMode) {
77
+ console.log(JSON.stringify({ key, value: displayValue }));
78
+ }
79
+ else {
80
+ console.log(` ${chalk.dim(key.padEnd(10))} ${displayValue}`);
81
+ }
82
+ });
83
+ // ── set ───────────────────────────────────────────────────────────────────────
84
+ config
85
+ .command("set <key> <value>")
86
+ .description(`Set a config value (keys: ${VALID_KEYS.join(", ")})`)
87
+ .option("--json", "Machine-readable output")
88
+ .action((key, value, opts) => {
89
+ const jsonMode = !!opts.json;
90
+ if (!VALID_KEYS.includes(key)) {
91
+ printError(`Unknown key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`, undefined, jsonMode);
92
+ process.exit(1);
93
+ }
94
+ setConfigValue(key, value);
95
+ printSuccess({ key, saved: "true", configFile: CONFIG_FILE }, jsonMode);
96
+ });
97
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * cv publish — publish content to CareerVivid
3
+ *
4
+ * Usage:
5
+ * cv publish <file> Publish a file
6
+ * cv publish - Read from stdin
7
+ * cv publish README.md --title "..." Override title
8
+ * cv publish arch.mmd --type whiteboard --format mermaid
9
+ * cv publish - --dry-run < article.md Validate without publishing
10
+ * cv publish - --json < article.md Agent-friendly JSON output
11
+ */
12
+ import { Command } from "commander";
13
+ export declare function registerPublishCommand(program: Command): void;
14
+ //# sourceMappingURL=publish.d.ts.map
@@ -0,0 +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,CAoJ7D"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * cv publish — publish content to CareerVivid
3
+ *
4
+ * Usage:
5
+ * cv publish <file> Publish a file
6
+ * cv publish - Read from stdin
7
+ * cv publish README.md --title "..." Override title
8
+ * cv publish arch.mmd --type whiteboard --format mermaid
9
+ * cv publish - --dry-run < article.md Validate without publishing
10
+ * cv publish - --json < article.md Agent-friendly JSON output
11
+ */
12
+ import { readFileSync } from "fs";
13
+ import { extname } from "path";
14
+ import chalk from "chalk";
15
+ import ora from "ora";
16
+ import { publishPost, isApiError } from "../api.js";
17
+ import { printError, printSuccess, handleApiError } from "../output.js";
18
+ function inferFormat(filePath) {
19
+ const ext = extname(filePath).toLowerCase();
20
+ if ([".mmd", ".mermaid"].includes(ext))
21
+ return "mermaid";
22
+ return "markdown";
23
+ }
24
+ function inferType(format) {
25
+ return format === "mermaid" ? "whiteboard" : "article";
26
+ }
27
+ async function readStdin() {
28
+ const chunks = [];
29
+ for await (const chunk of process.stdin) {
30
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
31
+ }
32
+ return Buffer.concat(chunks).toString("utf-8");
33
+ }
34
+ export function registerPublishCommand(program) {
35
+ program
36
+ .command("publish [file]")
37
+ .description([
38
+ "Publish a markdown or mermaid file to CareerVivid",
39
+ "",
40
+ " cv publish article.md",
41
+ " cv publish diagram.mmd --type whiteboard",
42
+ " cat article.md | cv publish - --title \"My Article\" --json",
43
+ ].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")
48
+ .option("--cover <url>", "URL to a cover image")
49
+ .option("--dry-run", "Validate payload without publishing")
50
+ .option("--json", "Machine-readable JSON output")
51
+ .action(async (fileArg, opts) => {
52
+ const jsonMode = !!opts.json;
53
+ const dryRun = !!opts.dryRun;
54
+ // ── Read content ────────────────────────────────────────────────────────
55
+ let content;
56
+ let filePath;
57
+ if (!fileArg || fileArg === "-") {
58
+ if (!jsonMode) {
59
+ console.log(` ${chalk.dim("Reading from stdin... (Ctrl+D to finish)")}`);
60
+ }
61
+ content = await readStdin();
62
+ filePath = "stdin";
63
+ }
64
+ else {
65
+ try {
66
+ content = readFileSync(fileArg, "utf-8");
67
+ filePath = fileArg;
68
+ }
69
+ catch (err) {
70
+ printError(`Cannot read file: ${err.message}`, undefined, jsonMode);
71
+ process.exit(1);
72
+ }
73
+ }
74
+ if (!content.trim()) {
75
+ printError("Content is empty.", undefined, jsonMode);
76
+ process.exit(1);
77
+ }
78
+ // ── Infer format and type ───────────────────────────────────────────────
79
+ const format = opts.format ||
80
+ (filePath !== "stdin" ? inferFormat(filePath) : "markdown");
81
+ const type = opts.type || inferType(format);
82
+ // ── Infer title from first heading if not provided ─────────────────────
83
+ let title = opts.title;
84
+ if (!title && format === "markdown") {
85
+ const firstHeading = content.match(/^#\s+(.+)$/m);
86
+ if (firstHeading) {
87
+ title = firstHeading[1].trim();
88
+ }
89
+ }
90
+ if (!title) {
91
+ if (jsonMode) {
92
+ printError("Title is required. Use --title <title> or add a # heading in your markdown.", undefined, true);
93
+ process.exit(1);
94
+ }
95
+ // Interactive prompt fallback
96
+ const { prompt } = await import("enquirer");
97
+ const answers = await prompt({
98
+ type: "input",
99
+ name: "title",
100
+ message: "Post title",
101
+ });
102
+ title = answers.title.trim();
103
+ }
104
+ // ── Build payload ──────────────────────────────────────────────────────
105
+ const tags = opts.tags
106
+ ? opts.tags.split(",").map((t) => t.trim()).filter(Boolean)
107
+ : [];
108
+ if (tags.length > 5) {
109
+ printError("Maximum 5 tags allowed.", undefined, jsonMode);
110
+ process.exit(1);
111
+ }
112
+ const payload = {
113
+ type,
114
+ dataFormat: format,
115
+ title,
116
+ content,
117
+ tags,
118
+ ...(opts.cover ? { coverImage: opts.cover } : {}),
119
+ };
120
+ // ── Publish ────────────────────────────────────────────────────────────
121
+ const spinner = jsonMode || dryRun
122
+ ? null
123
+ : ora(`${dryRun ? "Validating" : "Publishing"} ${type}...`).start();
124
+ const result = await publishPost(payload, dryRun);
125
+ spinner?.stop();
126
+ if (isApiError(result)) {
127
+ handleApiError(result, jsonMode);
128
+ }
129
+ printSuccess({
130
+ Title: title,
131
+ URL: result.url,
132
+ "Post ID": result.postId,
133
+ ...(dryRun ? { Note: "Dry run — not published" } : {}),
134
+ }, jsonMode);
135
+ });
136
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Config Manager — reads/writes ~/.careervividrc.json
3
+ *
4
+ * Stored fields:
5
+ * apiKey — cv_live_... key
6
+ * apiUrl — override for the publish endpoint (default: prod)
7
+ */
8
+ export declare const CONFIG_FILE: string;
9
+ export declare const DEFAULT_API_URL = "https://careervivid.app/api/publish";
10
+ export interface CareerVividConfig {
11
+ apiKey?: string;
12
+ apiUrl?: string;
13
+ }
14
+ export declare function loadConfig(): CareerVividConfig;
15
+ export declare function saveConfig(config: CareerVividConfig): void;
16
+ export declare function getApiKey(): string | undefined;
17
+ export declare function getApiUrl(): string;
18
+ export declare function setConfigValue(key: keyof CareerVividConfig, value: string): void;
19
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +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"}
package/dist/config.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Config Manager — reads/writes ~/.careervividrc.json
3
+ *
4
+ * Stored fields:
5
+ * apiKey — cv_live_... key
6
+ * apiUrl — override for the publish endpoint (default: prod)
7
+ */
8
+ import { homedir } from "os";
9
+ import { join } from "path";
10
+ import { readFileSync, writeFileSync, existsSync } from "fs";
11
+ export const CONFIG_FILE = join(homedir(), ".careervividrc.json");
12
+ export const DEFAULT_API_URL = "https://careervivid.app/api/publish";
13
+ export function loadConfig() {
14
+ if (!existsSync(CONFIG_FILE))
15
+ return {};
16
+ try {
17
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
18
+ return JSON.parse(raw);
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ export function saveConfig(config) {
25
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
26
+ }
27
+ export function getApiKey() {
28
+ // Priority: env var > config file
29
+ return process.env.CV_API_KEY || loadConfig().apiKey;
30
+ }
31
+ export function getApiUrl() {
32
+ return process.env.CV_API_URL || loadConfig().apiUrl || DEFAULT_API_URL;
33
+ }
34
+ export function setConfigValue(key, value) {
35
+ const current = loadConfig();
36
+ current[key] = value;
37
+ saveConfig(current);
38
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CareerVivid CLI — Entry Point
4
+ *
5
+ * Usage:
6
+ * cv publish <file> Publish a markdown/mermaid file
7
+ * cv publish - Read from stdin (pipe-friendly)
8
+ * cv auth set-key Save your API key
9
+ * cv auth check Test that your API key is valid
10
+ * cv config show Print current configuration
11
+ * cv config set <key> <value> Update a config value
12
+ * cv config get <key> Print a config value
13
+ * cv --help / cv --version
14
+ */
15
+ export {};
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;GAYG"}
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CareerVivid CLI — Entry Point
4
+ *
5
+ * Usage:
6
+ * cv publish <file> Publish a markdown/mermaid file
7
+ * cv publish - Read from stdin (pipe-friendly)
8
+ * cv auth set-key Save your API key
9
+ * cv auth check Test that your API key is valid
10
+ * cv config show Print current configuration
11
+ * cv config set <key> <value> Update a config value
12
+ * cv config get <key> Print a config value
13
+ * cv --help / cv --version
14
+ */
15
+ import { Command } from "commander";
16
+ import { registerAuthCommand } from "./commands/auth.js";
17
+ import { registerPublishCommand } from "./commands/publish.js";
18
+ import { registerConfigCommand } from "./commands/config.js";
19
+ const program = new Command();
20
+ program
21
+ .name("cv")
22
+ .description("CareerVivid CLI — publish articles, diagrams, and portfolio updates from your terminal or AI agent")
23
+ .version("1.0.0", "-v, --version", "Print CLI version")
24
+ .helpOption("-h, --help", "Show help");
25
+ registerAuthCommand(program);
26
+ registerPublishCommand(program);
27
+ registerConfigCommand(program);
28
+ program.parseAsync(process.argv).catch((err) => {
29
+ console.error(`\nFatal error: ${err.message}`);
30
+ process.exit(1);
31
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Output helpers — pretty-print (human) vs --json (machine) modes.
3
+ *
4
+ * Every command receives an `opts.json` flag. Pass it through to these helpers
5
+ * to ensure consistent behaviour across human and agent callers.
6
+ */
7
+ import type { ApiError } from "./api.js";
8
+ export declare function printSuccess(fields: Record<string, string>, jsonMode: boolean): void;
9
+ export declare function printError(message: string, fields?: {
10
+ field: string;
11
+ message: string;
12
+ }[], jsonMode?: boolean): void;
13
+ export declare function printInfo(message: string, jsonMode: boolean): void;
14
+ export declare function printTable(rows: Record<string, string>[], jsonMode: boolean): void;
15
+ export declare function handleApiError(err: ApiError, jsonMode: boolean): never;
16
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIzC,wBAAgB,YAAY,CACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,QAAQ,EAAE,OAAO,GAClB,IAAI,CAaN;AAED,wBAAgB,UAAU,CACtB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EAC7C,QAAQ,UAAQ,GACjB,IAAI,CAeN;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAIlE;AAED,wBAAgB,UAAU,CACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAC9B,QAAQ,EAAE,OAAO,GAClB,IAAI,CAWN;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAG,KAAK,CAGtE"}
package/dist/output.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Output helpers — pretty-print (human) vs --json (machine) modes.
3
+ *
4
+ * Every command receives an `opts.json` flag. Pass it through to these helpers
5
+ * to ensure consistent behaviour across human and agent callers.
6
+ */
7
+ import chalk from "chalk";
8
+ // ── Output helpers ─────────────────────────────────────────────────────────────
9
+ export function printSuccess(fields, jsonMode) {
10
+ if (jsonMode) {
11
+ console.log(JSON.stringify({ success: true, ...fields }));
12
+ return;
13
+ }
14
+ console.log();
15
+ console.log(` ${chalk.green("✔")} ${chalk.bold("Success!")}`);
16
+ console.log();
17
+ for (const [label, value] of Object.entries(fields)) {
18
+ console.log(` ${chalk.dim(label.padEnd(10))} ${chalk.cyan(value)}`);
19
+ }
20
+ console.log();
21
+ }
22
+ export function printError(message, fields, jsonMode = false) {
23
+ if (jsonMode) {
24
+ console.error(JSON.stringify({ success: false, error: message, fields }));
25
+ return;
26
+ }
27
+ console.error();
28
+ console.error(` ${chalk.red("✖")} ${chalk.bold("Error:")} ${message}`);
29
+ if (fields && fields.length > 0) {
30
+ console.error();
31
+ for (const f of fields) {
32
+ console.error(` ${chalk.yellow("•")} ${chalk.bold(f.field)}: ${f.message}`);
33
+ }
34
+ }
35
+ console.error();
36
+ }
37
+ export function printInfo(message, jsonMode) {
38
+ if (!jsonMode) {
39
+ console.log(` ${chalk.blue("ℹ")} ${message}`);
40
+ }
41
+ }
42
+ export function printTable(rows, jsonMode) {
43
+ if (jsonMode) {
44
+ console.log(JSON.stringify(rows));
45
+ return;
46
+ }
47
+ for (const row of rows) {
48
+ for (const [k, v] of Object.entries(row)) {
49
+ console.log(` ${chalk.dim(k.padEnd(12))} ${v}`);
50
+ }
51
+ console.log();
52
+ }
53
+ }
54
+ export function handleApiError(err, jsonMode) {
55
+ printError(err.message, err.fields, jsonMode);
56
+ process.exit(1);
57
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "careervivid",
3
+ "version": "1.0.0",
4
+ "description": "Official CLI for CareerVivid — publish articles, diagrams, and portfolio updates from your terminal or AI agent",
5
+ "type": "module",
6
+ "bin": {
7
+ "cv": "dist/index.js",
8
+ "careervivid": "dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "node --loader ts-node/esm src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "chalk": "^5.3.0",
24
+ "commander": "^12.1.0",
25
+ "enquirer": "^2.4.1",
26
+ "ora": "^8.1.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^20.0.0",
30
+ "ts-node": "^10.9.2",
31
+ "typescript": "^5.4.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "keywords": [
37
+ "careervivid",
38
+ "cli",
39
+ "developer-tools",
40
+ "publish",
41
+ "portfolio",
42
+ "ai-agent",
43
+ "mcp"
44
+ ],
45
+ "author": "CareerVivid",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/Jastalk/CareerVivid.git",
50
+ "directory": "cli"
51
+ },
52
+ "homepage": "https://careervivid.app",
53
+ "bugs": {
54
+ "url": "https://github.com/Jastalk/CareerVivid/issues"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }