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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/build/__tests__/cli-utils.test.js +287 -0
- package/build/__tests__/client-pagination.test.js +103 -0
- package/build/__tests__/discovery.test.js +40 -0
- package/build/__tests__/envelope.test.js +91 -0
- package/build/__tests__/filters.test.js +235 -0
- package/build/__tests__/integration/comment.test.js +48 -0
- package/build/__tests__/integration/discovery.test.js +24 -0
- package/build/__tests__/integration/file.test.js +33 -0
- package/build/__tests__/integration/group.test.js +48 -0
- package/build/__tests__/integration/helpers/global-setup.js +80 -0
- package/build/__tests__/integration/helpers/run-cli.js +163 -0
- package/build/__tests__/integration/invite.test.js +34 -0
- package/build/__tests__/integration/page.test.js +69 -0
- package/build/__tests__/integration/search.test.js +45 -0
- package/build/__tests__/integration/share.test.js +49 -0
- package/build/__tests__/integration/space.test.js +56 -0
- package/build/__tests__/integration/user.test.js +15 -0
- package/build/__tests__/integration/workspace.test.js +42 -0
- package/build/__tests__/markdown-converter.test.js +445 -0
- package/build/__tests__/mcp-tooling.test.js +58 -0
- package/build/__tests__/page-mentions.test.js +65 -0
- package/build/__tests__/tiptap-extensions.test.js +135 -0
- package/build/client.js +715 -0
- package/build/commands/comment.js +54 -0
- package/build/commands/discovery.js +21 -0
- package/build/commands/file.js +36 -0
- package/build/commands/group.js +91 -0
- package/build/commands/invite.js +67 -0
- package/build/commands/page.js +227 -0
- package/build/commands/search.js +33 -0
- package/build/commands/share.js +65 -0
- package/build/commands/space.js +154 -0
- package/build/commands/user.js +38 -0
- package/build/commands/workspace.js +77 -0
- package/build/index.js +19 -0
- package/build/lib/auth-utils.js +53 -0
- package/build/lib/cli-utils.js +293 -0
- package/build/lib/collaboration.js +126 -0
- package/build/lib/filters.js +137 -0
- package/build/lib/markdown-converter.js +187 -0
- package/build/lib/mcp-tooling.js +295 -0
- package/build/lib/page-mentions.js +162 -0
- package/build/lib/tiptap-extensions.js +86 -0
- package/build/mcp.js +186 -0
- package/build/program.js +60 -0
- package/package.json +64 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { filterWorkspace, filterSpace, filterGroup, filterPage, filterSearchResult, filterHistoryEntry, filterMember, filterInvite, filterUser, filterComment, filterShare, filterHistoryDetail, } from "../lib/filters.js";
|
|
3
|
+
const extra = { __v: 9, internalField: "secret", _meta: { x: 1 } };
|
|
4
|
+
describe("filterWorkspace", () => {
|
|
5
|
+
it("keeps expected fields and strips extras", () => {
|
|
6
|
+
const input = {
|
|
7
|
+
id: "w1", name: "My WS", description: "desc",
|
|
8
|
+
defaultSpaceId: "s1", createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
9
|
+
...extra,
|
|
10
|
+
};
|
|
11
|
+
const result = filterWorkspace(input);
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
id: "w1", name: "My WS", description: "desc",
|
|
14
|
+
defaultSpaceId: "s1", createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
15
|
+
});
|
|
16
|
+
expect(result).not.toHaveProperty("__v");
|
|
17
|
+
expect(result).not.toHaveProperty("internalField");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("filterSpace", () => {
|
|
21
|
+
it("keeps expected fields and strips extras", () => {
|
|
22
|
+
const input = {
|
|
23
|
+
id: "s1", name: "Space", description: "d", slug: "space",
|
|
24
|
+
visibility: "public", createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
25
|
+
...extra,
|
|
26
|
+
};
|
|
27
|
+
const result = filterSpace(input);
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
id: "s1", name: "Space", description: "d", slug: "space",
|
|
30
|
+
visibility: "public", createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
31
|
+
});
|
|
32
|
+
expect(result).not.toHaveProperty("__v");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("filterGroup", () => {
|
|
36
|
+
it("keeps expected fields and strips extras", () => {
|
|
37
|
+
const input = {
|
|
38
|
+
id: "g1", name: "Group", description: "gd", workspaceId: "w1",
|
|
39
|
+
createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
40
|
+
...extra,
|
|
41
|
+
};
|
|
42
|
+
const result = filterGroup(input);
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
id: "g1", name: "Group", description: "gd", workspaceId: "w1",
|
|
45
|
+
createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
46
|
+
});
|
|
47
|
+
expect(result).not.toHaveProperty("_meta");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("filterPage", () => {
|
|
51
|
+
const page = {
|
|
52
|
+
id: "p1", title: "Page", parentPageId: null, spaceId: "s1",
|
|
53
|
+
isLocked: false, createdAt: "2024-01-01", updatedAt: "2024-02-01", deletedAt: null,
|
|
54
|
+
...extra,
|
|
55
|
+
};
|
|
56
|
+
it("keeps page fields and strips extras", () => {
|
|
57
|
+
const result = filterPage(page);
|
|
58
|
+
expect(result).not.toHaveProperty("__v");
|
|
59
|
+
expect(result.id).toBe("p1");
|
|
60
|
+
});
|
|
61
|
+
it("includes content when string provided", () => {
|
|
62
|
+
const result = filterPage(page, "# Hello");
|
|
63
|
+
expect(result.content).toBe("# Hello");
|
|
64
|
+
});
|
|
65
|
+
it("includes content when empty string provided", () => {
|
|
66
|
+
const result = filterPage(page, "");
|
|
67
|
+
expect(result.content).toBe("");
|
|
68
|
+
});
|
|
69
|
+
it("excludes content when undefined", () => {
|
|
70
|
+
const result = filterPage(page);
|
|
71
|
+
expect(result).not.toHaveProperty("content");
|
|
72
|
+
});
|
|
73
|
+
it("includes subpages when non-empty", () => {
|
|
74
|
+
const subs = [{ id: "sp1", title: "Sub1", extra: "x" }, { id: "sp2", title: "Sub2" }];
|
|
75
|
+
const result = filterPage(page, undefined, subs);
|
|
76
|
+
expect(result.subpages).toEqual([{ id: "sp1", title: "Sub1" }, { id: "sp2", title: "Sub2" }]);
|
|
77
|
+
});
|
|
78
|
+
it("excludes subpages when empty array", () => {
|
|
79
|
+
const result = filterPage(page, undefined, []);
|
|
80
|
+
expect(result).not.toHaveProperty("subpages");
|
|
81
|
+
});
|
|
82
|
+
it("excludes subpages when undefined", () => {
|
|
83
|
+
const result = filterPage(page);
|
|
84
|
+
expect(result).not.toHaveProperty("subpages");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("filterSearchResult", () => {
|
|
88
|
+
it("maps space.id→spaceId and space.name→spaceName, keeps rank and highlight", () => {
|
|
89
|
+
const input = {
|
|
90
|
+
id: "p1", title: "Found", parentPageId: null,
|
|
91
|
+
createdAt: "2024-01-01", updatedAt: "2024-02-01",
|
|
92
|
+
rank: 0.95, highlight: "<b>found</b>",
|
|
93
|
+
space: { id: "s1", name: "Main", slug: "main" },
|
|
94
|
+
...extra,
|
|
95
|
+
};
|
|
96
|
+
const result = filterSearchResult(input);
|
|
97
|
+
expect(result.spaceId).toBe("s1");
|
|
98
|
+
expect(result.spaceName).toBe("Main");
|
|
99
|
+
expect(result.rank).toBe(0.95);
|
|
100
|
+
expect(result.highlight).toBe("<b>found</b>");
|
|
101
|
+
expect(result).not.toHaveProperty("space");
|
|
102
|
+
expect(result).not.toHaveProperty("__v");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe("filterHistoryEntry", () => {
|
|
106
|
+
it("maps lastUpdatedBy.name", () => {
|
|
107
|
+
const input = {
|
|
108
|
+
id: "h1", pageId: "p1", title: "V2", version: 2,
|
|
109
|
+
createdAt: "2024-01-01",
|
|
110
|
+
lastUpdatedBy: { id: "u1", name: "Alice" },
|
|
111
|
+
lastUpdatedById: "u1",
|
|
112
|
+
contributors: [{ name: "Alice" }, { name: "Bob" }],
|
|
113
|
+
...extra,
|
|
114
|
+
};
|
|
115
|
+
const result = filterHistoryEntry(input);
|
|
116
|
+
expect(result.lastUpdatedBy).toBe("Alice");
|
|
117
|
+
expect(result.contributors).toEqual(["Alice", "Bob"]);
|
|
118
|
+
expect(result).not.toHaveProperty("__v");
|
|
119
|
+
});
|
|
120
|
+
it("falls back to lastUpdatedById when lastUpdatedBy is missing", () => {
|
|
121
|
+
const input = {
|
|
122
|
+
id: "h2", pageId: "p1", title: "V1", version: 1,
|
|
123
|
+
createdAt: "2024-01-01",
|
|
124
|
+
lastUpdatedById: "u99",
|
|
125
|
+
contributors: [],
|
|
126
|
+
};
|
|
127
|
+
const result = filterHistoryEntry(input);
|
|
128
|
+
expect(result.lastUpdatedBy).toBe("u99");
|
|
129
|
+
});
|
|
130
|
+
it("returns empty array when contributors missing", () => {
|
|
131
|
+
const input = {
|
|
132
|
+
id: "h3", pageId: "p1", title: "V0", version: 0,
|
|
133
|
+
createdAt: "2024-01-01",
|
|
134
|
+
lastUpdatedById: "u1",
|
|
135
|
+
};
|
|
136
|
+
const result = filterHistoryEntry(input);
|
|
137
|
+
expect(result.contributors).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe("filterMember", () => {
|
|
141
|
+
it("keeps expected fields and strips extras", () => {
|
|
142
|
+
const input = {
|
|
143
|
+
id: "m1", name: "Alice", email: "a@b.com", role: "admin",
|
|
144
|
+
createdAt: "2024-01-01", ...extra,
|
|
145
|
+
};
|
|
146
|
+
const result = filterMember(input);
|
|
147
|
+
expect(result).toEqual({
|
|
148
|
+
id: "m1", name: "Alice", email: "a@b.com", role: "admin", createdAt: "2024-01-01",
|
|
149
|
+
});
|
|
150
|
+
expect(result).not.toHaveProperty("internalField");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe("filterInvite", () => {
|
|
154
|
+
it("keeps expected fields and strips extras", () => {
|
|
155
|
+
const input = {
|
|
156
|
+
id: "i1", email: "x@y.com", role: "editor", status: "pending",
|
|
157
|
+
invitedById: "u1", createdAt: "2024-01-01", ...extra,
|
|
158
|
+
};
|
|
159
|
+
const result = filterInvite(input);
|
|
160
|
+
expect(result).toEqual({
|
|
161
|
+
id: "i1", email: "x@y.com", role: "editor", status: "pending",
|
|
162
|
+
invitedById: "u1", createdAt: "2024-01-01",
|
|
163
|
+
});
|
|
164
|
+
expect(result).not.toHaveProperty("__v");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe("filterUser", () => {
|
|
168
|
+
it("keeps expected fields and strips extras", () => {
|
|
169
|
+
const input = {
|
|
170
|
+
id: "u1", name: "Bob", email: "b@b.com", role: "owner",
|
|
171
|
+
locale: "en", createdAt: "2024-01-01", ...extra,
|
|
172
|
+
};
|
|
173
|
+
const result = filterUser(input);
|
|
174
|
+
expect(result).toEqual({
|
|
175
|
+
id: "u1", name: "Bob", email: "b@b.com", role: "owner",
|
|
176
|
+
locale: "en", createdAt: "2024-01-01",
|
|
177
|
+
});
|
|
178
|
+
expect(result).not.toHaveProperty("_meta");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe("filterComment", () => {
|
|
182
|
+
it("keeps all comment fields and strips extras", () => {
|
|
183
|
+
const input = {
|
|
184
|
+
id: "c1", pageId: "p1", content: "Nice!", selection: "text",
|
|
185
|
+
parentCommentId: null, creatorId: "u1",
|
|
186
|
+
createdAt: "2024-01-01", updatedAt: "2024-02-01",
|
|
187
|
+
...extra,
|
|
188
|
+
};
|
|
189
|
+
const result = filterComment(input);
|
|
190
|
+
expect(result).toEqual({
|
|
191
|
+
id: "c1", pageId: "p1", content: "Nice!", selection: "text",
|
|
192
|
+
parentCommentId: null, creatorId: "u1",
|
|
193
|
+
createdAt: "2024-01-01", updatedAt: "2024-02-01",
|
|
194
|
+
});
|
|
195
|
+
expect(result).not.toHaveProperty("__v");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
describe("filterShare", () => {
|
|
199
|
+
it("keeps expected fields and strips extras", () => {
|
|
200
|
+
const input = {
|
|
201
|
+
id: "sh1", pageId: "p1", includeSubPages: true,
|
|
202
|
+
searchIndexing: false, createdAt: "2024-01-01", ...extra,
|
|
203
|
+
};
|
|
204
|
+
const result = filterShare(input);
|
|
205
|
+
expect(result).toEqual({
|
|
206
|
+
id: "sh1", pageId: "p1", includeSubPages: true,
|
|
207
|
+
searchIndexing: false, createdAt: "2024-01-01",
|
|
208
|
+
});
|
|
209
|
+
expect(result).not.toHaveProperty("internalField");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe("filterHistoryDetail", () => {
|
|
213
|
+
const entry = {
|
|
214
|
+
id: "h1", pageId: "p1", title: "V3", version: 3,
|
|
215
|
+
createdAt: "2024-01-01",
|
|
216
|
+
lastUpdatedBy: { name: "Carol" },
|
|
217
|
+
contributors: [{ name: "Carol" }],
|
|
218
|
+
...extra,
|
|
219
|
+
};
|
|
220
|
+
it("extends filterHistoryEntry with content", () => {
|
|
221
|
+
const result = filterHistoryDetail(entry, "# Content");
|
|
222
|
+
expect(result.content).toBe("# Content");
|
|
223
|
+
expect(result.id).toBe("h1");
|
|
224
|
+
expect(result.lastUpdatedBy).toBe("Carol");
|
|
225
|
+
expect(result).not.toHaveProperty("__v");
|
|
226
|
+
});
|
|
227
|
+
it("includes content when empty string", () => {
|
|
228
|
+
const result = filterHistoryDetail(entry, "");
|
|
229
|
+
expect(result.content).toBe("");
|
|
230
|
+
});
|
|
231
|
+
it("excludes content when undefined", () => {
|
|
232
|
+
const result = filterHistoryDetail(entry);
|
|
233
|
+
expect(result).not.toHaveProperty("content");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { runCli, parseEnvelope, testEnv } from "./helpers/run-cli.js";
|
|
3
|
+
const env = testEnv();
|
|
4
|
+
describe("comment commands", () => {
|
|
5
|
+
let spaceId;
|
|
6
|
+
let pageId;
|
|
7
|
+
let commentId;
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
const spaceResult = await runCli(["space-create", "--name", `commentspace${Date.now()}`, "--slug", `cs${Date.now()}`], env);
|
|
10
|
+
spaceId = parseEnvelope(spaceResult).data.id;
|
|
11
|
+
const pageResult = await runCli(["page-create", "--space-id", spaceId, "--title", "Comment Test Page"], env);
|
|
12
|
+
pageId = parseEnvelope(pageResult).data.id;
|
|
13
|
+
});
|
|
14
|
+
it("comment-create creates a comment", async () => {
|
|
15
|
+
const result = await runCli(["comment-create", "--page-id", pageId, "--content", "Test comment"], env);
|
|
16
|
+
expect(result.exitCode).toBe(0);
|
|
17
|
+
const envelope = parseEnvelope(result);
|
|
18
|
+
expect(envelope.ok).toBe(true);
|
|
19
|
+
// createComment returns raw response.data; actual comment may be nested in .data
|
|
20
|
+
const comment = envelope.data.data ?? envelope.data;
|
|
21
|
+
expect(comment).toHaveProperty("id");
|
|
22
|
+
commentId = comment.id;
|
|
23
|
+
});
|
|
24
|
+
it("comment-list returns comments for page", async () => {
|
|
25
|
+
const result = await runCli(["comment-list", "--page-id", pageId], env);
|
|
26
|
+
expect(result.exitCode).toBe(0);
|
|
27
|
+
const envelope = parseEnvelope(result);
|
|
28
|
+
expect(envelope.ok).toBe(true);
|
|
29
|
+
expect(Array.isArray(envelope.data)).toBe(true);
|
|
30
|
+
expect(envelope.data.length).toBeGreaterThanOrEqual(1);
|
|
31
|
+
});
|
|
32
|
+
it("comment-info returns comment details", async () => {
|
|
33
|
+
const result = await runCli(["comment-info", "--comment-id", commentId], env);
|
|
34
|
+
expect(result.exitCode).toBe(0);
|
|
35
|
+
const envelope = parseEnvelope(result);
|
|
36
|
+
expect(envelope.ok).toBe(true);
|
|
37
|
+
expect(envelope.data.id).toBe(commentId);
|
|
38
|
+
});
|
|
39
|
+
it("comment-delete deletes the comment", async () => {
|
|
40
|
+
const result = await runCli(["comment-delete", "--comment-id", commentId], env);
|
|
41
|
+
expect(result.exitCode).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
if (spaceId) {
|
|
45
|
+
await runCli(["space-delete", "--space-id", spaceId], env);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { runCli, parseEnvelope } from "./helpers/run-cli.js";
|
|
3
|
+
describe("commands discovery", () => {
|
|
4
|
+
it("returns envelope with all commands", async () => {
|
|
5
|
+
// Discovery doesn't need auth
|
|
6
|
+
const result = await runCli(["commands"], {});
|
|
7
|
+
expect(result.exitCode).toBe(0);
|
|
8
|
+
const envelope = parseEnvelope(result);
|
|
9
|
+
expect(envelope.ok).toBe(true);
|
|
10
|
+
expect(Array.isArray(envelope.data)).toBe(true);
|
|
11
|
+
expect(envelope.data.length).toBeGreaterThan(50);
|
|
12
|
+
expect(envelope.meta).toEqual({ count: envelope.data.length, hasMore: false });
|
|
13
|
+
});
|
|
14
|
+
it("each command has name, description, options", async () => {
|
|
15
|
+
const result = await runCli(["commands"], {});
|
|
16
|
+
const envelope = parseEnvelope(result);
|
|
17
|
+
for (const cmd of envelope.data) {
|
|
18
|
+
expect(cmd).toHaveProperty("name");
|
|
19
|
+
expect(cmd).toHaveProperty("description");
|
|
20
|
+
expect(cmd).toHaveProperty("options");
|
|
21
|
+
expect(Array.isArray(cmd.options)).toBe(true);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { writeFileSync, mkdtempSync, rmSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { runCli, parseEnvelope, testEnv } from "./helpers/run-cli.js";
|
|
6
|
+
const env = testEnv();
|
|
7
|
+
describe("file commands", () => {
|
|
8
|
+
let spaceId;
|
|
9
|
+
let pageId;
|
|
10
|
+
let tmpDir;
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
const spaceResult = await runCli(["space-create", "--name", `filespace${Date.now()}`, "--slug", `fs${Date.now()}`], env);
|
|
13
|
+
spaceId = parseEnvelope(spaceResult).data.id;
|
|
14
|
+
const pageResult = await runCli(["page-create", "--space-id", spaceId, "--title", "File Test Page"], env);
|
|
15
|
+
pageId = parseEnvelope(pageResult).data.id;
|
|
16
|
+
// Create a temp file for upload
|
|
17
|
+
tmpDir = mkdtempSync(join(tmpdir(), "docmost-test-"));
|
|
18
|
+
writeFileSync(join(tmpDir, "test.txt"), "Hello from integration test");
|
|
19
|
+
});
|
|
20
|
+
it("file-upload uploads a file", async () => {
|
|
21
|
+
const result = await runCli(["file-upload", "--page-id", pageId, "--file", join(tmpDir, "test.txt")], env);
|
|
22
|
+
expect(result.exitCode).toBe(0);
|
|
23
|
+
const envelope = parseEnvelope(result);
|
|
24
|
+
expect(envelope.ok).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
if (tmpDir)
|
|
28
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
if (spaceId) {
|
|
30
|
+
await runCli(["space-delete", "--space-id", spaceId], env);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { runCli, parseEnvelope, testEnv } from "./helpers/run-cli.js";
|
|
3
|
+
const env = testEnv();
|
|
4
|
+
describe("group commands", () => {
|
|
5
|
+
let groupId;
|
|
6
|
+
const groupName = `testgroup${Date.now()}`;
|
|
7
|
+
it("group-create creates a group", async () => {
|
|
8
|
+
const result = await runCli(["group-create", "--name", groupName], env);
|
|
9
|
+
expect(result.exitCode).toBe(0);
|
|
10
|
+
const envelope = parseEnvelope(result);
|
|
11
|
+
expect(envelope.ok).toBe(true);
|
|
12
|
+
expect(envelope.data).toHaveProperty("id");
|
|
13
|
+
groupId = envelope.data.id;
|
|
14
|
+
});
|
|
15
|
+
it("group-list includes created group", async () => {
|
|
16
|
+
const result = await runCli(["group-list"], env);
|
|
17
|
+
expect(result.exitCode).toBe(0);
|
|
18
|
+
const envelope = parseEnvelope(result);
|
|
19
|
+
expect(envelope.ok).toBe(true);
|
|
20
|
+
const names = envelope.data.map((g) => g.name);
|
|
21
|
+
expect(names).toContain(groupName);
|
|
22
|
+
});
|
|
23
|
+
it("group-info returns group details", async () => {
|
|
24
|
+
const result = await runCli(["group-info", "--group-id", groupId], env);
|
|
25
|
+
expect(result.exitCode).toBe(0);
|
|
26
|
+
const envelope = parseEnvelope(result);
|
|
27
|
+
expect(envelope.ok).toBe(true);
|
|
28
|
+
expect(envelope.data.id).toBe(groupId);
|
|
29
|
+
});
|
|
30
|
+
it("group-update changes name", async () => {
|
|
31
|
+
const newName = `${groupName}-updated`;
|
|
32
|
+
const result = await runCli(["group-update", "--group-id", groupId, "--name", newName], env);
|
|
33
|
+
expect(result.exitCode).toBe(0);
|
|
34
|
+
const envelope = parseEnvelope(result);
|
|
35
|
+
expect(envelope.ok).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it("group-member-list returns members", async () => {
|
|
38
|
+
const result = await runCli(["group-member-list", "--group-id", groupId], env);
|
|
39
|
+
expect(result.exitCode).toBe(0);
|
|
40
|
+
const envelope = parseEnvelope(result);
|
|
41
|
+
expect(envelope.ok).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
if (groupId) {
|
|
45
|
+
await runCli(["group-delete", "--group-id", groupId], env);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { writeFileSync, rmSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
/** DOCMOST_TEST_URL should include /api suffix (e.g. http://localhost:4010/api)
|
|
6
|
+
* because that's what the CLI client expects. We strip it for direct API calls. */
|
|
7
|
+
const API_URL = process.env.DOCMOST_TEST_URL || "http://localhost:4010/api";
|
|
8
|
+
const ROOT_URL = API_URL.replace(/\/api\/?$/, "");
|
|
9
|
+
const EMAIL = process.env.DOCMOST_TEST_EMAIL || "test@example.com";
|
|
10
|
+
const PASSWORD = process.env.DOCMOST_TEST_PASSWORD || "TestPassword123!";
|
|
11
|
+
const WORKSPACE_NAME = "CLI Integration Tests";
|
|
12
|
+
/** Shared file path for token — globalSetup runs in a separate process,
|
|
13
|
+
* so process.env changes are NOT visible in test workers.
|
|
14
|
+
* We write the token to a file and read it in testEnv(). */
|
|
15
|
+
export const TOKEN_FILE = join(tmpdir(), "docmost-test-token");
|
|
16
|
+
export async function setup() {
|
|
17
|
+
// Check if Docmost is reachable
|
|
18
|
+
try {
|
|
19
|
+
const health = await axios.get(`${ROOT_URL}/api/health`);
|
|
20
|
+
if (health.status !== 200) {
|
|
21
|
+
throw new Error(`Docmost health check failed: ${health.status}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
throw new Error(`Cannot reach Docmost at ${ROOT_URL}. Is docker-compose running?\n` +
|
|
26
|
+
`Run: docker compose -f docker-compose.test.yml up -d\n` +
|
|
27
|
+
`Original error: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
28
|
+
}
|
|
29
|
+
// Try to setup workspace (first-run) or skip if already done
|
|
30
|
+
try {
|
|
31
|
+
await axios.post(`${ROOT_URL}/api/auth/setup`, {
|
|
32
|
+
workspaceName: WORKSPACE_NAME,
|
|
33
|
+
name: "Test User",
|
|
34
|
+
email: EMAIL,
|
|
35
|
+
password: PASSWORD,
|
|
36
|
+
});
|
|
37
|
+
console.log("[global-setup] Created workspace and admin user");
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const status = axios.isAxiosError(err) ? err.response?.status : null;
|
|
41
|
+
if (status === 400 || status === 403) {
|
|
42
|
+
const msg = axios.isAxiosError(err) ? err.response?.data?.message : "";
|
|
43
|
+
console.log(`[global-setup] Setup skipped (${status}): ${msg || "workspace likely already exists"}`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Login to get token — Docmost returns token in Set-Cookie header, not body
|
|
50
|
+
let loginResp;
|
|
51
|
+
try {
|
|
52
|
+
loginResp = await axios.post(`${ROOT_URL}/api/auth/login`, {
|
|
53
|
+
email: EMAIL,
|
|
54
|
+
password: PASSWORD,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new Error(`Login failed for ${EMAIL} at ${ROOT_URL}: ${err instanceof Error ? err.message : err}`, { cause: err });
|
|
59
|
+
}
|
|
60
|
+
const cookies = loginResp.headers["set-cookie"];
|
|
61
|
+
const authCookie = cookies?.find((c) => c.startsWith("authToken="));
|
|
62
|
+
const token = authCookie?.split(";")[0].split("=")[1];
|
|
63
|
+
if (!token) {
|
|
64
|
+
throw new Error("Failed to obtain auth token from login response. " +
|
|
65
|
+
`Cookies: ${JSON.stringify(cookies)}`);
|
|
66
|
+
}
|
|
67
|
+
// Write token to shared file so test workers can read it
|
|
68
|
+
writeFileSync(TOKEN_FILE, token, "utf-8");
|
|
69
|
+
console.log("[global-setup] Obtained auth token, wrote to", TOKEN_FILE);
|
|
70
|
+
}
|
|
71
|
+
export async function teardown() {
|
|
72
|
+
try {
|
|
73
|
+
rmSync(TOKEN_FILE);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err?.code !== "ENOENT") {
|
|
77
|
+
console.warn("[global-setup] Failed to remove token file:", err.message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { normalizeError, printError, isCommanderHelpExit, } from "../../../lib/cli-utils.js";
|
|
6
|
+
import { register as registerPageCommands } from "../../../commands/page.js";
|
|
7
|
+
import { register as registerWorkspaceCommands } from "../../../commands/workspace.js";
|
|
8
|
+
import { register as registerInviteCommands } from "../../../commands/invite.js";
|
|
9
|
+
import { register as registerUserCommands } from "../../../commands/user.js";
|
|
10
|
+
import { register as registerSpaceCommands } from "../../../commands/space.js";
|
|
11
|
+
import { register as registerGroupCommands } from "../../../commands/group.js";
|
|
12
|
+
import { register as registerCommentCommands } from "../../../commands/comment.js";
|
|
13
|
+
import { register as registerShareCommands } from "../../../commands/share.js";
|
|
14
|
+
import { register as registerFileCommands } from "../../../commands/file.js";
|
|
15
|
+
import { register as registerSearchCommands } from "../../../commands/search.js";
|
|
16
|
+
import { register as registerDiscoveryCommands } from "../../../commands/discovery.js";
|
|
17
|
+
function buildProgram(stdout, stderr) {
|
|
18
|
+
const program = new Command()
|
|
19
|
+
.name("docmost")
|
|
20
|
+
.exitOverride()
|
|
21
|
+
.configureOutput({
|
|
22
|
+
writeOut: (str) => stdout.push(str),
|
|
23
|
+
writeErr: (str) => stderr.push(str),
|
|
24
|
+
});
|
|
25
|
+
// Global options matching src/index.ts
|
|
26
|
+
program
|
|
27
|
+
.option("-u, --api-url <url>", "Docmost API URL")
|
|
28
|
+
.option("-e, --email <email>", "Docmost account email")
|
|
29
|
+
.option("--password <password>", "Docmost account password")
|
|
30
|
+
.option("-t, --token <token>", "Docmost API auth token")
|
|
31
|
+
.option("-f, --format <format>", "Output format: json | table | text", "json")
|
|
32
|
+
.option("-q, --quiet", "Suppress output, exit code only")
|
|
33
|
+
.option("--limit <n>", "Items per API page (1-100)")
|
|
34
|
+
.option("--max-items <n>", "Stop after N total items");
|
|
35
|
+
registerPageCommands(program);
|
|
36
|
+
registerWorkspaceCommands(program);
|
|
37
|
+
registerInviteCommands(program);
|
|
38
|
+
registerUserCommands(program);
|
|
39
|
+
registerSpaceCommands(program);
|
|
40
|
+
registerGroupCommands(program);
|
|
41
|
+
registerCommentCommands(program);
|
|
42
|
+
registerShareCommands(program);
|
|
43
|
+
registerFileCommands(program);
|
|
44
|
+
registerSearchCommands(program);
|
|
45
|
+
registerDiscoveryCommands(program);
|
|
46
|
+
return program;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run a CLI command programmatically and capture output.
|
|
50
|
+
* NOTE: Mutates process.env and monkey-patches console/stdout — not concurrency-safe.
|
|
51
|
+
* Requires vitest config: fileParallelism: false, sequence.concurrent: false.
|
|
52
|
+
*
|
|
53
|
+
* @param args - CLI arguments (e.g., ["page-list", "--space-id", "abc"])
|
|
54
|
+
* @param env - Extra env vars for this invocation
|
|
55
|
+
*/
|
|
56
|
+
export async function runCli(args, env = {}) {
|
|
57
|
+
const stdout = [];
|
|
58
|
+
const stderr = [];
|
|
59
|
+
// Save and override env
|
|
60
|
+
const savedEnv = {};
|
|
61
|
+
for (const [k, v] of Object.entries(env)) {
|
|
62
|
+
savedEnv[k] = process.env[k];
|
|
63
|
+
process.env[k] = v;
|
|
64
|
+
}
|
|
65
|
+
// Intercept console
|
|
66
|
+
const origLog = console.log;
|
|
67
|
+
const origError = console.error;
|
|
68
|
+
const origTable = console.table;
|
|
69
|
+
const origStdoutWrite = process.stdout.write;
|
|
70
|
+
const origStderrWrite = process.stderr.write;
|
|
71
|
+
console.log = (...a) => stdout.push(a.map(String).join(" "));
|
|
72
|
+
console.error = (...a) => stderr.push(a.map(String).join(" "));
|
|
73
|
+
console.table = (...a) => stdout.push(JSON.stringify(a));
|
|
74
|
+
process.stdout.write = ((chunk) => {
|
|
75
|
+
stdout.push(chunk);
|
|
76
|
+
return true;
|
|
77
|
+
});
|
|
78
|
+
process.stderr.write = ((chunk) => {
|
|
79
|
+
stderr.push(chunk);
|
|
80
|
+
return true;
|
|
81
|
+
});
|
|
82
|
+
let exitCode = 0;
|
|
83
|
+
try {
|
|
84
|
+
const program = buildProgram(stdout, stderr);
|
|
85
|
+
await program.parseAsync(["node", "docmost", ...args]);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (isCommanderHelpExit(error)) {
|
|
89
|
+
exitCode = 0;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Simplified error handling (always JSON — tests use default format)
|
|
93
|
+
const normalized = normalizeError(error);
|
|
94
|
+
printError(normalized, "json");
|
|
95
|
+
exitCode = normalized.exitCode;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
// Restore
|
|
100
|
+
console.log = origLog;
|
|
101
|
+
console.error = origError;
|
|
102
|
+
console.table = origTable;
|
|
103
|
+
process.stdout.write = origStdoutWrite;
|
|
104
|
+
process.stderr.write = origStderrWrite;
|
|
105
|
+
for (const [k, v] of Object.entries(savedEnv)) {
|
|
106
|
+
if (v === undefined)
|
|
107
|
+
delete process.env[k];
|
|
108
|
+
else
|
|
109
|
+
process.env[k] = v;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
stdout: stdout.join("\n"),
|
|
114
|
+
stderr: stderr.join("\n"),
|
|
115
|
+
exitCode,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Parse JSON envelope from CLI output (tries stdout first, then stderr) */
|
|
119
|
+
export function parseEnvelope(result) {
|
|
120
|
+
const source = result.stdout.trim() || result.stderr.trim();
|
|
121
|
+
if (!source) {
|
|
122
|
+
throw new Error(`Empty CLI output (exitCode=${result.exitCode}). ` +
|
|
123
|
+
`stdout=${JSON.stringify(result.stdout)}, stderr=${JSON.stringify(result.stderr)}`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(source);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
throw new Error(`Failed to parse CLI output as JSON (exitCode=${result.exitCode}): ${source.slice(0, 500)}`, { cause: err });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Get test server URL from env */
|
|
133
|
+
export function testUrl() {
|
|
134
|
+
return process.env.DOCMOST_TEST_URL || "http://localhost:4010/api";
|
|
135
|
+
}
|
|
136
|
+
/** Must match TOKEN_FILE in global-setup.ts (duplicated to avoid ESM import issues) */
|
|
137
|
+
const TOKEN_FILE = join(tmpdir(), "docmost-test-token");
|
|
138
|
+
/** Read token written by global-setup (runs in a separate process) */
|
|
139
|
+
function readTestToken() {
|
|
140
|
+
try {
|
|
141
|
+
return readFileSync(TOKEN_FILE, "utf-8").trim();
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (err?.code !== "ENOENT") {
|
|
145
|
+
throw new Error(`Failed to read test token from ${TOKEN_FILE}: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
const envToken = process.env.DOCMOST_TEST_TOKEN;
|
|
148
|
+
if (!envToken) {
|
|
149
|
+
throw new Error("No test token available. Global setup may have failed. " +
|
|
150
|
+
`Check that global-setup.ts ran successfully or set DOCMOST_TEST_TOKEN.`);
|
|
151
|
+
}
|
|
152
|
+
return envToken;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Get test credentials env vars for runCli */
|
|
156
|
+
export function testEnv() {
|
|
157
|
+
return {
|
|
158
|
+
DOCMOST_API_URL: testUrl(),
|
|
159
|
+
DOCMOST_TOKEN: readTestToken(),
|
|
160
|
+
DOCMOST_EMAIL: process.env.DOCMOST_TEST_EMAIL || "",
|
|
161
|
+
DOCMOST_PASSWORD: process.env.DOCMOST_TEST_PASSWORD || "",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { runCli, parseEnvelope, testEnv } from "./helpers/run-cli.js";
|
|
3
|
+
const env = testEnv();
|
|
4
|
+
describe("invite commands", () => {
|
|
5
|
+
let inviteId;
|
|
6
|
+
const inviteEmail = `invite-${Date.now()}@example.com`;
|
|
7
|
+
it("invite-create sends an invite", async () => {
|
|
8
|
+
const result = await runCli(["invite-create", "--emails", inviteEmail, "--role", "member"], env);
|
|
9
|
+
expect(result.exitCode).toBe(0);
|
|
10
|
+
const envelope = parseEnvelope(result);
|
|
11
|
+
expect(envelope.ok).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("invite-list includes the invite", async () => {
|
|
14
|
+
const result = await runCli(["invite-list"], env);
|
|
15
|
+
expect(result.exitCode).toBe(0);
|
|
16
|
+
const envelope = parseEnvelope(result);
|
|
17
|
+
expect(envelope.ok).toBe(true);
|
|
18
|
+
expect(Array.isArray(envelope.data)).toBe(true);
|
|
19
|
+
const invite = envelope.data.find((i) => i.email === inviteEmail);
|
|
20
|
+
expect(invite).toBeDefined();
|
|
21
|
+
inviteId = invite.id;
|
|
22
|
+
});
|
|
23
|
+
it("invite-info returns invite details", async () => {
|
|
24
|
+
const result = await runCli(["invite-info", "--invitation-id", inviteId], env);
|
|
25
|
+
expect(result.exitCode).toBe(0);
|
|
26
|
+
const envelope = parseEnvelope(result);
|
|
27
|
+
expect(envelope.ok).toBe(true);
|
|
28
|
+
expect(envelope.data.email).toBe(inviteEmail);
|
|
29
|
+
});
|
|
30
|
+
it("invite-revoke revokes the invite", async () => {
|
|
31
|
+
const result = await runCli(["invite-revoke", "--invitation-id", inviteId], env);
|
|
32
|
+
expect(result.exitCode).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
});
|