Skip to content

Commit 0de5263

Browse files
facundofariasclaude
andcommitted
Fix auth for create command and add global config support
- Add --api-key to create command (required for --private, optional for public) - Send Authorization header on doc creation so private docs actually work - Show private URL with ?token= for private doc creation output - Add dm login/logout/whoami for global credentials (~/.config/draftmark/config.json) - All resolve functions now check: flag > env > .draftmark.json > global config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 710a953 commit 0de5263

2 files changed

Lines changed: 173 additions & 12 deletions

File tree

src/cli.ts

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import { api } from "./api.js";
66
import {
77
appendConfig,
88
getLastEntry,
9+
readGlobalConfig,
910
resolveApiKey,
11+
resolveApiKeyOptional,
1012
resolveMagicToken,
1113
resolveSlug,
14+
writeGlobalConfig,
1215
} from "./config.js";
1316
import {
1417
bold,
@@ -43,6 +46,7 @@ program
4346
.option("--title <title>", "Document title")
4447
.option("--expected-reviews <n>", "Number of expected reviews", parseInt)
4548
.option("--review-deadline <date>", "Review deadline (ISO date)")
49+
.option("--api-key <key>", "Account API key (required for private docs)")
4650
.option("--json", "Output raw JSON response")
4751
.action(async (file: string, opts) => {
4852
info("Creating document...");
@@ -55,6 +59,13 @@ program
5559
process.exit(1);
5660
}
5761

62+
// Resolve API key (required for private, optional for public)
63+
const entry = await getLastEntry();
64+
const global = await readGlobalConfig();
65+
const apiKey = opts.private
66+
? resolveApiKey(opts, entry, global)
67+
: resolveApiKeyOptional(opts, entry, global);
68+
5869
const body: Record<string, unknown> = { content };
5970
if (opts.title) body.title = opts.title;
6071
if (opts.private) body.visibility = "private";
@@ -69,6 +80,7 @@ program
6980
}>("/docs", {
7081
method: "POST",
7182
body,
83+
apiKey,
7284
});
7385

7486
if (!res.ok) {
@@ -92,6 +104,10 @@ program
92104
} else {
93105
success("Document created\n");
94106
process.stdout.write(`${label("URL", cyan(shareUrl))}\n`);
107+
if (opts.private) {
108+
const privateUrl = `${shareUrl}?token=${doc.magic_token}`;
109+
process.stdout.write(`${label("Private URL", cyan(privateUrl))}\n`);
110+
}
95111
process.stdout.write(`${label("Slug", doc.slug)}\n`);
96112
process.stdout.write(`${label("Magic Token", doc.magic_token)}\n`);
97113
process.stdout.write(`${label("API Key", doc.api_key)}\n`);
@@ -110,8 +126,9 @@ program
110126
.option("--json", "Output raw JSON response")
111127
.action(async (slugArg: string | undefined, opts) => {
112128
const entry = await getLastEntry();
129+
const global = await readGlobalConfig();
113130
const slug = resolveSlug(slugArg, entry);
114-
const apiKey = resolveApiKey(opts, entry);
131+
const apiKey = resolveApiKey(opts, entry, global);
115132

116133
const res = await api<Record<string, unknown>>(`/docs/${slug}`, {
117134
apiKey,
@@ -163,8 +180,9 @@ program
163180
.option("--json", "Output raw JSON response")
164181
.action(async (slugArg: string | undefined, opts) => {
165182
const entry = await getLastEntry();
183+
const global = await readGlobalConfig();
166184
const slug = resolveSlug(slugArg, entry);
167-
const apiKey = resolveApiKey(opts, entry);
185+
const apiKey = resolveApiKey(opts, entry, global);
168186

169187
const params: Record<string, string> = {};
170188
if (opts.status) params.status = opts.status;
@@ -210,6 +228,7 @@ program
210228
.option("--json", "Output raw JSON response")
211229
.action(async (first: string, second: string | undefined, opts) => {
212230
const entry = await getLastEntry();
231+
const global = await readGlobalConfig();
213232

214233
// If only one positional arg, it's the body and slug comes from config
215234
let slug: string;
@@ -222,7 +241,7 @@ program
222241
body = second;
223242
}
224243

225-
const apiKey = resolveApiKey(opts, entry);
244+
const apiKey = resolveApiKey(opts, entry, global);
226245

227246
const payload: Record<string, unknown> = { body };
228247
if (opts.author) payload.author = opts.author;
@@ -268,8 +287,9 @@ program
268287
.option("--json", "Output raw JSON response")
269288
.action(async (slugArg: string | undefined, opts) => {
270289
const entry = await getLastEntry();
290+
const global = await readGlobalConfig();
271291
const slug = resolveSlug(slugArg, entry);
272-
const apiKey = resolveApiKey(opts, entry);
292+
const apiKey = resolveApiKey(opts, entry, global);
273293

274294
const payload: Record<string, unknown> = {};
275295
if (opts.name) payload.reviewer_name = opts.name;
@@ -309,8 +329,9 @@ program
309329
.option("--api-key <key>", "API key for authentication")
310330
.action(async (slugArg: string | undefined, opts) => {
311331
const entry = await getLastEntry();
332+
const global = await readGlobalConfig();
312333
const slug = resolveSlug(slugArg, entry);
313-
const apiKey = resolveApiKey(opts, entry);
334+
const apiKey = resolveApiKey(opts, entry, global);
314335

315336
const res = await api<string>(`/docs/${slug}`, {
316337
apiKey,
@@ -336,8 +357,9 @@ program
336357
.option("--json", "Output raw JSON response")
337358
.action(async (slugArg: string | undefined, opts) => {
338359
const entry = await getLastEntry();
360+
const global = await readGlobalConfig();
339361
const slug = resolveSlug(slugArg, entry);
340-
const magicToken = resolveMagicToken(opts, entry);
362+
const magicToken = resolveMagicToken(opts, entry, global);
341363

342364
const res = await api(`/docs/${slug}`, {
343365
method: "PATCH",
@@ -369,8 +391,9 @@ program
369391
.option("--json", "Output raw JSON response")
370392
.action(async (slugArg: string | undefined, opts) => {
371393
const entry = await getLastEntry();
394+
const global = await readGlobalConfig();
372395
const slug = resolveSlug(slugArg, entry);
373-
const magicToken = resolveMagicToken(opts, entry);
396+
const magicToken = resolveMagicToken(opts, entry, global);
374397

375398
const res = await api(`/docs/${slug}`, {
376399
method: "PATCH",
@@ -408,8 +431,9 @@ program
408431
}
409432

410433
const entry = await getLastEntry();
434+
const global = await readGlobalConfig();
411435
const slug = resolveSlug(slugArg, entry);
412-
const magicToken = resolveMagicToken(opts, entry);
436+
const magicToken = resolveMagicToken(opts, entry, global);
413437

414438
const res = await api(`/docs/${slug}`, {
415439
method: "DELETE",
@@ -429,4 +453,79 @@ program
429453
}
430454
});
431455

456+
// ---------------------------------------------------------------------------
457+
// dm login
458+
// ---------------------------------------------------------------------------
459+
program
460+
.command("login")
461+
.description("Save account credentials globally (~/.config/draftmark/config.json)")
462+
.option("--api-key <key>", "Account API key")
463+
.option("--magic-token <token>", "Default magic token")
464+
.action(async (opts) => {
465+
if (!opts.apiKey && !opts.magicToken) {
466+
error("Provide at least one of --api-key or --magic-token.");
467+
process.exit(1);
468+
}
469+
470+
const existing = await readGlobalConfig();
471+
const updated = { ...existing };
472+
if (opts.apiKey) updated.api_key = opts.apiKey;
473+
if (opts.magicToken) updated.magic_token = opts.magicToken;
474+
475+
await writeGlobalConfig(updated);
476+
success("Credentials saved to ~/.config/draftmark/config.json");
477+
});
478+
479+
// ---------------------------------------------------------------------------
480+
// dm logout
481+
// ---------------------------------------------------------------------------
482+
program
483+
.command("logout")
484+
.description("Remove saved global credentials")
485+
.action(async () => {
486+
await writeGlobalConfig({});
487+
success("Global credentials removed.");
488+
});
489+
490+
// ---------------------------------------------------------------------------
491+
// dm whoami
492+
// ---------------------------------------------------------------------------
493+
program
494+
.command("whoami")
495+
.description("Show current authentication sources")
496+
.action(async () => {
497+
const entry = await getLastEntry();
498+
const global = await readGlobalConfig();
499+
500+
process.stdout.write("\n");
501+
502+
// Global config
503+
if (global.api_key) {
504+
process.stdout.write(
505+
`${label("Global API Key", green(global.api_key.slice(0, 12) + "..."))}\n`
506+
);
507+
} else {
508+
process.stdout.write(`${label("Global API Key", dim("not set"))}\n`);
509+
}
510+
511+
// Env vars
512+
if (process.env.DM_API_KEY) {
513+
process.stdout.write(`${label("DM_API_KEY env", green("set"))}\n`);
514+
}
515+
if (process.env.DM_MAGIC_TOKEN) {
516+
process.stdout.write(`${label("DM_MAGIC_TOKEN env", green("set"))}\n`);
517+
}
518+
519+
// Local config
520+
if (entry) {
521+
process.stdout.write(
522+
`${label("Local config", green(`.draftmark.json (${entry.slug})`))}\n`
523+
);
524+
} else {
525+
process.stdout.write(`${label("Local config", dim("no .draftmark.json"))}\n`);
526+
}
527+
528+
process.stdout.write("\n");
529+
});
530+
432531
program.parse();

src/config.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { readFile, writeFile } from "node:fs/promises";
1+
import { readFile, writeFile, mkdir } from "node:fs/promises";
22
import { join } from "node:path";
3+
import { homedir } from "node:os";
34

45
const CONFIG_FILE = ".draftmark.json";
56

@@ -10,6 +11,15 @@ export interface DraftmarkEntry {
1011
url: string;
1112
}
1213

14+
export interface GlobalConfig {
15+
api_key?: string;
16+
magic_token?: string;
17+
}
18+
19+
// ---------------------------------------------------------------------------
20+
// Local config (.draftmark.json in cwd)
21+
// ---------------------------------------------------------------------------
22+
1323
function configPath(): string {
1424
return join(process.cwd(), CONFIG_FILE);
1525
}
@@ -39,25 +49,77 @@ export async function getLastEntry(): Promise<DraftmarkEntry | null> {
3949
return entries.length > 0 ? entries[entries.length - 1] : null;
4050
}
4151

52+
// ---------------------------------------------------------------------------
53+
// Global config (~/.config/draftmark/config.json)
54+
// ---------------------------------------------------------------------------
55+
56+
function globalConfigDir(): string {
57+
return join(homedir(), ".config", "draftmark");
58+
}
59+
60+
function globalConfigPath(): string {
61+
return join(globalConfigDir(), "config.json");
62+
}
63+
64+
export async function readGlobalConfig(): Promise<GlobalConfig> {
65+
try {
66+
const raw = await readFile(globalConfigPath(), "utf-8");
67+
return JSON.parse(raw);
68+
} catch {
69+
return {};
70+
}
71+
}
72+
73+
export async function writeGlobalConfig(config: GlobalConfig): Promise<void> {
74+
await mkdir(globalConfigDir(), { recursive: true });
75+
await writeFile(globalConfigPath(), JSON.stringify(config, null, 2) + "\n");
76+
}
77+
78+
// ---------------------------------------------------------------------------
79+
// Resolution helpers (flag > env > local config > global config)
80+
// ---------------------------------------------------------------------------
81+
4282
export function resolveSlug(slugArg: string | undefined, entry: DraftmarkEntry | null): string {
4383
if (slugArg) return slugArg;
4484
if (entry?.slug) return entry.slug;
4585
throw new Error("No slug provided and no .draftmark.json found in current directory.");
4686
}
4787

48-
export function resolveApiKey(opts: { apiKey?: string }, entry: DraftmarkEntry | null): string {
88+
export function resolveApiKey(
89+
opts: { apiKey?: string },
90+
entry: DraftmarkEntry | null,
91+
global?: GlobalConfig
92+
): string {
4993
if (opts.apiKey) return opts.apiKey;
5094
if (process.env.DM_API_KEY) return process.env.DM_API_KEY;
5195
if (entry?.api_key) return entry.api_key;
96+
if (global?.api_key) return global.api_key;
5297
throw new Error(
53-
"No API key found. Provide --api-key, set DM_API_KEY, or run from a directory with .draftmark.json."
98+
"No API key found. Provide --api-key, set DM_API_KEY, run from a directory with .draftmark.json, or run `dm login`."
5499
);
55100
}
56101

57-
export function resolveMagicToken(opts: { magicToken?: string }, entry: DraftmarkEntry | null): string {
102+
export function resolveApiKeyOptional(
103+
opts: { apiKey?: string },
104+
entry: DraftmarkEntry | null,
105+
global?: GlobalConfig
106+
): string | undefined {
107+
if (opts.apiKey) return opts.apiKey;
108+
if (process.env.DM_API_KEY) return process.env.DM_API_KEY;
109+
if (entry?.api_key) return entry.api_key;
110+
if (global?.api_key) return global.api_key;
111+
return undefined;
112+
}
113+
114+
export function resolveMagicToken(
115+
opts: { magicToken?: string },
116+
entry: DraftmarkEntry | null,
117+
global?: GlobalConfig
118+
): string {
58119
if (opts.magicToken) return opts.magicToken;
59120
if (process.env.DM_MAGIC_TOKEN) return process.env.DM_MAGIC_TOKEN;
60121
if (entry?.magic_token) return entry.magic_token;
122+
if (global?.magic_token) return global.magic_token;
61123
throw new Error(
62124
"No magic token found. Provide --magic-token, set DM_MAGIC_TOKEN, or ensure .draftmark.json has magic_token."
63125
);

0 commit comments

Comments
 (0)