fantsec-docmost-cli 2.2.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/build/__tests__/cli-utils.test.js +287 -0
  4. package/build/__tests__/client-pagination.test.js +103 -0
  5. package/build/__tests__/discovery.test.js +40 -0
  6. package/build/__tests__/envelope.test.js +91 -0
  7. package/build/__tests__/filters.test.js +235 -0
  8. package/build/__tests__/integration/comment.test.js +48 -0
  9. package/build/__tests__/integration/discovery.test.js +24 -0
  10. package/build/__tests__/integration/file.test.js +33 -0
  11. package/build/__tests__/integration/group.test.js +48 -0
  12. package/build/__tests__/integration/helpers/global-setup.js +80 -0
  13. package/build/__tests__/integration/helpers/run-cli.js +163 -0
  14. package/build/__tests__/integration/invite.test.js +34 -0
  15. package/build/__tests__/integration/page.test.js +69 -0
  16. package/build/__tests__/integration/search.test.js +45 -0
  17. package/build/__tests__/integration/share.test.js +49 -0
  18. package/build/__tests__/integration/space.test.js +56 -0
  19. package/build/__tests__/integration/user.test.js +15 -0
  20. package/build/__tests__/integration/workspace.test.js +42 -0
  21. package/build/__tests__/markdown-converter.test.js +445 -0
  22. package/build/__tests__/mcp-tooling.test.js +58 -0
  23. package/build/__tests__/page-mentions.test.js +65 -0
  24. package/build/__tests__/tiptap-extensions.test.js +135 -0
  25. package/build/client.js +715 -0
  26. package/build/commands/comment.js +54 -0
  27. package/build/commands/discovery.js +21 -0
  28. package/build/commands/file.js +36 -0
  29. package/build/commands/group.js +91 -0
  30. package/build/commands/invite.js +67 -0
  31. package/build/commands/page.js +227 -0
  32. package/build/commands/search.js +33 -0
  33. package/build/commands/share.js +65 -0
  34. package/build/commands/space.js +154 -0
  35. package/build/commands/user.js +38 -0
  36. package/build/commands/workspace.js +77 -0
  37. package/build/index.js +19 -0
  38. package/build/lib/auth-utils.js +53 -0
  39. package/build/lib/cli-utils.js +293 -0
  40. package/build/lib/collaboration.js +126 -0
  41. package/build/lib/filters.js +137 -0
  42. package/build/lib/markdown-converter.js +187 -0
  43. package/build/lib/mcp-tooling.js +295 -0
  44. package/build/lib/page-mentions.js +162 -0
  45. package/build/lib/tiptap-extensions.js +86 -0
  46. package/build/mcp.js +186 -0
  47. package/build/program.js +60 -0
  48. package/package.json +64 -0
@@ -0,0 +1,154 @@
1
+ import { writeFileSync } from "fs";
2
+ import { resolve, relative } from "path";
3
+ import { Option } from "commander";
4
+ import { CliError, ensureOutputSupported, parseCommaSeparatedIds, printResult, withClient, } from "../lib/cli-utils.js";
5
+ export function register(program) {
6
+ program
7
+ .command("space-list")
8
+ .description("List all available spaces")
9
+ .action(() => withClient(program, async (client, opts) => {
10
+ ensureOutputSupported(opts, { allowTable: true });
11
+ const result = await client.getSpaces();
12
+ printResult(result.items, opts, { allowTable: true, hasMore: result.hasMore });
13
+ }));
14
+ program
15
+ .command("space-info")
16
+ .description("Get space details")
17
+ .requiredOption("--space-id <id>", "Space ID")
18
+ .action((options) => withClient(program, async (client, opts) => {
19
+ ensureOutputSupported(opts, { allowTable: true });
20
+ const result = await client.getSpaceInfo(options.spaceId);
21
+ printResult(result, opts, { allowTable: true });
22
+ }));
23
+ program
24
+ .command("space-create")
25
+ .description("Create a new space")
26
+ .requiredOption("--name <name>", "Space name")
27
+ .option("--slug <slug>", "Space slug")
28
+ .option("--description <description>", "Space description")
29
+ .action((options) => withClient(program, async (client, opts) => {
30
+ ensureOutputSupported(opts);
31
+ const result = await client.createSpace(options.name, options.slug, options.description);
32
+ printResult(result, opts);
33
+ }));
34
+ program
35
+ .command("space-update")
36
+ .description("Update space settings")
37
+ .requiredOption("--space-id <id>", "Space ID")
38
+ .option("--name <name>", "Space name")
39
+ .option("--description <description>", "Space description")
40
+ .action((options) => withClient(program, async (client, opts) => {
41
+ ensureOutputSupported(opts);
42
+ const params = {
43
+ ...(options.name !== undefined && { name: options.name }),
44
+ ...(options.description !== undefined && { description: options.description }),
45
+ };
46
+ if (Object.keys(params).length === 0) {
47
+ throw new CliError("VALIDATION_ERROR", "At least one update flag is required (--name or --description).");
48
+ }
49
+ const result = await client.updateSpace(options.spaceId, params);
50
+ printResult(result, opts);
51
+ }));
52
+ program
53
+ .command("space-delete")
54
+ .description("Delete a space")
55
+ .requiredOption("--space-id <id>", "Space ID")
56
+ .action((options) => withClient(program, async (client, opts) => {
57
+ ensureOutputSupported(opts);
58
+ const result = await client.deleteSpace(options.spaceId);
59
+ printResult(result, opts);
60
+ }));
61
+ program
62
+ .command("space-export")
63
+ .description("Export a space")
64
+ .requiredOption("--space-id <id>", "Space ID")
65
+ .addOption(new Option("--export-format <format>", "Export format")
66
+ .choices(["html", "markdown"]))
67
+ .option("--include-attachments", "Include attachments in export")
68
+ .option("--output <path>", "Output file path (default: stdout)")
69
+ .action((options) => withClient(program, async (client, opts) => {
70
+ ensureOutputSupported(opts);
71
+ const data = await client.exportSpace(options.spaceId, options.exportFormat, options.includeAttachments);
72
+ if (options.output) {
73
+ const resolved = resolve(options.output);
74
+ const rel = relative(process.cwd(), resolved);
75
+ if (rel.startsWith("..") || resolve(rel) !== resolved) {
76
+ process.stderr.write(`Warning: writing to path outside CWD: ${resolved}\n`);
77
+ }
78
+ writeFileSync(resolved, Buffer.from(data));
79
+ printResult({ success: true, message: `Exported to ${resolved}` }, opts);
80
+ }
81
+ else {
82
+ process.stdout.write(Buffer.from(data));
83
+ }
84
+ }));
85
+ program
86
+ .command("space-member-list")
87
+ .description("List space members")
88
+ .requiredOption("--space-id <id>", "Space ID")
89
+ .action((options) => withClient(program, async (client, opts) => {
90
+ ensureOutputSupported(opts, { allowTable: true });
91
+ const result = await client.getSpaceMembers(options.spaceId);
92
+ printResult(result.items, opts, { allowTable: true, hasMore: result.hasMore });
93
+ }));
94
+ program
95
+ .command("space-member-add")
96
+ .description("Add members to a space")
97
+ .requiredOption("--space-id <id>", "Space ID")
98
+ .addOption(new Option("--role <role>", "Member role")
99
+ .choices(["admin", "writer", "reader"])
100
+ .makeOptionMandatory())
101
+ .option("--user-ids <ids>", "Comma-separated user IDs")
102
+ .option("--group-ids <ids>", "Comma-separated group IDs")
103
+ .action((options) => withClient(program, async (client, opts) => {
104
+ ensureOutputSupported(opts);
105
+ if (!options.userIds && !options.groupIds) {
106
+ throw new CliError("VALIDATION_ERROR", "At least one of --user-ids or --group-ids is required.");
107
+ }
108
+ const userIds = options.userIds
109
+ ? parseCommaSeparatedIds("--user-ids", options.userIds)
110
+ : undefined;
111
+ const groupIds = options.groupIds
112
+ ? parseCommaSeparatedIds("--group-ids", options.groupIds)
113
+ : undefined;
114
+ const result = await client.addSpaceMembers(options.spaceId, options.role, userIds, groupIds);
115
+ printResult(result, opts);
116
+ }));
117
+ program
118
+ .command("space-member-remove")
119
+ .description("Remove a member from a space")
120
+ .requiredOption("--space-id <id>", "Space ID")
121
+ .option("--user-id <id>", "User ID to remove")
122
+ .option("--group-id <id>", "Group ID to remove")
123
+ .action((options) => withClient(program, async (client, opts) => {
124
+ ensureOutputSupported(opts);
125
+ if (options.userId && options.groupId) {
126
+ throw new CliError("VALIDATION_ERROR", "Specify either --user-id or --group-id, not both.");
127
+ }
128
+ if (!options.userId && !options.groupId) {
129
+ throw new CliError("VALIDATION_ERROR", "Specify either --user-id or --group-id.");
130
+ }
131
+ const result = await client.removeSpaceMember(options.spaceId, options.userId, options.groupId);
132
+ printResult(result, opts);
133
+ }));
134
+ program
135
+ .command("space-member-role")
136
+ .description("Change a space member's role")
137
+ .requiredOption("--space-id <id>", "Space ID")
138
+ .addOption(new Option("--role <role>", "New role")
139
+ .choices(["admin", "writer", "reader"])
140
+ .makeOptionMandatory())
141
+ .option("--user-id <id>", "User ID")
142
+ .option("--group-id <id>", "Group ID")
143
+ .action((options) => withClient(program, async (client, opts) => {
144
+ ensureOutputSupported(opts);
145
+ if (options.userId && options.groupId) {
146
+ throw new CliError("VALIDATION_ERROR", "Specify either --user-id or --group-id, not both.");
147
+ }
148
+ if (!options.userId && !options.groupId) {
149
+ throw new CliError("VALIDATION_ERROR", "Specify either --user-id or --group-id.");
150
+ }
151
+ const result = await client.changeSpaceMemberRole(options.spaceId, options.role, options.userId, options.groupId);
152
+ printResult(result, opts);
153
+ }));
154
+ }
@@ -0,0 +1,38 @@
1
+ import { Option } from "commander";
2
+ import { CliError, ensureOutputSupported, printResult, withClient, } from "../lib/cli-utils.js";
3
+ export function register(program) {
4
+ program
5
+ .command("user-me")
6
+ .description("Get current user information")
7
+ .action(() => withClient(program, async (client, opts) => {
8
+ ensureOutputSupported(opts, { allowTable: true });
9
+ const result = await client.getCurrentUser();
10
+ printResult(result, opts, { allowTable: true });
11
+ }));
12
+ program
13
+ .command("user-update")
14
+ .description("Update current user settings")
15
+ .option("--name <name>", "User display name")
16
+ .option("--email <email>", "User email")
17
+ .option("--avatar-url <url>", "Avatar URL")
18
+ .addOption(new Option("--full-page-width <bool>", "Enable full page width").choices(["true", "false"]))
19
+ .addOption(new Option("--page-edit-mode <mode>", "Default page edit mode")
20
+ .choices(["read", "edit"]))
21
+ .option("--locale <locale>", "User locale")
22
+ .action((options) => withClient(program, async (client, opts) => {
23
+ ensureOutputSupported(opts);
24
+ const params = {
25
+ ...(options.name !== undefined && { name: options.name }),
26
+ ...(options.email !== undefined && { email: options.email }),
27
+ ...(options.avatarUrl !== undefined && { avatarUrl: options.avatarUrl }),
28
+ ...(options.fullPageWidth !== undefined && { fullPageWidth: options.fullPageWidth === "true" }),
29
+ ...(options.pageEditMode !== undefined && { pageEditMode: options.pageEditMode }),
30
+ ...(options.locale !== undefined && { locale: options.locale }),
31
+ };
32
+ if (Object.keys(params).length === 0) {
33
+ throw new CliError("VALIDATION_ERROR", "At least one update flag is required.");
34
+ }
35
+ const result = await client.updateUser(params);
36
+ printResult(result, opts);
37
+ }));
38
+ }
@@ -0,0 +1,77 @@
1
+ import { Option } from "commander";
2
+ import { CliError, ensureOutputSupported, printResult, withClient, withPublicClient, } from "../lib/cli-utils.js";
3
+ export function register(program) {
4
+ program
5
+ .command("workspace-info")
6
+ .description("Get the current Docmost workspace")
7
+ .action(() => withClient(program, async (client, opts) => {
8
+ ensureOutputSupported(opts, { allowTable: true });
9
+ const result = await client.getWorkspace();
10
+ printResult(result.data, opts, { allowTable: true });
11
+ }));
12
+ program
13
+ .command("workspace-public")
14
+ .description("Get public workspace information (no auth required)")
15
+ .action(() => withPublicClient(program, async (client, opts) => {
16
+ ensureOutputSupported(opts, { allowTable: true });
17
+ const result = await client.getWorkspacePublic();
18
+ printResult(result, opts, { allowTable: true });
19
+ }));
20
+ program
21
+ .command("workspace-update")
22
+ .description("Update workspace settings")
23
+ .option("--name <name>", "Workspace name")
24
+ .option("--hostname <hostname>", "Workspace hostname")
25
+ .option("--description <description>", "Workspace description")
26
+ .option("--logo <logo>", "Workspace logo URL")
27
+ .option("--email-domains <domains>", "Allowed email domains (comma-separated)")
28
+ .addOption(new Option("--enforce-sso <bool>", "Enforce SSO authentication").choices(["true", "false"]))
29
+ .addOption(new Option("--enforce-mfa <bool>", "Enforce multi-factor authentication").choices(["true", "false"]))
30
+ .addOption(new Option("--restrict-api-to-admins <bool>", "Restrict API access to admins only").choices(["true", "false"]))
31
+ .action((options) => withClient(program, async (client, opts) => {
32
+ ensureOutputSupported(opts);
33
+ const params = {
34
+ ...(options.name !== undefined && { name: options.name }),
35
+ ...(options.hostname !== undefined && { hostname: options.hostname }),
36
+ ...(options.description !== undefined && { description: options.description }),
37
+ ...(options.logo !== undefined && { logo: options.logo }),
38
+ ...(options.emailDomains !== undefined && { emailDomains: options.emailDomains.split(",").map(d => d.trim()).filter(Boolean) }),
39
+ ...(options.enforceSso !== undefined && { enableSSO: options.enforceSso === "true" }),
40
+ ...(options.enforceMfa !== undefined && { enableMFA: options.enforceMfa === "true" }),
41
+ ...(options.restrictApiToAdmins !== undefined && { restrictApiToAdmins: options.restrictApiToAdmins === "true" }),
42
+ };
43
+ if (Object.keys(params).length === 0) {
44
+ throw new CliError("VALIDATION_ERROR", "At least one update flag is required.");
45
+ }
46
+ const result = await client.updateWorkspace(params);
47
+ printResult(result, opts);
48
+ }));
49
+ // Member commands
50
+ program
51
+ .command("member-list")
52
+ .description("List workspace members")
53
+ .action(() => withClient(program, async (client, opts) => {
54
+ ensureOutputSupported(opts, { allowTable: true });
55
+ const result = await client.getMembers();
56
+ printResult(result.items, opts, { allowTable: true, hasMore: result.hasMore });
57
+ }));
58
+ program
59
+ .command("member-remove")
60
+ .description("Remove a member from the workspace")
61
+ .requiredOption("--user-id <id>", "User ID to remove")
62
+ .action((options) => withClient(program, async (client, opts) => {
63
+ ensureOutputSupported(opts);
64
+ const result = await client.removeMember(options.userId);
65
+ printResult(result, opts);
66
+ }));
67
+ program
68
+ .command("member-role")
69
+ .description("Change a member's role")
70
+ .requiredOption("--user-id <id>", "User ID")
71
+ .addOption(new Option("--role <role>", "New role").choices(["owner", "admin", "member"]).makeOptionMandatory())
72
+ .action((options) => withClient(program, async (client, opts) => {
73
+ ensureOutputSupported(opts);
74
+ const result = await client.changeMemberRole(options.userId, options.role);
75
+ printResult(result, opts);
76
+ }));
77
+ }
package/build/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { isCommanderHelpExit, getSafeOutput, normalizeError, printError, } from "./lib/cli-utils.js";
3
+ import { createProgram } from "./program.js";
4
+ async function main() {
5
+ const program = createProgram();
6
+ try {
7
+ await program.parseAsync(process.argv);
8
+ }
9
+ catch (error) {
10
+ if (isCommanderHelpExit(error)) {
11
+ process.exit(0);
12
+ }
13
+ const output = getSafeOutput(program);
14
+ const normalized = normalizeError(error);
15
+ printError(normalized, output);
16
+ process.exit(normalized.exitCode);
17
+ }
18
+ }
19
+ main();
@@ -0,0 +1,53 @@
1
+ import axios from "axios";
2
+ export async function getCollabToken(baseUrl, apiToken) {
3
+ try {
4
+ const response = await axios.post(`${baseUrl}/auth/collab-token`, {}, {
5
+ headers: {
6
+ Authorization: `Bearer ${apiToken}`,
7
+ "Content-Type": "application/json",
8
+ },
9
+ });
10
+ // console.error('Collab Token Response:', response.data);
11
+ // Response is wrapped in { data: { token: ... } }
12
+ const token = response.data.data?.token || response.data.token;
13
+ if (!token) {
14
+ throw new Error("Collab token not found in API response");
15
+ }
16
+ return token;
17
+ }
18
+ catch (error) {
19
+ if (axios.isAxiosError(error)) {
20
+ throw new Error(`Failed to get collab token: ${error.response?.status} ${error.response?.statusText} - ${JSON.stringify(error.response?.data)}`);
21
+ }
22
+ throw error;
23
+ }
24
+ }
25
+ export async function performLogin(baseUrl, email, password) {
26
+ try {
27
+ const response = await axios.post(`${baseUrl}/auth/login`, {
28
+ email,
29
+ password,
30
+ });
31
+ // Extract token from Set-Cookie header
32
+ const cookies = response.headers["set-cookie"];
33
+ if (!cookies) {
34
+ throw new Error("No Set-Cookie header found in login response");
35
+ }
36
+ const authCookie = cookies.find((c) => c.startsWith("authToken="));
37
+ if (!authCookie) {
38
+ throw new Error("No authToken cookie found in login response");
39
+ }
40
+ const token = authCookie.split(";")[0].split("=")[1];
41
+ if (!token) {
42
+ throw new Error("authToken cookie is empty in login response");
43
+ }
44
+ return token;
45
+ }
46
+ catch (error) {
47
+ if (axios.isAxiosError(error)) {
48
+ const status = error.response?.status;
49
+ throw new Error(`Login failed: ${status} ${error.response?.statusText}`);
50
+ }
51
+ throw error;
52
+ }
53
+ }
@@ -0,0 +1,293 @@
1
+ import { readFile } from "fs/promises";
2
+ import axios from "axios";
3
+ import { CommanderError } from "commander";
4
+ import { DocmostClient } from "../client.js";
5
+ export const EXIT_CODES = {
6
+ AUTH_ERROR: 2,
7
+ NOT_FOUND: 3,
8
+ VALIDATION_ERROR: 4,
9
+ NETWORK_ERROR: 5,
10
+ INTERNAL_ERROR: 1,
11
+ };
12
+ export class CliError extends Error {
13
+ code;
14
+ exitCode;
15
+ details;
16
+ constructor(code, message, details) {
17
+ super(message);
18
+ this.code = code;
19
+ this.exitCode = EXIT_CODES[code];
20
+ this.details = details;
21
+ }
22
+ }
23
+ export function normalizeOutputFormat(value) {
24
+ const normalized = (value || "json").toLowerCase();
25
+ if (normalized === "json" || normalized === "table" || normalized === "text") {
26
+ return normalized;
27
+ }
28
+ throw new CliError("VALIDATION_ERROR", `Unsupported output format '${value}'. Use json, table, or text.`);
29
+ }
30
+ export function resolveOptions(raw, options) {
31
+ const requireAuth = options?.requireAuth ?? true;
32
+ const apiUrl = raw.apiUrl || process.env.DOCMOST_API_URL;
33
+ const token = raw.token || process.env.DOCMOST_TOKEN;
34
+ const email = raw.email || process.env.DOCMOST_EMAIL;
35
+ const password = raw.password || process.env.DOCMOST_PASSWORD;
36
+ if (!apiUrl) {
37
+ throw new CliError("VALIDATION_ERROR", "API URL is required. Use --api-url or DOCMOST_API_URL.");
38
+ }
39
+ if (requireAuth && !token && (!email || !password)) {
40
+ throw new CliError("VALIDATION_ERROR", "Authentication is required: provide --token (or DOCMOST_TOKEN) or both --email/--password (or DOCMOST_EMAIL/DOCMOST_PASSWORD).");
41
+ }
42
+ const outputFormat = normalizeOutputFormat(raw.format);
43
+ const parsedLimit = parseInt(raw.limit || "100", 10);
44
+ if (isNaN(parsedLimit)) {
45
+ throw new CliError("VALIDATION_ERROR", `Invalid --limit value '${raw.limit}'. Must be a number.`);
46
+ }
47
+ const limit = Math.max(1, Math.min(100, parsedLimit));
48
+ const parsedMaxItems = parseInt(raw.maxItems || "0", 10);
49
+ if (isNaN(parsedMaxItems)) {
50
+ throw new CliError("VALIDATION_ERROR", `Invalid --max-items value '${raw.maxItems}'. Must be a number.`);
51
+ }
52
+ const maxItems = parsedMaxItems > 0 ? parsedMaxItems : Infinity;
53
+ const auth = token
54
+ ? { token }
55
+ : (email && password ? { email, password } : {});
56
+ return {
57
+ apiUrl,
58
+ format: outputFormat,
59
+ quiet: raw.quiet ?? false,
60
+ limit,
61
+ maxItems,
62
+ auth,
63
+ };
64
+ }
65
+ export function flattenForTable(row) {
66
+ if (row === null || typeof row !== "object") {
67
+ return { value: row };
68
+ }
69
+ const output = {};
70
+ for (const [key, value] of Object.entries(row)) {
71
+ if (value === null ||
72
+ typeof value === "string" ||
73
+ typeof value === "number" ||
74
+ typeof value === "boolean") {
75
+ output[key] = value;
76
+ continue;
77
+ }
78
+ if (Array.isArray(value)) {
79
+ const allPrimitive = value.every((item) => item === null ||
80
+ typeof item === "string" ||
81
+ typeof item === "number" ||
82
+ typeof item === "boolean");
83
+ output[key] = allPrimitive ? value.join(", ") : JSON.stringify(value);
84
+ continue;
85
+ }
86
+ output[key] = JSON.stringify(value);
87
+ }
88
+ return output;
89
+ }
90
+ export function toTableRows(data) {
91
+ if (Array.isArray(data)) {
92
+ return data.map((row) => flattenForTable(row));
93
+ }
94
+ if (data && typeof data === "object") {
95
+ const value = data;
96
+ if (Array.isArray(value.items)) {
97
+ return value.items.map((row) => flattenForTable(row));
98
+ }
99
+ if (value.data && typeof value.data === "object") {
100
+ const inner = value.data;
101
+ if (Array.isArray(inner.items)) {
102
+ return inner.items.map((row) => flattenForTable(row));
103
+ }
104
+ }
105
+ }
106
+ return [flattenForTable(data)];
107
+ }
108
+ export function printResult(data, opts, options = {}) {
109
+ if (opts.quiet)
110
+ return;
111
+ const output = opts.format;
112
+ if (output === "json") {
113
+ if (Array.isArray(data)) {
114
+ const envelope = {
115
+ ok: true,
116
+ data,
117
+ meta: { count: data.length, hasMore: options.hasMore ?? false },
118
+ };
119
+ console.log(JSON.stringify(envelope, null, 2));
120
+ }
121
+ else {
122
+ const envelope = { ok: true, data };
123
+ console.log(JSON.stringify(envelope, null, 2));
124
+ }
125
+ return;
126
+ }
127
+ if (output === "table") {
128
+ if (!options.allowTable) {
129
+ throw new CliError("VALIDATION_ERROR", "Output format 'table' is not supported for this command.");
130
+ }
131
+ const rows = toTableRows(data);
132
+ if (rows.length === 0) {
133
+ console.log("(empty)");
134
+ return;
135
+ }
136
+ console.table(rows);
137
+ return;
138
+ }
139
+ if (!options.textExtractor) {
140
+ throw new CliError("VALIDATION_ERROR", "Output format 'text' is not supported for this command.");
141
+ }
142
+ const text = options.textExtractor(data);
143
+ if (typeof text !== "string") {
144
+ throw new CliError("VALIDATION_ERROR", "No text content available.");
145
+ }
146
+ process.stdout.write(text);
147
+ if (!text.endsWith("\n")) {
148
+ process.stdout.write("\n");
149
+ }
150
+ }
151
+ export function ensureOutputSupported(opts, options = {}) {
152
+ const output = opts.format;
153
+ if (output === "table" && !options.allowTable) {
154
+ throw new CliError("VALIDATION_ERROR", "Output format 'table' is not supported for this command.");
155
+ }
156
+ if (output === "text" && !options.allowText) {
157
+ throw new CliError("VALIDATION_ERROR", "Output format 'text' is not supported for this command.");
158
+ }
159
+ }
160
+ export function isCommanderHelpExit(error) {
161
+ return (error instanceof CommanderError &&
162
+ (error.code === "commander.helpDisplayed" ||
163
+ error.code === "commander.help" ||
164
+ error.code === "commander.version" ||
165
+ error.message === "(outputHelp)"));
166
+ }
167
+ export function normalizeError(error) {
168
+ if (error instanceof CliError) {
169
+ return error;
170
+ }
171
+ if (error instanceof CommanderError) {
172
+ return new CliError("VALIDATION_ERROR", error.message);
173
+ }
174
+ if (axios.isAxiosError(error)) {
175
+ const status = error.response?.status;
176
+ const responseData = error.response?.data;
177
+ const message = (typeof responseData?.message === "string" && responseData.message) ||
178
+ error.message ||
179
+ "Request failed";
180
+ if (status === 401 || status === 403) {
181
+ return new CliError("AUTH_ERROR", message, responseData);
182
+ }
183
+ if (status === 404) {
184
+ return new CliError("NOT_FOUND", message, responseData);
185
+ }
186
+ if (status === 400 || status === 422) {
187
+ return new CliError("VALIDATION_ERROR", message, responseData);
188
+ }
189
+ if (!status) {
190
+ return new CliError("NETWORK_ERROR", message, {
191
+ code: error.code,
192
+ });
193
+ }
194
+ return new CliError("INTERNAL_ERROR", message, responseData);
195
+ }
196
+ if (error instanceof Error) {
197
+ return new CliError("INTERNAL_ERROR", error.message, error.cause ? { cause: error.cause instanceof Error ? error.cause.message : String(error.cause) } : undefined);
198
+ }
199
+ return new CliError("INTERNAL_ERROR", "Unknown error");
200
+ }
201
+ export function printError(error, output) {
202
+ const envelope = {
203
+ ok: false,
204
+ error: {
205
+ code: error.code,
206
+ message: error.message,
207
+ details: error.details,
208
+ },
209
+ };
210
+ if (output === "json") {
211
+ console.error(JSON.stringify(envelope, null, 2));
212
+ }
213
+ else {
214
+ console.error(`Error [${error.code}]: ${error.message}`);
215
+ if (error.details) {
216
+ console.error(JSON.stringify(error.details, null, 2));
217
+ }
218
+ }
219
+ }
220
+ export function getSafeOutput(program) {
221
+ const opts = program.opts();
222
+ try {
223
+ return normalizeOutputFormat(opts.format);
224
+ }
225
+ catch (err) {
226
+ const msg = err instanceof Error ? err.message : String(err);
227
+ console.error(`Warning: ${msg} Falling back to json.`);
228
+ return "json";
229
+ }
230
+ }
231
+ export async function readStdin() {
232
+ if (process.stdin.isTTY) {
233
+ throw new CliError("VALIDATION_ERROR", "No stdin data. Pipe content or use @file syntax.");
234
+ }
235
+ return new Promise((resolve, reject) => {
236
+ let data = "";
237
+ process.stdin.setEncoding("utf-8");
238
+ process.stdin.on("data", (chunk) => {
239
+ data += chunk;
240
+ });
241
+ process.stdin.on("end", () => {
242
+ if (!data.trim()) {
243
+ reject(new CliError("VALIDATION_ERROR", "Stdin is empty. Provide content via pipe."));
244
+ return;
245
+ }
246
+ resolve(data);
247
+ });
248
+ process.stdin.on("error", reject);
249
+ });
250
+ }
251
+ export async function resolveContentInput(content) {
252
+ if (content === "-") {
253
+ return readStdin();
254
+ }
255
+ if (content.startsWith("@")) {
256
+ const filePath = content.slice(1);
257
+ if (!filePath) {
258
+ throw new CliError("VALIDATION_ERROR", "Invalid content file syntax. Use --content @path/to/file.md");
259
+ }
260
+ try {
261
+ return await readFile(filePath, "utf-8");
262
+ }
263
+ catch (err) {
264
+ const msg = err instanceof Error ? err.message : String(err);
265
+ throw new CliError("VALIDATION_ERROR", `Cannot read file '${filePath}': ${msg}`);
266
+ }
267
+ }
268
+ return content;
269
+ }
270
+ export function parseCommaSeparatedIds(flagName, csv) {
271
+ const ids = csv
272
+ .split(",")
273
+ .map((id) => id.trim())
274
+ .filter(Boolean);
275
+ if (ids.length === 0) {
276
+ throw new CliError("VALIDATION_ERROR", `${flagName} must not be empty`);
277
+ }
278
+ return ids;
279
+ }
280
+ /** @deprecated Use parseCommaSeparatedIds instead */
281
+ export function parsePageIds(csv) {
282
+ return parseCommaSeparatedIds("--page-ids", csv);
283
+ }
284
+ export async function withClient(program, run) {
285
+ const opts = resolveOptions(program.opts());
286
+ const client = new DocmostClient(opts.apiUrl, opts.auth);
287
+ await run(client, opts);
288
+ }
289
+ export async function withPublicClient(program, run) {
290
+ const opts = resolveOptions(program.opts(), { requireAuth: false });
291
+ const client = new DocmostClient(opts.apiUrl, opts.auth);
292
+ await run(client, opts);
293
+ }