highspot-cli 0.1.1
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 +138 -0
- package/dist/bin/highspot.js +3 -0
- package/dist/commands/content.js +70 -0
- package/dist/commands/help.js +23 -0
- package/dist/commands/item.js +66 -0
- package/dist/commands/me.js +48 -0
- package/dist/commands/search.js +78 -0
- package/dist/lib/api.js +215 -0
- package/dist/lib/command.js +62 -0
- package/dist/lib/config.js +114 -0
- package/dist/lib/flags.js +75 -0
- package/dist/lib/help.js +23 -0
- package/dist/lib/output.js +29 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Advait Shinde
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# highspot-cli
|
|
2
|
+
|
|
3
|
+
`highspot-cli` is an unofficial CLI client for the [Highspot](https://www.highspot.com/) REST API.
|
|
4
|
+
|
|
5
|
+
It is designed for both humans and agents:
|
|
6
|
+
|
|
7
|
+
- default output: JSON on stdout
|
|
8
|
+
- script-stable output: `--plain`
|
|
9
|
+
- diagnostics/errors: stderr
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g highspot-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run without global install:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx highspot-cli --help
|
|
21
|
+
bunx highspot-cli --help
|
|
22
|
+
deno run -A npm:highspot-cli --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Auth
|
|
26
|
+
|
|
27
|
+
Set credentials with environment variables:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export HIGHSPOT_API_KEY_ID=hs_key_id_xxx
|
|
31
|
+
export HIGHSPOT_API_KEY_SECRET=hs_key_secret_xxx
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Optional:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export HIGHSPOT_API_ENDPOINT=https://api.highspot.com/v1.0
|
|
38
|
+
export HIGHSPOT_HS_USER=user@example.com
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`HIGHSPOT_HS_USER` (or `--hs-user`) is optional impersonation context.
|
|
42
|
+
It is not implied by the API key:
|
|
43
|
+
- API key (`HIGHSPOT_API_KEY_ID` + `HIGHSPOT_API_KEY_SECRET`) authenticates the caller.
|
|
44
|
+
- `hs-user` sets an explicit user context for requests where impersonation is needed.
|
|
45
|
+
- CLI flag precedence still applies, so `--hs-user` overrides `HIGHSPOT_HS_USER`.
|
|
46
|
+
|
|
47
|
+
## Config Files
|
|
48
|
+
|
|
49
|
+
Config precedence (highest to lowest):
|
|
50
|
+
|
|
51
|
+
1. CLI flags
|
|
52
|
+
2. Environment variables
|
|
53
|
+
3. Project config: `.highspot-cli.json`
|
|
54
|
+
4. User config: `~/.config/highspot-cli/config.json`
|
|
55
|
+
5. System config: `/etc/highspot-cli/config.json`
|
|
56
|
+
|
|
57
|
+
Example `.highspot-cli.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"endpoint": "https://api.highspot.com/v1.0",
|
|
62
|
+
"hsUser": "user@example.com",
|
|
63
|
+
"maxRetries": 3,
|
|
64
|
+
"retryDelayMs": 1200,
|
|
65
|
+
"timeoutMs": 30000,
|
|
66
|
+
"apiKeyId": "hs_key_id_xxx",
|
|
67
|
+
"apiKeySecret": "hs_key_secret_xxx"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Commands
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
highspot search <query>
|
|
75
|
+
highspot item <item-id>
|
|
76
|
+
highspot content <item-id>
|
|
77
|
+
highspot me
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Global flags:
|
|
81
|
+
|
|
82
|
+
- `-h, --help`
|
|
83
|
+
- `--version`
|
|
84
|
+
- `--json` (default output mode)
|
|
85
|
+
- `--plain` (line-based stable output)
|
|
86
|
+
- `--dry-run` (print request and exit)
|
|
87
|
+
- `--hs-user <value>`
|
|
88
|
+
- `--endpoint <url>`
|
|
89
|
+
- `--timeout-ms <n>`
|
|
90
|
+
- `--max-retries <n>`
|
|
91
|
+
- `--retry-delay-ms <n>`
|
|
92
|
+
- `--quiet`
|
|
93
|
+
- `--verbose`
|
|
94
|
+
- `--no-input`
|
|
95
|
+
- `--no-color`
|
|
96
|
+
|
|
97
|
+
Exit codes:
|
|
98
|
+
|
|
99
|
+
- `0` success
|
|
100
|
+
- `1` API/runtime failure
|
|
101
|
+
- `2` invalid usage or missing configuration
|
|
102
|
+
|
|
103
|
+
## Examples
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
highspot search "GoGuardian Teacher" --limit 10
|
|
107
|
+
highspot search "Beacon" --sort-by date_added --plain
|
|
108
|
+
highspot item it_abc123
|
|
109
|
+
highspot content it_abc123 --format text/plain --plain
|
|
110
|
+
highspot me --json
|
|
111
|
+
highspot search "Fleet" --dry-run
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Behavior notes:
|
|
115
|
+
|
|
116
|
+
- Prompts are not used; `--no-input` is accepted for automation consistency.
|
|
117
|
+
- Primary data goes to stdout, errors go to stderr.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install
|
|
123
|
+
npm run build
|
|
124
|
+
npm run check
|
|
125
|
+
npm run format
|
|
126
|
+
node dist/bin/highspot.js --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Publish (npm)
|
|
130
|
+
|
|
131
|
+
For `advait/highspot-cli`:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm version patch
|
|
135
|
+
npm publish --access public
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Then tag/push your release in GitHub.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BaseCommand } from "../lib/command.js";
|
|
3
|
+
import { contentFlags, globalFlags } from "../lib/flags.js";
|
|
4
|
+
export default class Content extends BaseCommand {
|
|
5
|
+
static description = "Fetch Highspot item content";
|
|
6
|
+
static examples = [
|
|
7
|
+
"highspot content it_abc123",
|
|
8
|
+
"highspot content it_abc123 --format text/plain --plain",
|
|
9
|
+
"highspot content it_abc123 --start cursor-2",
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
itemId: Args.string({
|
|
13
|
+
description: "Highspot item id",
|
|
14
|
+
required: false,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
...globalFlags,
|
|
19
|
+
...contentFlags,
|
|
20
|
+
};
|
|
21
|
+
async run() {
|
|
22
|
+
const { args, flags } = await this.parse(Content);
|
|
23
|
+
this.ensureOutputFlags(flags);
|
|
24
|
+
this.ensureVerbosityFlags(flags);
|
|
25
|
+
if (!args.itemId) {
|
|
26
|
+
this.fail("itemId is required", 2, flags);
|
|
27
|
+
}
|
|
28
|
+
const payload = {
|
|
29
|
+
endpoint: `/items/${encodeURIComponent(args.itemId)}/content`,
|
|
30
|
+
query: {
|
|
31
|
+
...(flags.format ? { format: flags.format } : {}),
|
|
32
|
+
...(flags.start ? { start: flags.start } : {}),
|
|
33
|
+
},
|
|
34
|
+
headers: {
|
|
35
|
+
...(flags["hs-user"] ? { "hs-user": flags["hs-user"] } : {}),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
if (flags["dry-run"]) {
|
|
39
|
+
this.printJson(payload);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const client = this.clientFromFlags(flags);
|
|
44
|
+
const data = await client.getItemContent({
|
|
45
|
+
itemId: args.itemId,
|
|
46
|
+
format: flags.format,
|
|
47
|
+
start: flags.start,
|
|
48
|
+
hsUser: this.effectiveHsUser(flags),
|
|
49
|
+
});
|
|
50
|
+
if (this.outputMode(flags) === "plain") {
|
|
51
|
+
writeContentPlain(data.content);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.printJson(data);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.handleError(error, flags);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeContentPlain(content) {
|
|
62
|
+
if (typeof content === "string") {
|
|
63
|
+
process.stdout.write(content);
|
|
64
|
+
if (!content.endsWith("\n")) {
|
|
65
|
+
process.stdout.write("\n");
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
process.stdout.write(`${JSON.stringify(content, null, 2)}\n`);
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Args, Command, loadHelpClass } from "@oclif/core";
|
|
2
|
+
export default class Help extends Command {
|
|
3
|
+
static description = "Show help for a command";
|
|
4
|
+
static args = {
|
|
5
|
+
command: Args.string({
|
|
6
|
+
description: "Command to show help for",
|
|
7
|
+
required: false,
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
static examples = [
|
|
11
|
+
"highspot help",
|
|
12
|
+
"highspot help search",
|
|
13
|
+
"highspot search --help",
|
|
14
|
+
"highspot -h",
|
|
15
|
+
];
|
|
16
|
+
async run() {
|
|
17
|
+
const { args } = await this.parse(Help);
|
|
18
|
+
const HelpClass = await loadHelpClass(this.config);
|
|
19
|
+
const help = new HelpClass(this.config, this.config.pjson.oclif.helpOptions ?? this.config.pjson.helpOptions);
|
|
20
|
+
const argv = args.command ? [args.command] : [];
|
|
21
|
+
await help.showHelp(argv);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BaseCommand } from "../lib/command.js";
|
|
3
|
+
import { globalFlags } from "../lib/flags.js";
|
|
4
|
+
export default class Item extends BaseCommand {
|
|
5
|
+
static description = "Fetch metadata for a Highspot item";
|
|
6
|
+
static examples = [
|
|
7
|
+
"highspot item it_abc123",
|
|
8
|
+
"highspot item it_abc123 --hs-user user@example.com",
|
|
9
|
+
"highspot item it_abc123 --plain",
|
|
10
|
+
];
|
|
11
|
+
static args = {
|
|
12
|
+
itemId: Args.string({
|
|
13
|
+
description: "Highspot item id",
|
|
14
|
+
required: false,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
static flags = {
|
|
18
|
+
...globalFlags,
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { args, flags } = await this.parse(Item);
|
|
22
|
+
this.ensureOutputFlags(flags);
|
|
23
|
+
this.ensureVerbosityFlags(flags);
|
|
24
|
+
if (!args.itemId) {
|
|
25
|
+
this.fail("itemId is required", 2, flags);
|
|
26
|
+
}
|
|
27
|
+
const payload = {
|
|
28
|
+
endpoint: `/items/${encodeURIComponent(args.itemId)}`,
|
|
29
|
+
headers: {
|
|
30
|
+
...(flags["hs-user"] ? { "hs-user": flags["hs-user"] } : {}),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
if (flags["dry-run"]) {
|
|
34
|
+
this.printJson(payload);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const client = this.clientFromFlags(flags);
|
|
39
|
+
const data = await client.getItem({
|
|
40
|
+
itemId: args.itemId,
|
|
41
|
+
hsUser: this.effectiveHsUser(flags),
|
|
42
|
+
});
|
|
43
|
+
if (this.outputMode(flags) === "plain") {
|
|
44
|
+
writeItemPlain(data.item);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.printJson(data);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.handleError(error, flags);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function writeItemPlain(item) {
|
|
55
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
56
|
+
process.stdout.write(`${JSON.stringify(item)}\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const record = item;
|
|
60
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
61
|
+
const title = typeof record.title === "string" ? record.title : "";
|
|
62
|
+
const url = typeof record.url === "string" ? record.url : "";
|
|
63
|
+
const contentType = typeof record.content_type === "string" ? record.content_type : "";
|
|
64
|
+
const description = typeof record.description === "string" ? record.description : "";
|
|
65
|
+
process.stdout.write(`${id}\t${url}\t${title}\t${contentType}\t${description}\n`);
|
|
66
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseCommand } from "../lib/command.js";
|
|
2
|
+
import { globalFlags } from "../lib/flags.js";
|
|
3
|
+
export default class Me extends BaseCommand {
|
|
4
|
+
static description = "Fetch /me from the Highspot API";
|
|
5
|
+
static aliases = ["whoami"];
|
|
6
|
+
static examples = ["highspot me", "highspot me --plain", "highspot whoami"];
|
|
7
|
+
static flags = {
|
|
8
|
+
...globalFlags,
|
|
9
|
+
};
|
|
10
|
+
async run() {
|
|
11
|
+
const { flags } = await this.parse(Me);
|
|
12
|
+
this.ensureOutputFlags(flags);
|
|
13
|
+
this.ensureVerbosityFlags(flags);
|
|
14
|
+
const payload = {
|
|
15
|
+
endpoint: "/me",
|
|
16
|
+
headers: {
|
|
17
|
+
...(flags["hs-user"] ? { "hs-user": flags["hs-user"] } : {}),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
if (flags["dry-run"]) {
|
|
21
|
+
this.printJson(payload);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const client = this.clientFromFlags(flags);
|
|
26
|
+
const data = await client.getMe({ hsUser: this.effectiveHsUser(flags) });
|
|
27
|
+
if (this.outputMode(flags) === "plain") {
|
|
28
|
+
writeMePlain(data);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.printJson(data);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
this.handleError(error, flags);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function writeMePlain(data) {
|
|
39
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
40
|
+
process.stdout.write(`${JSON.stringify(data)}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const record = data;
|
|
44
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
45
|
+
const email = typeof record.email === "string" ? record.email : "";
|
|
46
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
47
|
+
process.stdout.write(`${id}\t${email}\t${name}\n`);
|
|
48
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Args } from "@oclif/core";
|
|
2
|
+
import { BaseCommand } from "../lib/command.js";
|
|
3
|
+
import { globalFlags, searchFlags } from "../lib/flags.js";
|
|
4
|
+
export default class Search extends BaseCommand {
|
|
5
|
+
static description = "Search Highspot items";
|
|
6
|
+
static examples = [
|
|
7
|
+
'highspot search "GoGuardian Teacher"',
|
|
8
|
+
'highspot search "Beacon" --limit 5 --sort-by date_added',
|
|
9
|
+
'highspot search "Hall Pass" --with-fields spot,id,title,url --plain',
|
|
10
|
+
'highspot search "Fleet" --dry-run',
|
|
11
|
+
];
|
|
12
|
+
static args = {
|
|
13
|
+
query: Args.string({
|
|
14
|
+
description: "Search query",
|
|
15
|
+
required: false,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
static flags = {
|
|
19
|
+
...globalFlags,
|
|
20
|
+
...searchFlags,
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(Search);
|
|
24
|
+
this.ensureOutputFlags(flags);
|
|
25
|
+
this.ensureVerbosityFlags(flags);
|
|
26
|
+
if (!args.query) {
|
|
27
|
+
this.fail("query is required", 2, flags);
|
|
28
|
+
}
|
|
29
|
+
const sortBy = flags["sort-by"];
|
|
30
|
+
const normalizedSortBy = sortBy === "relevancy" || sortBy === "date_added" ? sortBy : undefined;
|
|
31
|
+
if (sortBy && !normalizedSortBy) {
|
|
32
|
+
this.fail("--sort-by must be one of: relevancy, date_added", 2, flags);
|
|
33
|
+
}
|
|
34
|
+
const payload = {
|
|
35
|
+
endpoint: "/search/items",
|
|
36
|
+
query: {
|
|
37
|
+
"query-string": args.query,
|
|
38
|
+
...(flags.limit !== undefined ? { limit: flags.limit } : {}),
|
|
39
|
+
...(flags.start !== undefined ? { start: flags.start } : {}),
|
|
40
|
+
...(normalizedSortBy ? { sortby: normalizedSortBy } : {}),
|
|
41
|
+
...(flags["with-fields"]
|
|
42
|
+
? { "with-fields": flags["with-fields"] }
|
|
43
|
+
: {}),
|
|
44
|
+
},
|
|
45
|
+
headers: {
|
|
46
|
+
...(flags["hs-user"] ? { "hs-user": flags["hs-user"] } : {}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
if (flags["dry-run"]) {
|
|
50
|
+
this.printJson(payload);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const client = this.clientFromFlags(flags);
|
|
55
|
+
const data = await client.searchItems({
|
|
56
|
+
query: args.query,
|
|
57
|
+
limit: flags.limit,
|
|
58
|
+
start: flags.start,
|
|
59
|
+
sortBy: normalizedSortBy,
|
|
60
|
+
withFields: flags["with-fields"],
|
|
61
|
+
hsUser: this.effectiveHsUser(flags),
|
|
62
|
+
});
|
|
63
|
+
if (this.outputMode(flags) === "plain") {
|
|
64
|
+
writeSearchPlain(data);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.printJson(data);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.handleError(error, flags);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeSearchPlain(data) {
|
|
75
|
+
for (const [index, item] of data.results.entries()) {
|
|
76
|
+
process.stdout.write(`${index + 1}\t${item.id}\t${item.url}\t${item.title}\t${item.contentType}\n`);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
const TEXTUAL_CONTENT_TYPE_HINTS = [
|
|
3
|
+
"application/xml",
|
|
4
|
+
"application/xhtml+xml",
|
|
5
|
+
"application/csv",
|
|
6
|
+
"application/rtf",
|
|
7
|
+
"application/markdown",
|
|
8
|
+
"application/x-markdown",
|
|
9
|
+
"text/csv",
|
|
10
|
+
"text/html",
|
|
11
|
+
"text/markdown",
|
|
12
|
+
];
|
|
13
|
+
export class ApiError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
body;
|
|
16
|
+
constructor(message, status, body) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ApiError";
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.body = body;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
24
|
+
setTimeout(resolve, ms);
|
|
25
|
+
});
|
|
26
|
+
function sanitizeEndpoint(endpoint) {
|
|
27
|
+
return endpoint.replace(/\/+$/, "");
|
|
28
|
+
}
|
|
29
|
+
function looksTextual(contentType) {
|
|
30
|
+
if (contentType.startsWith("text/")) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return TEXTUAL_CONTENT_TYPE_HINTS.some((hint) => contentType.includes(hint));
|
|
34
|
+
}
|
|
35
|
+
export class HighspotClient {
|
|
36
|
+
#authorizationHeader;
|
|
37
|
+
#endpoint;
|
|
38
|
+
#hsUser;
|
|
39
|
+
#maxRetries;
|
|
40
|
+
#retryDelayMs;
|
|
41
|
+
#timeoutMs;
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.#authorizationHeader = `Basic ${Buffer.from(`${config.apiKeyId}:${config.apiKeySecret}`).toString("base64")}`;
|
|
44
|
+
this.#endpoint = sanitizeEndpoint(config.endpoint);
|
|
45
|
+
this.#hsUser = config.hsUser;
|
|
46
|
+
this.#maxRetries = config.maxRetries;
|
|
47
|
+
this.#retryDelayMs = config.retryDelayMs;
|
|
48
|
+
this.#timeoutMs = config.timeoutMs;
|
|
49
|
+
}
|
|
50
|
+
#buildUrl(pathname) {
|
|
51
|
+
return new URL(`${this.#endpoint}${pathname}`);
|
|
52
|
+
}
|
|
53
|
+
async #fetchWithRetry(args) {
|
|
54
|
+
let response;
|
|
55
|
+
for (let attempt = 0; attempt <= this.#maxRetries; attempt += 1) {
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timeoutHandle = setTimeout(() => controller.abort(), this.#timeoutMs);
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch(args.url, {
|
|
60
|
+
method: "GET",
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "*/*",
|
|
63
|
+
Authorization: this.#authorizationHeader,
|
|
64
|
+
...((args.hsUser ?? this.#hsUser)
|
|
65
|
+
? { "hs-user": args.hsUser ?? this.#hsUser }
|
|
66
|
+
: {}),
|
|
67
|
+
},
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (attempt === this.#maxRetries) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const delayMs = this.#retryDelayMs * (attempt + 1);
|
|
76
|
+
await sleep(delayMs);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timeoutHandle);
|
|
81
|
+
}
|
|
82
|
+
if (response.ok) {
|
|
83
|
+
return response;
|
|
84
|
+
}
|
|
85
|
+
if (attempt < this.#maxRetries &&
|
|
86
|
+
(response.status === 429 || response.status >= 500)) {
|
|
87
|
+
const delayMs = this.#retryDelayMs * (attempt + 1);
|
|
88
|
+
await sleep(delayMs);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const body = await readResponseBody(response);
|
|
92
|
+
throw new ApiError(`Highspot request "${args.operation}" failed with status ${response.status}`, response.status, body);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Highspot request "${args.operation}" failed unexpectedly`);
|
|
95
|
+
}
|
|
96
|
+
async searchItems(args) {
|
|
97
|
+
const limit = args.limit ?? 10;
|
|
98
|
+
const start = args.start ?? 0;
|
|
99
|
+
const url = this.#buildUrl("/search/items");
|
|
100
|
+
url.searchParams.set("query-string", args.query);
|
|
101
|
+
url.searchParams.set("limit", String(limit));
|
|
102
|
+
if (args.start !== undefined) {
|
|
103
|
+
url.searchParams.set("start", String(args.start));
|
|
104
|
+
}
|
|
105
|
+
if (args.sortBy) {
|
|
106
|
+
url.searchParams.set("sortby", args.sortBy);
|
|
107
|
+
}
|
|
108
|
+
if (args.withFields) {
|
|
109
|
+
url.searchParams.set("with-fields", args.withFields);
|
|
110
|
+
}
|
|
111
|
+
const response = await this.#fetchWithRetry({
|
|
112
|
+
operation: "searchItems",
|
|
113
|
+
url,
|
|
114
|
+
...(args.hsUser ? { hsUser: args.hsUser } : {}),
|
|
115
|
+
});
|
|
116
|
+
const payload = (await response.json());
|
|
117
|
+
const source = payload.collection ?? payload.results ?? [];
|
|
118
|
+
const results = source
|
|
119
|
+
.filter((item) => typeof item.id === "string" && item.id.trim().length > 0)
|
|
120
|
+
.map((item) => ({
|
|
121
|
+
contentType: item.content_type ?? "",
|
|
122
|
+
description: item.description ?? "",
|
|
123
|
+
id: item.id ?? "",
|
|
124
|
+
title: item.title ?? "",
|
|
125
|
+
url: item.url ?? "",
|
|
126
|
+
}));
|
|
127
|
+
return {
|
|
128
|
+
limit,
|
|
129
|
+
query: args.query,
|
|
130
|
+
results,
|
|
131
|
+
start,
|
|
132
|
+
total: payload.counts_total ?? payload.total_results ?? results.length,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async getItem(args) {
|
|
136
|
+
const url = this.#buildUrl(`/items/${encodeURIComponent(args.itemId)}`);
|
|
137
|
+
const response = await this.#fetchWithRetry({
|
|
138
|
+
operation: "getItem",
|
|
139
|
+
url,
|
|
140
|
+
...(args.hsUser ? { hsUser: args.hsUser } : {}),
|
|
141
|
+
});
|
|
142
|
+
const payload = (await response.json());
|
|
143
|
+
return { item: payload };
|
|
144
|
+
}
|
|
145
|
+
async getItemContent(args) {
|
|
146
|
+
const url = this.#buildUrl(`/items/${encodeURIComponent(args.itemId)}/content`);
|
|
147
|
+
if (args.format) {
|
|
148
|
+
url.searchParams.set("format", args.format);
|
|
149
|
+
}
|
|
150
|
+
if (args.start) {
|
|
151
|
+
url.searchParams.set("start", args.start);
|
|
152
|
+
}
|
|
153
|
+
const response = await this.#fetchWithRetry({
|
|
154
|
+
operation: "getItemContent",
|
|
155
|
+
url,
|
|
156
|
+
...(args.hsUser ? { hsUser: args.hsUser } : {}),
|
|
157
|
+
});
|
|
158
|
+
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim() ||
|
|
159
|
+
"application/octet-stream";
|
|
160
|
+
if (contentType.includes("json")) {
|
|
161
|
+
const payload = (await response.json());
|
|
162
|
+
return {
|
|
163
|
+
content: payload,
|
|
164
|
+
contentEncoding: "json",
|
|
165
|
+
contentType,
|
|
166
|
+
isBinary: false,
|
|
167
|
+
isJson: true,
|
|
168
|
+
itemId: args.itemId,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (!looksTextual(contentType)) {
|
|
172
|
+
const binary = await response.arrayBuffer();
|
|
173
|
+
return {
|
|
174
|
+
content: Buffer.from(binary).toString("base64"),
|
|
175
|
+
contentEncoding: "base64",
|
|
176
|
+
contentLength: binary.byteLength,
|
|
177
|
+
contentType,
|
|
178
|
+
isBinary: true,
|
|
179
|
+
isJson: false,
|
|
180
|
+
itemId: args.itemId,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const textContent = await response.text();
|
|
184
|
+
return {
|
|
185
|
+
content: textContent,
|
|
186
|
+
contentEncoding: "utf8",
|
|
187
|
+
contentLength: textContent.length,
|
|
188
|
+
contentType,
|
|
189
|
+
isBinary: false,
|
|
190
|
+
isJson: false,
|
|
191
|
+
itemId: args.itemId,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async getMe(args = {}) {
|
|
195
|
+
const url = this.#buildUrl("/me");
|
|
196
|
+
const response = await this.#fetchWithRetry({
|
|
197
|
+
operation: "getMe",
|
|
198
|
+
url,
|
|
199
|
+
...(args.hsUser ? { hsUser: args.hsUser } : {}),
|
|
200
|
+
});
|
|
201
|
+
return (await response.json());
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function readResponseBody(response) {
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
if (!text) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(text);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import { ApiError, HighspotClient } from "./api.js";
|
|
3
|
+
import { ConfigError, loadResolvedConfig } from "./config.js";
|
|
4
|
+
import { formatApiError, resolveOutputMode, writeError, writeJson, } from "./output.js";
|
|
5
|
+
export class BaseCommand extends Command {
|
|
6
|
+
outputMode(flags) {
|
|
7
|
+
return resolveOutputMode(flags);
|
|
8
|
+
}
|
|
9
|
+
ensureOutputFlags(flags) {
|
|
10
|
+
if (flags.json && flags.plain) {
|
|
11
|
+
this.fail("--json and --plain cannot be used together", 2, flags);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
ensureVerbosityFlags(flags) {
|
|
15
|
+
if (flags.quiet && flags.verbose) {
|
|
16
|
+
this.fail("--quiet and --verbose cannot be used together", 2, flags);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
clientFromFlags(flags) {
|
|
20
|
+
const config = loadResolvedConfig({
|
|
21
|
+
endpoint: flags.endpoint,
|
|
22
|
+
hsUser: flags["hs-user"],
|
|
23
|
+
maxRetries: flags["max-retries"],
|
|
24
|
+
retryDelayMs: flags["retry-delay-ms"],
|
|
25
|
+
timeoutMs: flags["timeout-ms"],
|
|
26
|
+
});
|
|
27
|
+
return new HighspotClient(config);
|
|
28
|
+
}
|
|
29
|
+
effectiveHsUser(flags) {
|
|
30
|
+
return flags["hs-user"];
|
|
31
|
+
}
|
|
32
|
+
printJson(data) {
|
|
33
|
+
writeJson(data);
|
|
34
|
+
}
|
|
35
|
+
fail(message, exitCode, flags) {
|
|
36
|
+
const mode = this.outputMode(flags);
|
|
37
|
+
writeError(mode, { error: message });
|
|
38
|
+
this.exit(exitCode);
|
|
39
|
+
}
|
|
40
|
+
failWithHint(message, hint, exitCode, flags) {
|
|
41
|
+
const mode = this.outputMode(flags);
|
|
42
|
+
writeError(mode, { error: message, hint });
|
|
43
|
+
this.exit(exitCode);
|
|
44
|
+
}
|
|
45
|
+
handleError(error, flags) {
|
|
46
|
+
const mode = this.outputMode(flags);
|
|
47
|
+
if (error instanceof ApiError) {
|
|
48
|
+
writeError(mode, formatApiError(error));
|
|
49
|
+
this.exit(1);
|
|
50
|
+
}
|
|
51
|
+
if (error instanceof ConfigError) {
|
|
52
|
+
writeError(mode, {
|
|
53
|
+
error: error.message,
|
|
54
|
+
hint: "Set HIGHSPOT_API_KEY_ID and HIGHSPOT_API_KEY_SECRET, or configure .highspot-cli.json.",
|
|
55
|
+
});
|
|
56
|
+
this.exit(2);
|
|
57
|
+
}
|
|
58
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
59
|
+
writeError(mode, { error: message });
|
|
60
|
+
this.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export class ConfigError extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ConfigError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_ENDPOINT = "https://api.highspot.com/v1.0";
|
|
10
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
11
|
+
const DEFAULT_RETRY_DELAY_MS = 1200;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
13
|
+
function readConfigFile(path) {
|
|
14
|
+
if (!existsSync(path)) {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
const raw = readFileSync(path, "utf8").trim();
|
|
18
|
+
if (!raw) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
23
|
+
throw new ConfigError(`Invalid config file at ${path}: expected a JSON object.`);
|
|
24
|
+
}
|
|
25
|
+
return parsed;
|
|
26
|
+
}
|
|
27
|
+
function toInteger(value) {
|
|
28
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
29
|
+
return Math.trunc(value);
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "string" && value.trim()) {
|
|
32
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
33
|
+
if (!Number.isNaN(parsed)) {
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function normalize(config) {
|
|
40
|
+
return {
|
|
41
|
+
endpoint: typeof config.endpoint === "string" ? config.endpoint.trim() : undefined,
|
|
42
|
+
hsUser: typeof config.hsUser === "string" ? config.hsUser.trim() : undefined,
|
|
43
|
+
maxRetries: toInteger(config.maxRetries),
|
|
44
|
+
retryDelayMs: toInteger(config.retryDelayMs),
|
|
45
|
+
timeoutMs: toInteger(config.timeoutMs),
|
|
46
|
+
apiKeyId: typeof config.apiKeyId === "string" ? config.apiKeyId.trim() : undefined,
|
|
47
|
+
apiKeySecret: typeof config.apiKeySecret === "string"
|
|
48
|
+
? config.apiKeySecret.trim()
|
|
49
|
+
: undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function systemConfigPath() {
|
|
53
|
+
if (process.platform === "win32") {
|
|
54
|
+
const programData = process.env.PROGRAMDATA ?? "C:\\ProgramData";
|
|
55
|
+
return join(programData, "highspot-cli", "config.json");
|
|
56
|
+
}
|
|
57
|
+
return "/etc/highspot-cli/config.json";
|
|
58
|
+
}
|
|
59
|
+
function userConfigPath() {
|
|
60
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
61
|
+
if (xdg?.trim()) {
|
|
62
|
+
return join(xdg, "highspot-cli", "config.json");
|
|
63
|
+
}
|
|
64
|
+
const home = process.env.HOME;
|
|
65
|
+
if (!home?.trim()) {
|
|
66
|
+
return join(process.cwd(), ".highspot-cli.user.json");
|
|
67
|
+
}
|
|
68
|
+
return join(home, ".config", "highspot-cli", "config.json");
|
|
69
|
+
}
|
|
70
|
+
function projectConfigPath() {
|
|
71
|
+
return join(process.cwd(), ".highspot-cli.json");
|
|
72
|
+
}
|
|
73
|
+
function fromEnv() {
|
|
74
|
+
return normalize({
|
|
75
|
+
endpoint: process.env.HIGHSPOT_API_ENDPOINT,
|
|
76
|
+
hsUser: process.env.HIGHSPOT_HS_USER,
|
|
77
|
+
maxRetries: process.env.HIGHSPOT_MAX_RETRIES,
|
|
78
|
+
retryDelayMs: process.env.HIGHSPOT_RETRY_DELAY_MS,
|
|
79
|
+
timeoutMs: process.env.HIGHSPOT_TIMEOUT_MS,
|
|
80
|
+
apiKeyId: process.env.HIGHSPOT_API_KEY_ID,
|
|
81
|
+
apiKeySecret: process.env.HIGHSPOT_API_KEY_SECRET,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function overlay(base, next) {
|
|
85
|
+
return {
|
|
86
|
+
...base,
|
|
87
|
+
...Object.fromEntries(Object.entries(next).filter(([, value]) => value !== undefined)),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function loadResolvedConfig(overrides = {}) {
|
|
91
|
+
let config = normalize(readConfigFile(systemConfigPath()));
|
|
92
|
+
config = overlay(config, normalize(readConfigFile(userConfigPath())));
|
|
93
|
+
config = overlay(config, normalize(readConfigFile(projectConfigPath())));
|
|
94
|
+
config = overlay(config, fromEnv());
|
|
95
|
+
config = overlay(config, normalize(overrides));
|
|
96
|
+
const maxRetries = toInteger(config.maxRetries) ?? DEFAULT_MAX_RETRIES;
|
|
97
|
+
const retryDelayMs = toInteger(config.retryDelayMs) ?? DEFAULT_RETRY_DELAY_MS;
|
|
98
|
+
const timeoutMs = toInteger(config.timeoutMs) ?? DEFAULT_TIMEOUT_MS;
|
|
99
|
+
if (!config.apiKeyId) {
|
|
100
|
+
throw new ConfigError("Missing HIGHSPOT_API_KEY_ID. Set env vars or configure apiKeyId in .highspot-cli.json.");
|
|
101
|
+
}
|
|
102
|
+
if (!config.apiKeySecret) {
|
|
103
|
+
throw new ConfigError("Missing HIGHSPOT_API_KEY_SECRET. Set env vars or configure apiKeySecret in .highspot-cli.json.");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
|
107
|
+
hsUser: config.hsUser,
|
|
108
|
+
maxRetries,
|
|
109
|
+
retryDelayMs,
|
|
110
|
+
timeoutMs,
|
|
111
|
+
apiKeyId: config.apiKeyId,
|
|
112
|
+
apiKeySecret: config.apiKeySecret,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Flags } from "@oclif/core";
|
|
2
|
+
export const globalFlags = {
|
|
3
|
+
json: Flags.boolean({
|
|
4
|
+
description: "Output JSON (default)",
|
|
5
|
+
default: false,
|
|
6
|
+
}),
|
|
7
|
+
plain: Flags.boolean({
|
|
8
|
+
description: "Output stable, line-based text",
|
|
9
|
+
default: false,
|
|
10
|
+
}),
|
|
11
|
+
"dry-run": Flags.boolean({
|
|
12
|
+
description: "Print request details and exit without calling the API",
|
|
13
|
+
default: false,
|
|
14
|
+
}),
|
|
15
|
+
"no-input": Flags.boolean({
|
|
16
|
+
description: "Disable any interactive prompts (none are used today)",
|
|
17
|
+
default: false,
|
|
18
|
+
}),
|
|
19
|
+
"no-color": Flags.boolean({
|
|
20
|
+
description: "Disable color output",
|
|
21
|
+
default: false,
|
|
22
|
+
}),
|
|
23
|
+
quiet: Flags.boolean({
|
|
24
|
+
description: "Reduce non-essential output",
|
|
25
|
+
default: false,
|
|
26
|
+
}),
|
|
27
|
+
verbose: Flags.boolean({
|
|
28
|
+
description: "Increase diagnostic output",
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
31
|
+
"hs-user": Flags.string({
|
|
32
|
+
description: "Highspot user context header (hs-user)",
|
|
33
|
+
}),
|
|
34
|
+
endpoint: Flags.string({
|
|
35
|
+
description: "Override Highspot API endpoint",
|
|
36
|
+
}),
|
|
37
|
+
"timeout-ms": Flags.integer({
|
|
38
|
+
description: "HTTP timeout in milliseconds",
|
|
39
|
+
min: 1,
|
|
40
|
+
}),
|
|
41
|
+
"max-retries": Flags.integer({
|
|
42
|
+
description: "Retry attempts for throttled/transient errors",
|
|
43
|
+
min: 0,
|
|
44
|
+
}),
|
|
45
|
+
"retry-delay-ms": Flags.integer({
|
|
46
|
+
description: "Base retry delay in milliseconds",
|
|
47
|
+
min: 1,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
export const searchFlags = {
|
|
51
|
+
limit: Flags.integer({
|
|
52
|
+
description: "Results per page",
|
|
53
|
+
min: 1,
|
|
54
|
+
max: 100,
|
|
55
|
+
}),
|
|
56
|
+
start: Flags.integer({
|
|
57
|
+
description: "Search offset",
|
|
58
|
+
min: 0,
|
|
59
|
+
}),
|
|
60
|
+
"sort-by": Flags.string({
|
|
61
|
+
description: "Sort order",
|
|
62
|
+
options: ["relevancy", "date_added"],
|
|
63
|
+
}),
|
|
64
|
+
"with-fields": Flags.string({
|
|
65
|
+
description: "Comma-separated list of extra fields to return",
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
export const contentFlags = {
|
|
69
|
+
format: Flags.string({
|
|
70
|
+
description: "Optional format query parameter forwarded to /content",
|
|
71
|
+
}),
|
|
72
|
+
start: Flags.string({
|
|
73
|
+
description: "Optional cursor/start token for paginated content",
|
|
74
|
+
}),
|
|
75
|
+
};
|
package/dist/lib/help.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Help as OclifHelp } from "@oclif/core";
|
|
2
|
+
const AUTH_TEXT = "Requires HIGHSPOT_API_KEY_ID and HIGHSPOT_API_KEY_SECRET via env or config file.";
|
|
3
|
+
const CONFIG_TEXT = "Precedence: flags > env > project config (.highspot-cli.json) > user config (~/.config/highspot-cli/config.json) > system config.";
|
|
4
|
+
export default class Help extends OclifHelp {
|
|
5
|
+
formatRoot() {
|
|
6
|
+
const base = super.formatRoot();
|
|
7
|
+
const auth = this.section("AUTH", AUTH_TEXT);
|
|
8
|
+
const config = this.section("CONFIG", CONFIG_TEXT);
|
|
9
|
+
const examples = this.section("EXAMPLES", this.renderList([
|
|
10
|
+
["highspot --help"],
|
|
11
|
+
['highspot search "GoGuardian Teacher" --limit 5'],
|
|
12
|
+
["highspot item it_abc123 --plain"],
|
|
13
|
+
["highspot content it_abc123 --format text/plain --plain"],
|
|
14
|
+
["highspot me --json"],
|
|
15
|
+
], { indentation: 2, spacer: "\n", stripAnsi: this.opts.stripAnsi }));
|
|
16
|
+
return `${base}\n\n${auth}\n\n${config}\n\n${examples}`;
|
|
17
|
+
}
|
|
18
|
+
formatCommand(command) {
|
|
19
|
+
const base = super.formatCommand(command);
|
|
20
|
+
const auth = this.section("AUTH", AUTH_TEXT);
|
|
21
|
+
return `${base}\n${auth}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function resolveOutputMode(flags) {
|
|
2
|
+
return flags.plain ? "plain" : "json";
|
|
3
|
+
}
|
|
4
|
+
export function writeJson(data) {
|
|
5
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
6
|
+
}
|
|
7
|
+
export function writeError(mode, error) {
|
|
8
|
+
if (mode === "plain") {
|
|
9
|
+
process.stderr.write(`${error.error}\n`);
|
|
10
|
+
if (error.hint) {
|
|
11
|
+
process.stderr.write(`hint: ${error.hint}\n`);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
process.stderr.write(`${JSON.stringify(error, null, 2)}\n`);
|
|
16
|
+
}
|
|
17
|
+
export function formatApiError(err) {
|
|
18
|
+
if (typeof err.body === "object" && err.body !== null) {
|
|
19
|
+
return {
|
|
20
|
+
error: "API error",
|
|
21
|
+
status: err.status,
|
|
22
|
+
details: err.body,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
error: err.body ? String(err.body) : err.message,
|
|
27
|
+
status: err.status,
|
|
28
|
+
};
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "highspot-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Agent-first CLI for the Highspot API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Advait Shinde",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/advait/highspot-cli.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/advait/highspot-cli/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/advait/highspot-cli#readme",
|
|
16
|
+
"bin": {
|
|
17
|
+
"highspot": "dist/bin/highspot.js",
|
|
18
|
+
"highspot-cli": "dist/bin/highspot.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.build.json",
|
|
22
|
+
"dev": "npm run build && node dist/bin/highspot.js",
|
|
23
|
+
"lint": "biome check .",
|
|
24
|
+
"format": "biome format --write .",
|
|
25
|
+
"check": "npm run typecheck && npm run lint",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.10.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@oclif/core": "^4.8.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "^2.4.4",
|
|
37
|
+
"@types/node": "^25.0.3",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
},
|
|
40
|
+
"oclif": {
|
|
41
|
+
"commands": "./dist/commands",
|
|
42
|
+
"bin": "highspot",
|
|
43
|
+
"additionalHelpFlags": [
|
|
44
|
+
"-h"
|
|
45
|
+
],
|
|
46
|
+
"additionalVersionFlags": [
|
|
47
|
+
"-v"
|
|
48
|
+
],
|
|
49
|
+
"helpClass": "./dist/lib/help.js"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE",
|
|
55
|
+
"package.json"
|
|
56
|
+
],
|
|
57
|
+
"keywords": [
|
|
58
|
+
"highspot",
|
|
59
|
+
"cli",
|
|
60
|
+
"oclif",
|
|
61
|
+
"sales-enablement"
|
|
62
|
+
]
|
|
63
|
+
}
|