@easycustomerfeedback/cli 0.1.0 → 0.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/README.md +82 -21
- package/dist/index.js +340 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @easycustomerfeedback/cli
|
|
2
2
|
|
|
3
|
-
`ecf` is the command-line interface for [EasyCustomerFeedback](https://easycustomerfeedback.com). Triage feedback submissions,
|
|
3
|
+
`ecf` is the command-line interface for [EasyCustomerFeedback](https://easycustomerfeedback.com). Triage feedback submissions, manage knowledge base articles, and leave internal comments from your terminal.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -21,41 +21,42 @@ Requires Node.js 18 or newer.
|
|
|
21
21
|
1. In the EasyCustomerFeedback dashboard, open **Integrations → Personal API tokens** and create a token. Copy it — tokens are only shown once.
|
|
22
22
|
2. Log in:
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
```sh
|
|
25
|
+
ecf login
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
You'll be prompted for the server URL (defaults to `https://easycustomerfeedback.com`) and the token. Credentials are saved to `~/.config/ecf/config.json`.
|
|
27
29
|
|
|
28
|
-
You'll be prompted for the server URL (defaults to `https://easycustomerfeedback.com`) and the token. Credentials are saved to `~/.config/ecf/config.json`.
|
|
29
30
|
3. If you belong to multiple workspaces, pick a default:
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
```sh
|
|
33
|
+
ecf workspace list
|
|
34
|
+
ecf workspace use <workspaceId>
|
|
35
|
+
```
|
|
35
36
|
|
|
36
37
|
## Commands
|
|
37
38
|
|
|
38
39
|
### Authentication
|
|
39
40
|
|
|
40
|
-
| Command
|
|
41
|
-
|
|
|
41
|
+
| Command | What it does |
|
|
42
|
+
| ----------- | ------------------------------------------------------------------------------------ |
|
|
42
43
|
| `ecf login` | Configure server URL and API token. Auto-selects the workspace if you only have one. |
|
|
43
44
|
|
|
44
45
|
### Workspaces
|
|
45
46
|
|
|
46
|
-
| Command
|
|
47
|
-
|
|
|
48
|
-
| `ecf workspace list`
|
|
49
|
-
| `ecf workspace use <id>` | Set the default workspace for future commands.
|
|
47
|
+
| Command | What it does |
|
|
48
|
+
| ------------------------ | --------------------------------------------------------- |
|
|
49
|
+
| `ecf workspace list` | Show the workspaces you belong to (marks the active one). |
|
|
50
|
+
| `ecf workspace use <id>` | Set the default workspace for future commands. |
|
|
50
51
|
|
|
51
52
|
### Submissions
|
|
52
53
|
|
|
53
|
-
| Command
|
|
54
|
-
|
|
|
55
|
-
| `ecf submissions list [options]`
|
|
56
|
-
| `ecf submissions get <id> [--json]`
|
|
57
|
-
| `ecf submissions status <id> <status>` | Update a submission's status.
|
|
58
|
-
| `ecf submissions comment <id> [body]`
|
|
54
|
+
| Command | What it does |
|
|
55
|
+
| -------------------------------------- | --------------------------------------------------- |
|
|
56
|
+
| `ecf submissions list [options]` | List submissions in the active workspace. |
|
|
57
|
+
| `ecf submissions get <id> [--json]` | Show details for a submission. |
|
|
58
|
+
| `ecf submissions status <id> <status>` | Update a submission's status. |
|
|
59
|
+
| `ecf submissions comment <id> [body]` | Add an internal comment (body may come from stdin). |
|
|
59
60
|
|
|
60
61
|
`ecf submissions list` options:
|
|
61
62
|
|
|
@@ -68,6 +69,48 @@ Requires Node.js 18 or newer.
|
|
|
68
69
|
|
|
69
70
|
Valid status values for `ecf submissions status`: `untriaged`, `open`, `in_progress`, `resolved`, `closed`.
|
|
70
71
|
|
|
72
|
+
### Knowledge base
|
|
73
|
+
|
|
74
|
+
Manage articles and categories scoped to a project. Pass the project's slug as the first positional argument.
|
|
75
|
+
|
|
76
|
+
#### Categories
|
|
77
|
+
|
|
78
|
+
| Command | What it does |
|
|
79
|
+
| --------------------------------------------------------------- | ------------------------------------- |
|
|
80
|
+
| `ecf kb categories list <projectSlug> [--json]` | List categories in the project. |
|
|
81
|
+
| `ecf kb categories create <projectSlug> --name <n> [--description <d>]` | Create a category. |
|
|
82
|
+
| `ecf kb categories delete <projectSlug> <categoryId>` | Delete a category. |
|
|
83
|
+
|
|
84
|
+
#### Articles
|
|
85
|
+
|
|
86
|
+
| Command | What it does |
|
|
87
|
+
| -------------------------------------------------------- | ----------------------------------------- |
|
|
88
|
+
| `ecf kb articles list <projectSlug> [options]` | List articles (optionally filter by status/category). |
|
|
89
|
+
| `ecf kb articles get <projectSlug> <articleId> [--json]` | Show an article (title, metadata, plain-text body). |
|
|
90
|
+
| `ecf kb articles create <projectSlug> --title <t> [options]` | Create a draft article. |
|
|
91
|
+
| `ecf kb articles update <projectSlug> <articleId> [options]` | Update an article. Unspecified fields keep their current values. |
|
|
92
|
+
| `ecf kb articles publish <projectSlug> <articleId>` | Publish (or republish) the article. |
|
|
93
|
+
| `ecf kb articles unpublish <projectSlug> <articleId>` | Move the article back to draft. |
|
|
94
|
+
| `ecf kb articles archive <projectSlug> <articleId>` | Archive the article. |
|
|
95
|
+
| `ecf kb articles delete <projectSlug> <articleId>` | Delete the article. |
|
|
96
|
+
|
|
97
|
+
`ecf kb articles list` options:
|
|
98
|
+
|
|
99
|
+
- `--status <s>` — filter by status (`draft`, `published`, `archived`)
|
|
100
|
+
- `--category <id>` — filter by category id
|
|
101
|
+
- `--json` — output raw JSON
|
|
102
|
+
|
|
103
|
+
`ecf kb articles create` / `update` options:
|
|
104
|
+
|
|
105
|
+
- `--title <t>` — required for `create`; optional for `update`
|
|
106
|
+
- `--summary <s>` — short blurb shown in lists and SEO meta
|
|
107
|
+
- `--category <id>` — assign to a category (use `--no-category` on `update` to detach)
|
|
108
|
+
- `--visibility public|internal` — defaults to `public`
|
|
109
|
+
- `--body-md <path|->` — read a Markdown file (or `-` for stdin) and convert to TipTap JSON
|
|
110
|
+
- `--body-json <path|->` — read a TipTap JSON document directly (file path or `-` for stdin)
|
|
111
|
+
|
|
112
|
+
Pass either `--body-md` or `--body-json`, not both. The Markdown importer supports headings, paragraphs, bullet/ordered lists, blockquotes, fenced code blocks, horizontal rules, hard breaks, and inline marks (`**bold**`, `*italic*`, `` `code` ``, `~~strike~~`, `[link](url)`).
|
|
113
|
+
|
|
71
114
|
## Examples
|
|
72
115
|
|
|
73
116
|
List open bugs, piped to your pager:
|
|
@@ -88,6 +131,24 @@ Resolve a submission:
|
|
|
88
131
|
ecf submissions status sub_abc123 resolved
|
|
89
132
|
```
|
|
90
133
|
|
|
134
|
+
Draft a knowledge base article from a Markdown file and publish it:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
ecf kb articles create my-project \
|
|
138
|
+
--title "Resetting your password" \
|
|
139
|
+
--summary "Step-by-step instructions for password resets." \
|
|
140
|
+
--body-md ./docs/password-reset.md
|
|
141
|
+
# → Created kba_abc123 (resetting-your-password)
|
|
142
|
+
|
|
143
|
+
ecf kb articles publish my-project kba_abc123
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Pipe Markdown straight from your editor:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
pbpaste | ecf kb articles update my-project kba_abc123 --body-md -
|
|
150
|
+
```
|
|
151
|
+
|
|
91
152
|
## Configuration
|
|
92
153
|
|
|
93
154
|
Configuration lives at `~/.config/ecf/config.json` and is created by `ecf login`. Delete the file to reset.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/commands/
|
|
4
|
-
import {
|
|
3
|
+
// src/commands/kb.ts
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
5
6
|
|
|
6
7
|
// src/api.ts
|
|
7
8
|
class ApiError extends Error {
|
|
@@ -68,7 +69,324 @@ function requireConfig() {
|
|
|
68
69
|
return config;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// src/commands/kb.ts
|
|
73
|
+
var VALID_STATUSES = ["draft", "published", "archived"];
|
|
74
|
+
var VALID_VISIBILITIES = ["public", "internal"];
|
|
75
|
+
async function kbCommand(args) {
|
|
76
|
+
const [resource, ...rest] = args;
|
|
77
|
+
if (resource === "categories")
|
|
78
|
+
return categoriesCommand(rest);
|
|
79
|
+
if (resource === "articles")
|
|
80
|
+
return articlesCommand(rest);
|
|
81
|
+
console.error("Usage: ecf kb <categories|articles> ...");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
async function categoriesCommand(args) {
|
|
85
|
+
const [sub, ...rest] = args;
|
|
86
|
+
if (sub === "list")
|
|
87
|
+
return listCategories(rest);
|
|
88
|
+
if (sub === "create")
|
|
89
|
+
return createCategory(rest);
|
|
90
|
+
if (sub === "delete")
|
|
91
|
+
return deleteCategory(rest);
|
|
92
|
+
console.error("Usage: ecf kb categories <list|create|delete> ...");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
async function listCategories(args) {
|
|
96
|
+
const { values, positionals } = parseArgs({
|
|
97
|
+
args,
|
|
98
|
+
options: { json: { type: "boolean" } },
|
|
99
|
+
allowPositionals: true
|
|
100
|
+
});
|
|
101
|
+
const [projectSlug] = positionals;
|
|
102
|
+
if (!projectSlug) {
|
|
103
|
+
console.error("Usage: ecf kb categories list <projectSlug> [--json]");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const config = requireConfig();
|
|
107
|
+
const { categories } = await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/categories`);
|
|
108
|
+
if (values.json) {
|
|
109
|
+
console.log(JSON.stringify(categories, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (categories.length === 0) {
|
|
113
|
+
console.log("No categories.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const c of categories) {
|
|
117
|
+
console.log(`${c.id} ${c.slug.padEnd(24)} ${c.name}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function createCategory(args) {
|
|
121
|
+
const { values, positionals } = parseArgs({
|
|
122
|
+
args,
|
|
123
|
+
options: {
|
|
124
|
+
name: { type: "string" },
|
|
125
|
+
description: { type: "string" }
|
|
126
|
+
},
|
|
127
|
+
allowPositionals: true
|
|
128
|
+
});
|
|
129
|
+
const [projectSlug] = positionals;
|
|
130
|
+
if (!projectSlug || !values.name) {
|
|
131
|
+
console.error("Usage: ecf kb categories create <projectSlug> --name <name> [--description <desc>]");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const config = requireConfig();
|
|
135
|
+
const { category } = await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/categories`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: JSON.stringify({ name: values.name, description: values.description })
|
|
138
|
+
});
|
|
139
|
+
console.log(`Created ${category.id} (${category.slug})`);
|
|
140
|
+
}
|
|
141
|
+
async function deleteCategory(args) {
|
|
142
|
+
const [projectSlug, categoryId] = args;
|
|
143
|
+
if (!projectSlug || !categoryId) {
|
|
144
|
+
console.error("Usage: ecf kb categories delete <projectSlug> <categoryId>");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const config = requireConfig();
|
|
148
|
+
await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/categories/${encodeURIComponent(categoryId)}`, { method: "DELETE" });
|
|
149
|
+
console.log(`Deleted ${categoryId}`);
|
|
150
|
+
}
|
|
151
|
+
async function articlesCommand(args) {
|
|
152
|
+
const [sub, ...rest] = args;
|
|
153
|
+
if (sub === "list")
|
|
154
|
+
return listArticles(rest);
|
|
155
|
+
if (sub === "get")
|
|
156
|
+
return getArticle(rest);
|
|
157
|
+
if (sub === "create")
|
|
158
|
+
return createArticle(rest);
|
|
159
|
+
if (sub === "update")
|
|
160
|
+
return updateArticle(rest);
|
|
161
|
+
if (sub === "publish")
|
|
162
|
+
return setStatus(rest, "published");
|
|
163
|
+
if (sub === "unpublish")
|
|
164
|
+
return setStatus(rest, "draft");
|
|
165
|
+
if (sub === "archive")
|
|
166
|
+
return setStatus(rest, "archived");
|
|
167
|
+
if (sub === "delete")
|
|
168
|
+
return deleteArticle(rest);
|
|
169
|
+
console.error("Usage: ecf kb articles <list|get|create|update|publish|unpublish|archive|delete> ...");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
async function listArticles(args) {
|
|
173
|
+
const { values, positionals } = parseArgs({
|
|
174
|
+
args,
|
|
175
|
+
options: {
|
|
176
|
+
status: { type: "string" },
|
|
177
|
+
category: { type: "string" },
|
|
178
|
+
json: { type: "boolean" }
|
|
179
|
+
},
|
|
180
|
+
allowPositionals: true
|
|
181
|
+
});
|
|
182
|
+
const [projectSlug] = positionals;
|
|
183
|
+
if (!projectSlug) {
|
|
184
|
+
console.error("Usage: ecf kb articles list <projectSlug> [--status draft|published|archived] [--category <id>] [--json]");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
if (values.status && !VALID_STATUSES.includes(values.status)) {
|
|
188
|
+
console.error(`Invalid --status. Must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const params = new URLSearchParams;
|
|
192
|
+
if (values.status)
|
|
193
|
+
params.set("status", values.status);
|
|
194
|
+
if (values.category)
|
|
195
|
+
params.set("categoryId", values.category);
|
|
196
|
+
const config = requireConfig();
|
|
197
|
+
const query = params.toString();
|
|
198
|
+
const { articles } = await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/articles${query ? `?${query}` : ""}`);
|
|
199
|
+
if (values.json) {
|
|
200
|
+
console.log(JSON.stringify(articles, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (articles.length === 0) {
|
|
204
|
+
console.log("No articles.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
for (const a of articles) {
|
|
208
|
+
const cat = a.categoryName ?? "—";
|
|
209
|
+
console.log(`${a.id} [${a.status.padEnd(9)}] ${a.visibility.padEnd(8)} ${cat.padEnd(20)} ${a.title}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function getArticle(args) {
|
|
213
|
+
const { values, positionals } = parseArgs({
|
|
214
|
+
args,
|
|
215
|
+
options: { json: { type: "boolean" } },
|
|
216
|
+
allowPositionals: true
|
|
217
|
+
});
|
|
218
|
+
const [projectSlug, articleId] = positionals;
|
|
219
|
+
if (!projectSlug || !articleId) {
|
|
220
|
+
console.error("Usage: ecf kb articles get <projectSlug> <articleId> [--json]");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const config = requireConfig();
|
|
224
|
+
const { article } = await fetchArticle(config, projectSlug, articleId);
|
|
225
|
+
if (values.json) {
|
|
226
|
+
console.log(JSON.stringify(article, null, 2));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
console.log(`# ${article.title}`);
|
|
230
|
+
console.log(`id: ${article.id} slug: ${article.slug} status: ${article.status} visibility: ${article.visibility}`);
|
|
231
|
+
if (article.categoryName)
|
|
232
|
+
console.log(`category: ${article.categoryName} (${article.categoryId})`);
|
|
233
|
+
if (article.summary)
|
|
234
|
+
console.log(`
|
|
235
|
+
${article.summary}`);
|
|
236
|
+
if (article.bodyText)
|
|
237
|
+
console.log(`
|
|
238
|
+
${article.bodyText}`);
|
|
239
|
+
}
|
|
240
|
+
async function createArticle(args) {
|
|
241
|
+
const { values, positionals } = parseArgs({
|
|
242
|
+
args,
|
|
243
|
+
options: {
|
|
244
|
+
title: { type: "string" },
|
|
245
|
+
summary: { type: "string" },
|
|
246
|
+
category: { type: "string" },
|
|
247
|
+
visibility: { type: "string" },
|
|
248
|
+
"body-md": { type: "string" },
|
|
249
|
+
"body-json": { type: "string" }
|
|
250
|
+
},
|
|
251
|
+
allowPositionals: true
|
|
252
|
+
});
|
|
253
|
+
const [projectSlug] = positionals;
|
|
254
|
+
if (!projectSlug || !values.title) {
|
|
255
|
+
console.error("Usage: ecf kb articles create <projectSlug> --title <t> [--summary <s>] [--category <id>] [--visibility public|internal] [--body-md <path|-> | --body-json <path|->]");
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
if (values.visibility && !VALID_VISIBILITIES.includes(values.visibility)) {
|
|
259
|
+
console.error(`Invalid --visibility. Must be one of: ${VALID_VISIBILITIES.join(", ")}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
const bodyFields = await readBodyFields({
|
|
263
|
+
bodyMd: values["body-md"],
|
|
264
|
+
bodyJson: values["body-json"]
|
|
265
|
+
});
|
|
266
|
+
if (bodyFields instanceof Error) {
|
|
267
|
+
console.error(bodyFields.message);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const payload = {
|
|
271
|
+
title: values.title,
|
|
272
|
+
summary: values.summary,
|
|
273
|
+
categoryId: values.category ?? null,
|
|
274
|
+
visibility: values.visibility,
|
|
275
|
+
...bodyFields
|
|
276
|
+
};
|
|
277
|
+
const config = requireConfig();
|
|
278
|
+
const { article } = await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/articles`, { method: "POST", body: JSON.stringify(payload) });
|
|
279
|
+
console.log(`Created ${article.id} (${article.slug})`);
|
|
280
|
+
}
|
|
281
|
+
async function updateArticle(args) {
|
|
282
|
+
const { values, positionals } = parseArgs({
|
|
283
|
+
args,
|
|
284
|
+
options: {
|
|
285
|
+
title: { type: "string" },
|
|
286
|
+
summary: { type: "string" },
|
|
287
|
+
category: { type: "string" },
|
|
288
|
+
"no-category": { type: "boolean" },
|
|
289
|
+
visibility: { type: "string" },
|
|
290
|
+
"body-md": { type: "string" },
|
|
291
|
+
"body-json": { type: "string" }
|
|
292
|
+
},
|
|
293
|
+
allowPositionals: true
|
|
294
|
+
});
|
|
295
|
+
const [projectSlug, articleId] = positionals;
|
|
296
|
+
if (!projectSlug || !articleId) {
|
|
297
|
+
console.error("Usage: ecf kb articles update <projectSlug> <articleId> [--title ...] [--summary ...] [--category <id> | --no-category] [--visibility ...] [--body-md <path|-> | --body-json <path|->]");
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
if (values.visibility && !VALID_VISIBILITIES.includes(values.visibility)) {
|
|
301
|
+
console.error(`Invalid --visibility. Must be one of: ${VALID_VISIBILITIES.join(", ")}`);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const bodyFields = await readBodyFields({
|
|
305
|
+
bodyMd: values["body-md"],
|
|
306
|
+
bodyJson: values["body-json"]
|
|
307
|
+
});
|
|
308
|
+
if (bodyFields instanceof Error) {
|
|
309
|
+
console.error(bodyFields.message);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
const config = requireConfig();
|
|
313
|
+
const { article: current } = await fetchArticle(config, projectSlug, articleId);
|
|
314
|
+
const nextTitle = values.title ?? current.title;
|
|
315
|
+
const nextSummary = values.summary !== undefined ? values.summary : current.summary ?? undefined;
|
|
316
|
+
const nextVisibility = values.visibility ?? current.visibility;
|
|
317
|
+
let nextCategoryId;
|
|
318
|
+
if (values["no-category"]) {
|
|
319
|
+
nextCategoryId = null;
|
|
320
|
+
} else if (typeof values.category === "string") {
|
|
321
|
+
nextCategoryId = values.category;
|
|
322
|
+
} else {
|
|
323
|
+
nextCategoryId = current.categoryId;
|
|
324
|
+
}
|
|
325
|
+
const payload = {
|
|
326
|
+
title: nextTitle,
|
|
327
|
+
summary: nextSummary,
|
|
328
|
+
categoryId: nextCategoryId,
|
|
329
|
+
visibility: nextVisibility,
|
|
330
|
+
...Object.keys(bodyFields).length > 0 ? bodyFields : { bodyJson: current.bodyJson ?? { type: "doc", content: [] } }
|
|
331
|
+
};
|
|
332
|
+
const { article } = await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/articles/${encodeURIComponent(articleId)}`, { method: "PATCH", body: JSON.stringify(payload) });
|
|
333
|
+
console.log(`Updated ${article.id} (${article.slug})`);
|
|
334
|
+
}
|
|
335
|
+
async function setStatus(args, status) {
|
|
336
|
+
const [projectSlug, articleId] = args;
|
|
337
|
+
if (!projectSlug || !articleId) {
|
|
338
|
+
console.error(`Usage: ecf kb articles <publish|unpublish|archive> <projectSlug> <articleId>`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
const config = requireConfig();
|
|
342
|
+
await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/articles/${encodeURIComponent(articleId)}/status`, { method: "POST", body: JSON.stringify({ status }) });
|
|
343
|
+
console.log(`${articleId} → ${status}`);
|
|
344
|
+
}
|
|
345
|
+
async function deleteArticle(args) {
|
|
346
|
+
const [projectSlug, articleId] = args;
|
|
347
|
+
if (!projectSlug || !articleId) {
|
|
348
|
+
console.error("Usage: ecf kb articles delete <projectSlug> <articleId>");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
const config = requireConfig();
|
|
352
|
+
await apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/articles/${encodeURIComponent(articleId)}`, { method: "DELETE" });
|
|
353
|
+
console.log(`Deleted ${articleId}`);
|
|
354
|
+
}
|
|
355
|
+
async function fetchArticle(config, projectSlug, articleId) {
|
|
356
|
+
return apiFetch(config, `/api/v1/projects/${encodeURIComponent(projectSlug)}/kb/articles/${encodeURIComponent(articleId)}`);
|
|
357
|
+
}
|
|
358
|
+
async function readSource(source) {
|
|
359
|
+
if (source === "-") {
|
|
360
|
+
const chunks = [];
|
|
361
|
+
for await (const chunk of process.stdin) {
|
|
362
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
363
|
+
}
|
|
364
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
365
|
+
}
|
|
366
|
+
return readFile(source, "utf8");
|
|
367
|
+
}
|
|
368
|
+
async function readBodyFields(input) {
|
|
369
|
+
if (input.bodyMd && input.bodyJson) {
|
|
370
|
+
return new Error("Pass either --body-md or --body-json, not both");
|
|
371
|
+
}
|
|
372
|
+
if (input.bodyMd) {
|
|
373
|
+
const text = await readSource(input.bodyMd);
|
|
374
|
+
return { bodyMarkdown: text };
|
|
375
|
+
}
|
|
376
|
+
if (input.bodyJson) {
|
|
377
|
+
const text = await readSource(input.bodyJson);
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(text);
|
|
380
|
+
return { bodyJson: parsed };
|
|
381
|
+
} catch {
|
|
382
|
+
return new Error(`--body-json: not valid JSON (${input.bodyJson})`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
|
|
71
388
|
// src/commands/login.ts
|
|
389
|
+
import { createInterface } from "node:readline/promises";
|
|
72
390
|
async function prompt(rl, question, def) {
|
|
73
391
|
const suffix = def ? ` [${def}]` : "";
|
|
74
392
|
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
@@ -109,7 +427,7 @@ Active workspace set to ${workspaces[0].name}.`);
|
|
|
109
427
|
}
|
|
110
428
|
|
|
111
429
|
// src/commands/submissions.ts
|
|
112
|
-
import { parseArgs } from "node:util";
|
|
430
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
113
431
|
function resolveWorkspace(config, override) {
|
|
114
432
|
const id = override ?? config.workspaceId;
|
|
115
433
|
if (!id) {
|
|
@@ -139,7 +457,7 @@ async function submissionsCommand(args) {
|
|
|
139
457
|
process.exit(1);
|
|
140
458
|
}
|
|
141
459
|
async function list(args) {
|
|
142
|
-
const { values } =
|
|
460
|
+
const { values } = parseArgs2({
|
|
143
461
|
args,
|
|
144
462
|
options: {
|
|
145
463
|
status: { type: "string", multiple: true },
|
|
@@ -177,7 +495,7 @@ async function list(args) {
|
|
|
177
495
|
}
|
|
178
496
|
}
|
|
179
497
|
async function get(args) {
|
|
180
|
-
const { values, positionals } =
|
|
498
|
+
const { values, positionals } = parseArgs2({
|
|
181
499
|
args,
|
|
182
500
|
options: { json: { type: "boolean" } },
|
|
183
501
|
allowPositionals: true
|
|
@@ -307,6 +625,20 @@ Usage:
|
|
|
307
625
|
ecf submissions status <id> <status> Update a submission's status
|
|
308
626
|
ecf submissions comment <id> [body] Add an internal comment
|
|
309
627
|
(if body is omitted, reads from stdin)
|
|
628
|
+
|
|
629
|
+
ecf kb categories list <projectSlug> [--json]
|
|
630
|
+
ecf kb categories create <projectSlug> --name <n> [--description <d>]
|
|
631
|
+
ecf kb categories delete <projectSlug> <categoryId>
|
|
632
|
+
|
|
633
|
+
ecf kb articles list <projectSlug> [--status <s>] [--category <id>] [--json]
|
|
634
|
+
ecf kb articles get <projectSlug> <articleId> [--json]
|
|
635
|
+
ecf kb articles create <projectSlug> --title <t> [options]
|
|
636
|
+
--summary <s> --category <id> --visibility public|internal
|
|
637
|
+
--body-md <path|-> Markdown source (file path, or "-" for stdin)
|
|
638
|
+
--body-json <path|-> TipTap JSON source (file path, or "-" for stdin)
|
|
639
|
+
ecf kb articles update <projectSlug> <articleId> [same options + --no-category]
|
|
640
|
+
ecf kb articles publish|unpublish|archive <projectSlug> <articleId>
|
|
641
|
+
ecf kb articles delete <projectSlug> <articleId>
|
|
310
642
|
`;
|
|
311
643
|
async function main() {
|
|
312
644
|
const argv = process.argv.slice(2);
|
|
@@ -326,6 +658,9 @@ async function main() {
|
|
|
326
658
|
case "submissions":
|
|
327
659
|
await submissionsCommand(rest);
|
|
328
660
|
return;
|
|
661
|
+
case "kb":
|
|
662
|
+
await kbCommand(rest);
|
|
663
|
+
return;
|
|
329
664
|
default:
|
|
330
665
|
console.error(`Unknown command: ${command}
|
|
331
666
|
`);
|
package/package.json
CHANGED