@witchpot/devlog-cli 0.1.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.
@@ -0,0 +1,149 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { readStoredToken } from "./auth/token-storage.js";
5
+ export const CONFIG_DIR = join(homedir(), ".devlog-cli");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+ function isValidConfigFile(obj) {
8
+ if (typeof obj !== "object" || obj === null)
9
+ return false;
10
+ const c = obj;
11
+ if (c.activeProjectId !== undefined && typeof c.activeProjectId !== "string")
12
+ return false;
13
+ if (c.apiUrl !== undefined && typeof c.apiUrl !== "string")
14
+ return false;
15
+ return true;
16
+ }
17
+ function ensureConfigDir() {
18
+ mkdirSync(CONFIG_DIR, { mode: 0o700, recursive: true });
19
+ }
20
+ function loadConfigFile() {
21
+ try {
22
+ const content = readFileSync(CONFIG_FILE, "utf-8");
23
+ const parsed = JSON.parse(content);
24
+ if (!isValidConfigFile(parsed))
25
+ return null;
26
+ return parsed;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function saveConfigFile(config) {
33
+ ensureConfigDir();
34
+ // Merge with existing config
35
+ const existing = loadConfigFile() || {};
36
+ const merged = { ...existing, ...config };
37
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", {
38
+ mode: 0o600,
39
+ });
40
+ }
41
+ function isLoopback(hostname) {
42
+ return ["localhost", "127.0.0.1", "::1"].includes(hostname);
43
+ }
44
+ function validateUrl(url) {
45
+ const cleanUrl = url.replace(/\/$/, "");
46
+ try {
47
+ const parsed = new URL(cleanUrl);
48
+ if (!["http:", "https:"].includes(parsed.protocol)) {
49
+ console.error(`Invalid URL protocol: ${parsed.protocol} -- only http/https are allowed.`);
50
+ process.exit(1);
51
+ }
52
+ if (parsed.protocol === "http:" && !isLoopback(parsed.hostname)) {
53
+ console.error(`Warning: Using unencrypted HTTP with a non-local host (${parsed.hostname}). Use HTTPS for production.`);
54
+ process.exit(1);
55
+ }
56
+ }
57
+ catch {
58
+ console.error(`Invalid URL: ${cleanUrl}`);
59
+ process.exit(1);
60
+ }
61
+ return cleanUrl;
62
+ }
63
+ /**
64
+ * Resolve configuration from flags, env vars, config file, and stored token.
65
+ *
66
+ * Priority:
67
+ * 1. --token flag / DEVLOG_TOKEN env var (explicit token)
68
+ * 2. Stored token (from `devlog login`)
69
+ * 3. Neither -- error at request time
70
+ */
71
+ export function resolveConfig(flags) {
72
+ const configFile = loadConfigFile();
73
+ // Resolve token
74
+ const explicitToken = flags["token"] || process.env.DEVLOG_TOKEN || undefined;
75
+ // Resolve API URL
76
+ const storedToken = readStoredToken();
77
+ const apiUrl = flags["url"] ||
78
+ process.env.DEVLOG_URL ||
79
+ configFile?.apiUrl ||
80
+ storedToken?.apiUrl ||
81
+ "https://devlog.witchpot.com";
82
+ const token = explicitToken || storedToken?.token || undefined;
83
+ return {
84
+ apiUrl: validateUrl(apiUrl),
85
+ token,
86
+ };
87
+ }
88
+ /**
89
+ * Get the active project ID from config.
90
+ * Returns the ID or exits with a helpful error message.
91
+ */
92
+ export function getActiveProjectId() {
93
+ const config = loadConfigFile();
94
+ if (!config?.activeProjectId) {
95
+ console.error('No project specified. Run "devlog project use <id>" first, or pass a project ID as an argument.');
96
+ process.exit(1);
97
+ }
98
+ return config.activeProjectId;
99
+ }
100
+ /**
101
+ * Set the active project ID in config.
102
+ */
103
+ export function setActiveProjectId(projectId) {
104
+ saveConfigFile({ activeProjectId: projectId });
105
+ }
106
+ /**
107
+ * Get the active project ID from config, or undefined if not set.
108
+ * Does not exit on missing -- use when the ID is optional.
109
+ */
110
+ export function getActiveProjectIdOptional() {
111
+ const config = loadConfigFile();
112
+ return config?.activeProjectId;
113
+ }
114
+ /**
115
+ * Parse CLI arguments into command, positional args, and flags.
116
+ */
117
+ export function parseArgs(argv) {
118
+ const rawArgs = argv.slice(2);
119
+ const command = rawArgs[0] || "help";
120
+ const rest = rawArgs.slice(1);
121
+ const flags = {};
122
+ const args = [];
123
+ for (let i = 0; i < rest.length; i++) {
124
+ const arg = rest[i];
125
+ if (arg.startsWith("--")) {
126
+ const eqIndex = arg.indexOf("=");
127
+ if (eqIndex !== -1) {
128
+ const key = arg.slice(2, eqIndex);
129
+ flags[key] = arg.slice(eqIndex + 1);
130
+ }
131
+ else {
132
+ const key = arg.slice(2);
133
+ const next = rest[i + 1];
134
+ if (next && !next.startsWith("--")) {
135
+ flags[key] = next;
136
+ i++;
137
+ }
138
+ else {
139
+ flags[key] = "true";
140
+ }
141
+ }
142
+ }
143
+ else {
144
+ args.push(arg);
145
+ }
146
+ }
147
+ return { command, args, flags };
148
+ }
149
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,23 @@
1
+ export interface ApiErrorDetail {
2
+ code?: string;
3
+ message: string;
4
+ hint?: string;
5
+ }
6
+ export declare class ApiError extends Error {
7
+ status: number;
8
+ detail: ApiErrorDetail;
9
+ constructor(status: number, detail: ApiErrorDetail);
10
+ }
11
+ /**
12
+ * Map HTTP status codes to CLI exit codes.
13
+ */
14
+ export declare function exitCodeFromError(error: ApiError): number;
15
+ /**
16
+ * Validate that an ID is safe to interpolate into a URL path.
17
+ * Blocks path traversal (../, /) and other injection characters.
18
+ */
19
+ export declare function assertSafeId(id: string, label: string): void;
20
+ /**
21
+ * Format an API error for CLI output.
22
+ */
23
+ export declare function formatError(error: ApiError): string;
@@ -0,0 +1,54 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ detail;
4
+ constructor(status, detail) {
5
+ super(detail.message);
6
+ this.status = status;
7
+ this.detail = detail;
8
+ this.name = "ApiError";
9
+ }
10
+ }
11
+ /**
12
+ * Map HTTP status codes to CLI exit codes.
13
+ */
14
+ export function exitCodeFromError(error) {
15
+ if (error.status === 401 || error.status === 403)
16
+ return 2;
17
+ if (error.status === 404)
18
+ return 3;
19
+ if (error.status === 409)
20
+ return 4;
21
+ if (error.status === 429)
22
+ return 4;
23
+ if (error.status >= 500)
24
+ return 5;
25
+ return 1;
26
+ }
27
+ const SAFE_ID_REGEX = /^[0-9a-zA-Z_-]+$/;
28
+ /**
29
+ * Validate that an ID is safe to interpolate into a URL path.
30
+ * Blocks path traversal (../, /) and other injection characters.
31
+ */
32
+ export function assertSafeId(id, label) {
33
+ if (!id || !SAFE_ID_REGEX.test(id)) {
34
+ console.error(`Invalid ${label}: ${id}`);
35
+ process.exit(1);
36
+ }
37
+ }
38
+ /**
39
+ * Format an API error for CLI output.
40
+ */
41
+ export function formatError(error) {
42
+ const lines = [];
43
+ if (error.detail.code) {
44
+ lines.push(`Error [${error.detail.code}]: ${error.detail.message}`);
45
+ }
46
+ else {
47
+ lines.push(`Error: ${error.detail.message}`);
48
+ }
49
+ if (error.detail.hint) {
50
+ lines.push(` Hint: ${error.detail.hint}`);
51
+ }
52
+ return lines.join("\n");
53
+ }
54
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/index.js ADDED
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs, resolveConfig } from "./config.js";
3
+ import { DevlogClient } from "./client.js";
4
+ import { ApiError, exitCodeFromError, formatError } from "./errors.js";
5
+ import { getFormat } from "./output.js";
6
+ // Auth commands
7
+ import { loginCommand, logoutCommand, statusCommand, } from "./commands/auth.js";
8
+ // API commands
9
+ import { projectListCommand, projectShowCommand, projectCreateCommand, projectUpdateCommand, projectDeleteCommand, projectUseCommand, } from "./commands/projects.js";
10
+ import { gamesListCommand, gamesAddCommand, gamesRemoveCommand, } from "./commands/games.js";
11
+ import { historyListCommand, historyAddCommand, } from "./commands/history.js";
12
+ import { draftListCommand, draftCreateCommand, draftUpdateCommand, draftDeleteCommand, } from "./commands/drafts.js";
13
+ import { benchListCommand, benchAddCommand, benchRemoveCommand, } from "./commands/bench.js";
14
+ import { eventsListCommand, eventsAddCommand, eventsRemoveCommand, } from "./commands/events.js";
15
+ import { contextCommand } from "./commands/context.js";
16
+ import { steamSearchCommand, steamDetailsCommand, } from "./commands/steam.js";
17
+ const HELP = `devlog - DevLog Social Copilot CLI
18
+
19
+ Usage: devlog <command> [subcommand] [options]
20
+
21
+ Authentication:
22
+ login Authenticate via browser
23
+ logout Clear stored credentials
24
+ status Show authentication status
25
+
26
+ Projects:
27
+ project list List all projects
28
+ project show [id] Show project details
29
+ project create Create a new project
30
+ project update [id] Update project
31
+ project delete <id> Delete project
32
+ project use <id> Set active project
33
+
34
+ Games (Similar Games):
35
+ games list [projectId] List similar games
36
+ games add [projectId] Add a similar game
37
+ games remove [projectId] Remove a similar game
38
+
39
+ History:
40
+ history list [projectId] List history cards
41
+ history add [projectId] Add a note or milestone
42
+
43
+ Drafts:
44
+ draft list [projectId] List post drafts
45
+ draft create [projectId] Create a draft
46
+ draft update <draftId> Update a draft
47
+ draft delete <draftId> Delete a draft
48
+
49
+ Benchmark:
50
+ bench list [projectId] List benchmark accounts
51
+ bench add [projectId] Add a benchmark account
52
+ bench remove <accountId> Remove a benchmark account
53
+
54
+ Events:
55
+ events list [projectId] List events
56
+ events add [projectId] Add an event
57
+ events remove <eventId> Remove an event
58
+
59
+ Context:
60
+ context [projectId] Get full project context (JSON)
61
+
62
+ Steam:
63
+ steam search Search Steam games
64
+ steam details <appid> Get game details
65
+
66
+ Options:
67
+ --url=<url> DevLog URL (default: http://localhost:3000)
68
+ --format=json|table Output format (default: json)
69
+ --token=<token> API token (or DEVLOG_TOKEN env var)
70
+ help Show this help
71
+
72
+ Examples:
73
+ devlog login
74
+ devlog project list --format=table
75
+ devlog project use 3f8a2b1c-...
76
+ devlog games list --format=table
77
+ devlog draft list --format=table
78
+ devlog context --section=games
79
+ devlog steam search --q=roguelike --sort=reviews_desc --limit=10
80
+ `;
81
+ // Commands that have subcommands (e.g. "project list", "games add")
82
+ const COMPOUND_COMMANDS = {
83
+ project: {
84
+ list: projectListCommand,
85
+ show: projectShowCommand,
86
+ create: projectCreateCommand,
87
+ update: projectUpdateCommand,
88
+ delete: projectDeleteCommand,
89
+ use: projectUseCommand,
90
+ },
91
+ games: {
92
+ list: gamesListCommand,
93
+ add: gamesAddCommand,
94
+ remove: gamesRemoveCommand,
95
+ },
96
+ history: {
97
+ list: historyListCommand,
98
+ add: historyAddCommand,
99
+ },
100
+ draft: {
101
+ list: draftListCommand,
102
+ create: draftCreateCommand,
103
+ update: draftUpdateCommand,
104
+ delete: draftDeleteCommand,
105
+ },
106
+ bench: {
107
+ list: benchListCommand,
108
+ add: benchAddCommand,
109
+ remove: benchRemoveCommand,
110
+ },
111
+ events: {
112
+ list: eventsListCommand,
113
+ add: eventsAddCommand,
114
+ remove: eventsRemoveCommand,
115
+ },
116
+ steam: {
117
+ search: steamSearchCommand,
118
+ details: steamDetailsCommand,
119
+ },
120
+ };
121
+ // Simple top-level commands (no subcommand)
122
+ const SIMPLE_COMMANDS = {
123
+ context: contextCommand,
124
+ };
125
+ function handleError(error, config) {
126
+ if (error instanceof ApiError) {
127
+ console.error(formatError(error));
128
+ process.exit(exitCodeFromError(error));
129
+ }
130
+ if (error instanceof TypeError && error.cause) {
131
+ const cause = error.cause;
132
+ if (cause.code === "ECONNREFUSED") {
133
+ console.error(`Connection refused: ${config.apiUrl}\nIs the DevLog app running?`);
134
+ process.exit(1);
135
+ }
136
+ }
137
+ throw error;
138
+ }
139
+ async function main() {
140
+ const { command, args, flags } = parseArgs(process.argv);
141
+ // Help
142
+ if (command === "help" || flags["help"] === "true" || !command) {
143
+ console.log(HELP);
144
+ return;
145
+ }
146
+ // Auth commands (no API client needed)
147
+ if (command === "login") {
148
+ await loginCommand(args, flags);
149
+ return;
150
+ }
151
+ if (command === "logout") {
152
+ logoutCommand();
153
+ return;
154
+ }
155
+ if (command === "status") {
156
+ statusCommand();
157
+ return;
158
+ }
159
+ // All other commands need authentication
160
+ const config = resolveConfig(flags);
161
+ const client = new DevlogClient(config);
162
+ const format = getFormat(flags);
163
+ // Compound commands (project list, games add, etc.)
164
+ const subCommands = COMPOUND_COMMANDS[command];
165
+ if (subCommands) {
166
+ const subCommand = args[0] || "list"; // default to "list"
167
+ const handler = subCommands[subCommand];
168
+ if (!handler) {
169
+ console.error(`Unknown subcommand: ${command} ${subCommand}`);
170
+ console.error('Run "devlog help" for usage.');
171
+ process.exit(1);
172
+ }
173
+ try {
174
+ await handler(client, args.slice(1), flags, format);
175
+ }
176
+ catch (error) {
177
+ handleError(error, config);
178
+ }
179
+ return;
180
+ }
181
+ // Simple commands
182
+ const simpleHandler = SIMPLE_COMMANDS[command];
183
+ if (simpleHandler) {
184
+ try {
185
+ await simpleHandler(client, args, flags, format);
186
+ }
187
+ catch (error) {
188
+ handleError(error, config);
189
+ }
190
+ return;
191
+ }
192
+ console.error(`Unknown command: ${command}`);
193
+ console.error('Run "devlog help" for usage.');
194
+ process.exit(1);
195
+ }
196
+ main();
197
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ export type OutputFormat = "json" | "table";
2
+ /**
3
+ * Determine output format from flags.
4
+ */
5
+ export declare function getFormat(flags: Record<string, string>): OutputFormat;
6
+ /**
7
+ * Print data as formatted JSON.
8
+ */
9
+ export declare function printJson(data: unknown): void;
10
+ /**
11
+ * Print an array of objects as an aligned table.
12
+ */
13
+ export declare function printTable(rows: Record<string, unknown>[], columns?: string[]): void;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Determine output format from flags.
3
+ */
4
+ export function getFormat(flags) {
5
+ if (flags["format"] === "table")
6
+ return "table";
7
+ return "json";
8
+ }
9
+ /**
10
+ * Print data as formatted JSON.
11
+ */
12
+ export function printJson(data) {
13
+ console.log(JSON.stringify(data, null, 2));
14
+ }
15
+ /**
16
+ * Print an array of objects as an aligned table.
17
+ */
18
+ export function printTable(rows, columns) {
19
+ if (rows.length === 0) {
20
+ console.log("No results found.");
21
+ return;
22
+ }
23
+ const cols = columns || Object.keys(rows[0]);
24
+ const widths = cols.map((c) => Math.max(c.length, ...rows.map((r) => {
25
+ const val = r[c];
26
+ return String(val ?? "-").length;
27
+ })));
28
+ // Cap column widths at 50 chars for readability
29
+ const maxWidth = 50;
30
+ const cappedWidths = widths.map((w) => Math.min(w, maxWidth));
31
+ // Header
32
+ console.log(cols
33
+ .map((c, i) => c.toUpperCase().padEnd(cappedWidths[i]))
34
+ .join(" "));
35
+ console.log(cols
36
+ .map((_, i) => "-".repeat(cappedWidths[i]))
37
+ .join(" "));
38
+ // Rows
39
+ for (const row of rows) {
40
+ console.log(cols
41
+ .map((c, i) => {
42
+ const val = String(row[c] ?? "-");
43
+ return truncate(val, cappedWidths[i]).padEnd(cappedWidths[i]);
44
+ })
45
+ .join(" "));
46
+ }
47
+ }
48
+ function truncate(str, max) {
49
+ if (str.length <= max)
50
+ return str;
51
+ return str.slice(0, max - 1) + "\u2026";
52
+ }
53
+ //# sourceMappingURL=output.js.map
@@ -0,0 +1,120 @@
1
+ /** Envelope returned by all /api/cli/* endpoints */
2
+ export interface ApiResponse<T> {
3
+ data: T;
4
+ error?: string;
5
+ }
6
+ export interface Project {
7
+ id: string;
8
+ title: string | null;
9
+ summary: string | null;
10
+ status: string;
11
+ phase: string | null;
12
+ created_at: string;
13
+ }
14
+ export interface ProjectDetail extends Project {
15
+ owner_type: string;
16
+ owner_id: string;
17
+ target_audience: string | null;
18
+ phase_other: string | null;
19
+ interview_context: Record<string, unknown> | null;
20
+ updated_at: string;
21
+ project_similar_games: SimilarGame[];
22
+ project_repositories: ProjectRepository[];
23
+ }
24
+ export interface SimilarGame {
25
+ id?: string;
26
+ game_name: string;
27
+ steam_appid: number | null;
28
+ tags: string[] | null;
29
+ developer: string | null;
30
+ publisher: string | null;
31
+ total_reviews: number | null;
32
+ rating_percent: number | null;
33
+ website: string | null;
34
+ note: string | null;
35
+ category?: string | null;
36
+ }
37
+ export interface ProjectRepository {
38
+ repo_full_name: string;
39
+ default_branch: string;
40
+ }
41
+ export interface HistoryCard {
42
+ id: string;
43
+ project_id: string;
44
+ source_type: string;
45
+ title: string;
46
+ summary: string | null;
47
+ metadata_json: Record<string, unknown> | null;
48
+ media_urls: string[] | null;
49
+ occurred_at: string;
50
+ created_at: string;
51
+ updated_at: string;
52
+ }
53
+ export interface PostDraft {
54
+ id: string;
55
+ project_id: string;
56
+ platform: string;
57
+ hook: string | null;
58
+ body: string;
59
+ hashtags: string[] | null;
60
+ cta: string | null;
61
+ status: string;
62
+ rationale: string | null;
63
+ language: string | null;
64
+ post_type: string | null;
65
+ emotional_hook: string | null;
66
+ cta_type: string | null;
67
+ media_style: string | null;
68
+ media_urls: string[] | null;
69
+ created_at: string;
70
+ updated_at: string;
71
+ }
72
+ export interface BenchAccount {
73
+ id: string;
74
+ project_id: string;
75
+ platform: string;
76
+ handle: string;
77
+ source: string;
78
+ reason: string | null;
79
+ developer_name: string | null;
80
+ game_name: string | null;
81
+ steam_appid: number | null;
82
+ follower_count: number | null;
83
+ created_at: string;
84
+ }
85
+ export interface EventSchedule {
86
+ id: string;
87
+ project_id: string;
88
+ event_name: string;
89
+ event_date: string;
90
+ importance: string | null;
91
+ }
92
+ export interface ProjectContext {
93
+ project: Project;
94
+ games: SimilarGame[];
95
+ repository: ProjectRepository | null;
96
+ recentHistory: HistoryCard[];
97
+ recentDrafts: PostDraft[];
98
+ benchAccounts: (BenchAccount & {
99
+ post_count: number;
100
+ })[];
101
+ upcomingEvents: EventSchedule[];
102
+ }
103
+ export interface SteamSearchGame {
104
+ appid: number;
105
+ name: string;
106
+ priceFinal: number | null;
107
+ totalReviews: number | null;
108
+ ratingPercent: number | null;
109
+ releaseDate: string | null;
110
+ tags: string[];
111
+ }
112
+ export interface SteamSearchResult {
113
+ games: SteamSearchGame[];
114
+ total: number;
115
+ hasMore: boolean;
116
+ nextCursor: string | null;
117
+ }
118
+ import type { DevlogClient } from "./client.js";
119
+ import type { OutputFormat } from "./output.js";
120
+ export type CommandHandler = (client: DevlogClient, args: string[], flags: Record<string, string>, format: OutputFormat) => Promise<void>;
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@witchpot/devlog-cli",
3
+ "version": "0.1.0",
4
+ "description": "DevLog Social Copilot CLI",
5
+ "type": "module",
6
+ "main": "./build/index.js",
7
+ "bin": {
8
+ "devlog": "./build/index.js"
9
+ },
10
+ "files": [
11
+ "build/**/*.js",
12
+ "build/**/*.d.ts",
13
+ "!build/__tests__"
14
+ ],
15
+ "keywords": [
16
+ "devlog",
17
+ "gamedev",
18
+ "indie",
19
+ "marketing",
20
+ "cli"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/Witchpot/DevLogSystem.git",
25
+ "directory": "packages/cli"
26
+ },
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "dev": "tsc --watch",
34
+ "clean": "rm -rf build",
35
+ "start": "node build/index.js",
36
+ "prepublishOnly": "npm run build",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20",
42
+ "typescript": "^5",
43
+ "vitest": "^4.1.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }