highspot-cli 0.1.1 → 0.2.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/README.md +29 -4
- package/dist/commands/get.js +263 -0
- package/dist/lib/api.js +5 -6
- package/dist/lib/command.js +4 -1
- package/dist/lib/config.js +34 -8
- package/dist/lib/flags.js +16 -0
- package/dist/lib/help.js +3 -3
- package/package.json +2 -2
- package/dist/commands/content.js +0 -70
- package/dist/commands/item.js +0 -66
package/README.md
CHANGED
|
@@ -31,6 +31,12 @@ export HIGHSPOT_API_KEY_ID=hs_key_id_xxx
|
|
|
31
31
|
export HIGHSPOT_API_KEY_SECRET=hs_key_secret_xxx
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Or provide a precomputed Basic auth header directly:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export HIGHSPOT_BASIC_AUTH="Basic <base64(id:secret)>"
|
|
38
|
+
```
|
|
39
|
+
|
|
34
40
|
Optional:
|
|
35
41
|
|
|
36
42
|
```bash
|
|
@@ -44,6 +50,10 @@ It is not implied by the API key:
|
|
|
44
50
|
- `hs-user` sets an explicit user context for requests where impersonation is needed.
|
|
45
51
|
- CLI flag precedence still applies, so `--hs-user` overrides `HIGHSPOT_HS_USER`.
|
|
46
52
|
|
|
53
|
+
Auth precedence:
|
|
54
|
+
- `HIGHSPOT_BASIC_AUTH` is used directly when set.
|
|
55
|
+
- Otherwise, `HIGHSPOT_API_KEY_ID` + `HIGHSPOT_API_KEY_SECRET` are used to compute `Authorization: Basic ...`.
|
|
56
|
+
|
|
47
57
|
## Config Files
|
|
48
58
|
|
|
49
59
|
Config precedence (highest to lowest):
|
|
@@ -63,6 +73,7 @@ Example `.highspot-cli.json`:
|
|
|
63
73
|
"maxRetries": 3,
|
|
64
74
|
"retryDelayMs": 1200,
|
|
65
75
|
"timeoutMs": 30000,
|
|
76
|
+
"basicAuth": "Basic <base64(id:secret)>",
|
|
66
77
|
"apiKeyId": "hs_key_id_xxx",
|
|
67
78
|
"apiKeySecret": "hs_key_secret_xxx"
|
|
68
79
|
}
|
|
@@ -72,8 +83,7 @@ Example `.highspot-cli.json`:
|
|
|
72
83
|
|
|
73
84
|
```bash
|
|
74
85
|
highspot search <query>
|
|
75
|
-
highspot
|
|
76
|
-
highspot content <item-id>
|
|
86
|
+
highspot get <item-id>
|
|
77
87
|
highspot me
|
|
78
88
|
```
|
|
79
89
|
|
|
@@ -94,6 +104,15 @@ Global flags:
|
|
|
94
104
|
- `--no-input`
|
|
95
105
|
- `--no-color`
|
|
96
106
|
|
|
107
|
+
`get` command flags:
|
|
108
|
+
|
|
109
|
+
- `--format <value>`
|
|
110
|
+
- `--start <value>`
|
|
111
|
+
- `--meta-only` (skip content download)
|
|
112
|
+
- `-o, --output <path>` (explicit file path)
|
|
113
|
+
- `--output-dir <path>` (directory for auto-saved binary files)
|
|
114
|
+
- `-f, --force` (overwrite existing output file)
|
|
115
|
+
|
|
97
116
|
Exit codes:
|
|
98
117
|
|
|
99
118
|
- `0` success
|
|
@@ -105,8 +124,11 @@ Exit codes:
|
|
|
105
124
|
```bash
|
|
106
125
|
highspot search "GoGuardian Teacher" --limit 10
|
|
107
126
|
highspot search "Beacon" --sort-by date_added --plain
|
|
108
|
-
highspot
|
|
109
|
-
highspot
|
|
127
|
+
highspot get it_abc123 --meta-only
|
|
128
|
+
highspot get it_abc123 --format text/plain --plain
|
|
129
|
+
highspot get it_abc123
|
|
130
|
+
highspot get it_abc123 --output ./custom-filename.pdf
|
|
131
|
+
highspot get it_abc123 --output-dir ./downloads
|
|
110
132
|
highspot me --json
|
|
111
133
|
highspot search "Fleet" --dry-run
|
|
112
134
|
```
|
|
@@ -115,6 +137,9 @@ Behavior notes:
|
|
|
115
137
|
|
|
116
138
|
- Prompts are not used; `--no-input` is accepted for automation consistency.
|
|
117
139
|
- Primary data goes to stdout, errors go to stderr.
|
|
140
|
+
- `get` always fetches `/items/{id}` metadata first, then fetches `/items/{id}/content` unless `--meta-only` is set.
|
|
141
|
+
- Binary content is automatically saved to disk using Highspot `content_name` (canonical filename) when available.
|
|
142
|
+
- Use `--output` to force a specific filename/path, or `--output-dir` to control where auto-saved binaries are written.
|
|
118
143
|
|
|
119
144
|
## Development
|
|
120
145
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
5
|
+
import { Args } from "@oclif/core";
|
|
6
|
+
import { BaseCommand } from "../lib/command.js";
|
|
7
|
+
import { contentFlags, globalFlags } from "../lib/flags.js";
|
|
8
|
+
export default class Get extends BaseCommand {
|
|
9
|
+
static description = "Fetch item metadata and content in one command";
|
|
10
|
+
static examples = [
|
|
11
|
+
"highspot get it_abc123",
|
|
12
|
+
"highspot get it_abc123 --meta-only",
|
|
13
|
+
"highspot get it_abc123 --output ./discover-guide.pdf",
|
|
14
|
+
"highspot get it_abc123 --output-dir ./downloads",
|
|
15
|
+
"highspot get it_abc123 --format text/plain --plain",
|
|
16
|
+
];
|
|
17
|
+
static args = {
|
|
18
|
+
itemId: Args.string({
|
|
19
|
+
description: "Highspot item id",
|
|
20
|
+
required: false,
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
static flags = {
|
|
24
|
+
...globalFlags,
|
|
25
|
+
...contentFlags,
|
|
26
|
+
};
|
|
27
|
+
async run() {
|
|
28
|
+
const { args, flags } = await this.parse(Get);
|
|
29
|
+
this.ensureOutputFlags(flags);
|
|
30
|
+
this.ensureVerbosityFlags(flags);
|
|
31
|
+
if (!args.itemId) {
|
|
32
|
+
this.fail("itemId is required", 2, flags);
|
|
33
|
+
}
|
|
34
|
+
const payload = {
|
|
35
|
+
operations: [
|
|
36
|
+
{ endpoint: `/items/${encodeURIComponent(args.itemId)}` },
|
|
37
|
+
...(flags["meta-only"]
|
|
38
|
+
? []
|
|
39
|
+
: [
|
|
40
|
+
{
|
|
41
|
+
endpoint: `/items/${encodeURIComponent(args.itemId)}/content`,
|
|
42
|
+
query: {
|
|
43
|
+
...(flags.format ? { format: flags.format } : {}),
|
|
44
|
+
...(flags.start ? { start: flags.start } : {}),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
]),
|
|
48
|
+
],
|
|
49
|
+
output: {
|
|
50
|
+
...(flags.output ? { output: flags.output } : {}),
|
|
51
|
+
...(flags["output-dir"] ? { outputDir: flags["output-dir"] } : {}),
|
|
52
|
+
force: flags.force,
|
|
53
|
+
},
|
|
54
|
+
headers: {
|
|
55
|
+
...(flags["hs-user"] ? { "hs-user": flags["hs-user"] } : {}),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
if (flags["dry-run"]) {
|
|
59
|
+
this.printJson(payload);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const client = this.clientFromFlags(flags);
|
|
64
|
+
const itemData = await client.getItem({
|
|
65
|
+
itemId: args.itemId,
|
|
66
|
+
hsUser: this.effectiveHsUser(flags),
|
|
67
|
+
});
|
|
68
|
+
const item = asItemRecord(itemData.item);
|
|
69
|
+
if (flags["meta-only"]) {
|
|
70
|
+
if (this.outputMode(flags) === "plain") {
|
|
71
|
+
writeItemPlain(item);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.printJson({ item });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const contentData = await client.getItemContent({
|
|
78
|
+
itemId: args.itemId,
|
|
79
|
+
format: flags.format,
|
|
80
|
+
start: flags.start,
|
|
81
|
+
hsUser: this.effectiveHsUser(flags),
|
|
82
|
+
});
|
|
83
|
+
const explicitOutputPath = flags.output
|
|
84
|
+
? resolve(process.cwd(), flags.output)
|
|
85
|
+
: undefined;
|
|
86
|
+
if (contentData.kind === "binary") {
|
|
87
|
+
const binaryOutputPath = explicitOutputPath ??
|
|
88
|
+
resolveBinaryOutputPath({
|
|
89
|
+
outputDir: flags["output-dir"],
|
|
90
|
+
item,
|
|
91
|
+
itemId: args.itemId,
|
|
92
|
+
contentType: contentData.contentType,
|
|
93
|
+
});
|
|
94
|
+
const fileResult = await writeBytesToFile({
|
|
95
|
+
bytes: contentData.bytes,
|
|
96
|
+
contentType: contentData.contentType,
|
|
97
|
+
force: flags.force,
|
|
98
|
+
item,
|
|
99
|
+
itemId: args.itemId,
|
|
100
|
+
outputPath: binaryOutputPath,
|
|
101
|
+
});
|
|
102
|
+
if (this.outputMode(flags) === "plain") {
|
|
103
|
+
writeFileResultPlain(fileResult);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.printJson(fileResult);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (explicitOutputPath) {
|
|
110
|
+
const fileResult = await writeInlineContentToFile({
|
|
111
|
+
content: contentData,
|
|
112
|
+
force: flags.force,
|
|
113
|
+
item,
|
|
114
|
+
itemId: args.itemId,
|
|
115
|
+
outputPath: explicitOutputPath,
|
|
116
|
+
});
|
|
117
|
+
if (this.outputMode(flags) === "plain") {
|
|
118
|
+
writeFileResultPlain(fileResult);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.printJson(fileResult);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (this.outputMode(flags) === "plain") {
|
|
125
|
+
writeInlineContentPlain(contentData);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.printJson({
|
|
129
|
+
item,
|
|
130
|
+
mode: "inline",
|
|
131
|
+
isBinary: false,
|
|
132
|
+
contentType: contentData.contentType,
|
|
133
|
+
content: contentData.content,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
this.handleError(error, flags);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function asItemRecord(value) {
|
|
142
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
function writeItemPlain(item) {
|
|
148
|
+
const id = asString(item.id);
|
|
149
|
+
const url = asString(item.url);
|
|
150
|
+
const title = asString(item.title);
|
|
151
|
+
const contentType = asString(item.content_type);
|
|
152
|
+
const contentName = asString(item.content_name);
|
|
153
|
+
process.stdout.write(`${id}\t${url}\t${title}\t${contentType}\t${contentName}\n`);
|
|
154
|
+
}
|
|
155
|
+
function writeInlineContentPlain(content) {
|
|
156
|
+
if (content.kind === "text") {
|
|
157
|
+
process.stdout.write(content.content);
|
|
158
|
+
if (!content.content.endsWith("\n")) {
|
|
159
|
+
process.stdout.write("\n");
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (content.kind === "json") {
|
|
164
|
+
process.stdout.write(`${JSON.stringify(content.content, null, 2)}\n`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
throw new Error("Binary content must be written to a file.");
|
|
168
|
+
}
|
|
169
|
+
function writeFileResultPlain(result) {
|
|
170
|
+
process.stdout.write(`${result.itemId}\t${result.outputPath}\t${result.contentType}\t${result.sizeBytes}\t${result.sha256}\n`);
|
|
171
|
+
}
|
|
172
|
+
function resolveBinaryOutputPath(args) {
|
|
173
|
+
const outputDir = args.outputDir
|
|
174
|
+
? resolve(process.cwd(), args.outputDir)
|
|
175
|
+
: process.cwd();
|
|
176
|
+
const fileName = preferredBinaryFileName(args.item, args.itemId, args.contentType);
|
|
177
|
+
return join(outputDir, fileName);
|
|
178
|
+
}
|
|
179
|
+
function preferredBinaryFileName(item, itemId, contentType) {
|
|
180
|
+
const candidates = [
|
|
181
|
+
item.content_name,
|
|
182
|
+
item.filename,
|
|
183
|
+
item.file_name,
|
|
184
|
+
item.name,
|
|
185
|
+
item.title,
|
|
186
|
+
]
|
|
187
|
+
.map((value) => asString(value).trim())
|
|
188
|
+
.filter((value) => value.length > 0);
|
|
189
|
+
const baseCandidate = candidates[0] ?? itemId;
|
|
190
|
+
const safeBase = sanitizeFileName(baseCandidate) || itemId;
|
|
191
|
+
if (extname(safeBase)) {
|
|
192
|
+
return safeBase;
|
|
193
|
+
}
|
|
194
|
+
const extension = extensionFromContentType(contentType) ?? "bin";
|
|
195
|
+
return `${safeBase}.${extension}`;
|
|
196
|
+
}
|
|
197
|
+
function sanitizeFileName(input) {
|
|
198
|
+
const invalidChars = new Set(["\\", "/", ":", "*", "?", '"', "<", ">", "|"]);
|
|
199
|
+
let output = "";
|
|
200
|
+
for (const char of input) {
|
|
201
|
+
const code = char.charCodeAt(0);
|
|
202
|
+
if (code >= 0 && code < 32) {
|
|
203
|
+
output += "_";
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
output += invalidChars.has(char) ? "_" : char;
|
|
207
|
+
}
|
|
208
|
+
return output.replace(/\s+/g, " ").trim();
|
|
209
|
+
}
|
|
210
|
+
function extensionFromContentType(contentType) {
|
|
211
|
+
const normalized = contentType.toLowerCase();
|
|
212
|
+
if (normalized.includes("pdf")) {
|
|
213
|
+
return "pdf";
|
|
214
|
+
}
|
|
215
|
+
if (normalized.includes("json")) {
|
|
216
|
+
return "json";
|
|
217
|
+
}
|
|
218
|
+
if (normalized.includes("csv")) {
|
|
219
|
+
return "csv";
|
|
220
|
+
}
|
|
221
|
+
if (normalized.startsWith("text/")) {
|
|
222
|
+
return "txt";
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
async function writeBytesToFile(args) {
|
|
227
|
+
await ensureWriteablePath(args.outputPath, args.force);
|
|
228
|
+
await writeFile(args.outputPath, args.bytes);
|
|
229
|
+
return {
|
|
230
|
+
contentType: args.contentType,
|
|
231
|
+
item: args.item,
|
|
232
|
+
itemId: args.itemId,
|
|
233
|
+
mode: "file",
|
|
234
|
+
outputPath: args.outputPath,
|
|
235
|
+
sha256: createHash("sha256").update(args.bytes).digest("hex"),
|
|
236
|
+
sizeBytes: args.bytes.byteLength,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
async function writeInlineContentToFile(args) {
|
|
240
|
+
await ensureWriteablePath(args.outputPath, args.force);
|
|
241
|
+
const text = args.content.kind === "text"
|
|
242
|
+
? args.content.content
|
|
243
|
+
: `${JSON.stringify(args.content.content, null, 2)}\n`;
|
|
244
|
+
await writeFile(args.outputPath, text, "utf8");
|
|
245
|
+
return {
|
|
246
|
+
contentType: args.content.contentType,
|
|
247
|
+
item: args.item,
|
|
248
|
+
itemId: args.itemId,
|
|
249
|
+
mode: "file",
|
|
250
|
+
outputPath: args.outputPath,
|
|
251
|
+
sha256: createHash("sha256").update(text, "utf8").digest("hex"),
|
|
252
|
+
sizeBytes: Buffer.byteLength(text, "utf8"),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
async function ensureWriteablePath(path, force) {
|
|
256
|
+
await mkdir(dirname(path), { recursive: true });
|
|
257
|
+
if (existsSync(path) && !force) {
|
|
258
|
+
throw new Error(`Output file already exists: ${path}. Use --force to overwrite.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function asString(value) {
|
|
262
|
+
return typeof value === "string" ? value : "";
|
|
263
|
+
}
|
package/dist/lib/api.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
1
|
const TEXTUAL_CONTENT_TYPE_HINTS = [
|
|
3
2
|
"application/xml",
|
|
4
3
|
"application/xhtml+xml",
|
|
@@ -40,7 +39,7 @@ export class HighspotClient {
|
|
|
40
39
|
#retryDelayMs;
|
|
41
40
|
#timeoutMs;
|
|
42
41
|
constructor(config) {
|
|
43
|
-
this.#authorizationHeader =
|
|
42
|
+
this.#authorizationHeader = config.authorizationHeader;
|
|
44
43
|
this.#endpoint = sanitizeEndpoint(config.endpoint);
|
|
45
44
|
this.#hsUser = config.hsUser;
|
|
46
45
|
this.#maxRetries = config.maxRetries;
|
|
@@ -160,8 +159,8 @@ export class HighspotClient {
|
|
|
160
159
|
if (contentType.includes("json")) {
|
|
161
160
|
const payload = (await response.json());
|
|
162
161
|
return {
|
|
162
|
+
kind: "json",
|
|
163
163
|
content: payload,
|
|
164
|
-
contentEncoding: "json",
|
|
165
164
|
contentType,
|
|
166
165
|
isBinary: false,
|
|
167
166
|
isJson: true,
|
|
@@ -171,8 +170,8 @@ export class HighspotClient {
|
|
|
171
170
|
if (!looksTextual(contentType)) {
|
|
172
171
|
const binary = await response.arrayBuffer();
|
|
173
172
|
return {
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
kind: "binary",
|
|
174
|
+
bytes: new Uint8Array(binary),
|
|
176
175
|
contentLength: binary.byteLength,
|
|
177
176
|
contentType,
|
|
178
177
|
isBinary: true,
|
|
@@ -182,8 +181,8 @@ export class HighspotClient {
|
|
|
182
181
|
}
|
|
183
182
|
const textContent = await response.text();
|
|
184
183
|
return {
|
|
184
|
+
kind: "text",
|
|
185
185
|
content: textContent,
|
|
186
|
-
contentEncoding: "utf8",
|
|
187
186
|
contentLength: textContent.length,
|
|
188
187
|
contentType,
|
|
189
188
|
isBinary: false,
|
package/dist/lib/command.js
CHANGED
|
@@ -43,6 +43,9 @@ export class BaseCommand extends Command {
|
|
|
43
43
|
this.exit(exitCode);
|
|
44
44
|
}
|
|
45
45
|
handleError(error, flags) {
|
|
46
|
+
if (error instanceof Error && /^EEXIT:\s*\d+/.test(error.message)) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
46
49
|
const mode = this.outputMode(flags);
|
|
47
50
|
if (error instanceof ApiError) {
|
|
48
51
|
writeError(mode, formatApiError(error));
|
|
@@ -51,7 +54,7 @@ export class BaseCommand extends Command {
|
|
|
51
54
|
if (error instanceof ConfigError) {
|
|
52
55
|
writeError(mode, {
|
|
53
56
|
error: error.message,
|
|
54
|
-
hint: "Set HIGHSPOT_API_KEY_ID and HIGHSPOT_API_KEY_SECRET, or configure .highspot-cli.json.",
|
|
57
|
+
hint: "Set HIGHSPOT_BASIC_AUTH, or set HIGHSPOT_API_KEY_ID and HIGHSPOT_API_KEY_SECRET, or configure .highspot-cli.json.",
|
|
55
58
|
});
|
|
56
59
|
this.exit(2);
|
|
57
60
|
}
|
package/dist/lib/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
export class ConfigError extends Error {
|
|
@@ -43,6 +44,9 @@ function normalize(config) {
|
|
|
43
44
|
maxRetries: toInteger(config.maxRetries),
|
|
44
45
|
retryDelayMs: toInteger(config.retryDelayMs),
|
|
45
46
|
timeoutMs: toInteger(config.timeoutMs),
|
|
47
|
+
basicAuth: typeof config.basicAuth === "string"
|
|
48
|
+
? config.basicAuth.trim()
|
|
49
|
+
: undefined,
|
|
46
50
|
apiKeyId: typeof config.apiKeyId === "string" ? config.apiKeyId.trim() : undefined,
|
|
47
51
|
apiKeySecret: typeof config.apiKeySecret === "string"
|
|
48
52
|
? config.apiKeySecret.trim()
|
|
@@ -77,10 +81,38 @@ function fromEnv() {
|
|
|
77
81
|
maxRetries: process.env.HIGHSPOT_MAX_RETRIES,
|
|
78
82
|
retryDelayMs: process.env.HIGHSPOT_RETRY_DELAY_MS,
|
|
79
83
|
timeoutMs: process.env.HIGHSPOT_TIMEOUT_MS,
|
|
84
|
+
basicAuth: process.env.HIGHSPOT_BASIC_AUTH,
|
|
80
85
|
apiKeyId: process.env.HIGHSPOT_API_KEY_ID,
|
|
81
86
|
apiKeySecret: process.env.HIGHSPOT_API_KEY_SECRET,
|
|
82
87
|
});
|
|
83
88
|
}
|
|
89
|
+
function normalizeAuthorizationHeader(value) {
|
|
90
|
+
const trimmed = value?.trim();
|
|
91
|
+
if (!trimmed) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (/^Basic\s+/i.test(trimmed)) {
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
return `Basic ${trimmed}`;
|
|
98
|
+
}
|
|
99
|
+
function resolveAuthorizationHeader(config) {
|
|
100
|
+
const explicitBasicAuth = normalizeAuthorizationHeader(config.basicAuth);
|
|
101
|
+
if (explicitBasicAuth) {
|
|
102
|
+
return explicitBasicAuth;
|
|
103
|
+
}
|
|
104
|
+
if (!config.apiKeyId && !config.apiKeySecret) {
|
|
105
|
+
throw new ConfigError("Missing auth configuration. Set HIGHSPOT_BASIC_AUTH, or set both HIGHSPOT_API_KEY_ID and HIGHSPOT_API_KEY_SECRET.");
|
|
106
|
+
}
|
|
107
|
+
if (!config.apiKeyId) {
|
|
108
|
+
throw new ConfigError("Missing HIGHSPOT_API_KEY_ID. Set HIGHSPOT_BASIC_AUTH, or configure apiKeyId with apiKeySecret in .highspot-cli.json.");
|
|
109
|
+
}
|
|
110
|
+
if (!config.apiKeySecret) {
|
|
111
|
+
throw new ConfigError("Missing HIGHSPOT_API_KEY_SECRET. Set HIGHSPOT_BASIC_AUTH, or configure apiKeySecret with apiKeyId in .highspot-cli.json.");
|
|
112
|
+
}
|
|
113
|
+
const token = Buffer.from(`${config.apiKeyId}:${config.apiKeySecret}`).toString("base64");
|
|
114
|
+
return `Basic ${token}`;
|
|
115
|
+
}
|
|
84
116
|
function overlay(base, next) {
|
|
85
117
|
return {
|
|
86
118
|
...base,
|
|
@@ -96,19 +128,13 @@ export function loadResolvedConfig(overrides = {}) {
|
|
|
96
128
|
const maxRetries = toInteger(config.maxRetries) ?? DEFAULT_MAX_RETRIES;
|
|
97
129
|
const retryDelayMs = toInteger(config.retryDelayMs) ?? DEFAULT_RETRY_DELAY_MS;
|
|
98
130
|
const timeoutMs = toInteger(config.timeoutMs) ?? DEFAULT_TIMEOUT_MS;
|
|
99
|
-
|
|
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
|
-
}
|
|
131
|
+
const authorizationHeader = resolveAuthorizationHeader(config);
|
|
105
132
|
return {
|
|
106
133
|
endpoint: config.endpoint || DEFAULT_ENDPOINT,
|
|
107
134
|
hsUser: config.hsUser,
|
|
108
135
|
maxRetries,
|
|
109
136
|
retryDelayMs,
|
|
110
137
|
timeoutMs,
|
|
111
|
-
|
|
112
|
-
apiKeySecret: config.apiKeySecret,
|
|
138
|
+
authorizationHeader,
|
|
113
139
|
};
|
|
114
140
|
}
|
package/dist/lib/flags.js
CHANGED
|
@@ -72,4 +72,20 @@ export const contentFlags = {
|
|
|
72
72
|
start: Flags.string({
|
|
73
73
|
description: "Optional cursor/start token for paginated content",
|
|
74
74
|
}),
|
|
75
|
+
output: Flags.string({
|
|
76
|
+
char: "o",
|
|
77
|
+
description: "Write content body to a specific file path",
|
|
78
|
+
}),
|
|
79
|
+
"output-dir": Flags.string({
|
|
80
|
+
description: "Directory for auto-saved binary content (default: current working directory)",
|
|
81
|
+
}),
|
|
82
|
+
"meta-only": Flags.boolean({
|
|
83
|
+
description: "Return item metadata only (skip content download)",
|
|
84
|
+
default: false,
|
|
85
|
+
}),
|
|
86
|
+
force: Flags.boolean({
|
|
87
|
+
char: "f",
|
|
88
|
+
description: "Allow overwriting an existing output file",
|
|
89
|
+
default: false,
|
|
90
|
+
}),
|
|
75
91
|
};
|
package/dist/lib/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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.";
|
|
2
|
+
const AUTH_TEXT = "Requires HIGHSPOT_BASIC_AUTH, or HIGHSPOT_API_KEY_ID and HIGHSPOT_API_KEY_SECRET, via env or config file.";
|
|
3
3
|
const CONFIG_TEXT = "Precedence: flags > env > project config (.highspot-cli.json) > user config (~/.config/highspot-cli/config.json) > system config.";
|
|
4
4
|
export default class Help extends OclifHelp {
|
|
5
5
|
formatRoot() {
|
|
@@ -9,8 +9,8 @@ export default class Help extends OclifHelp {
|
|
|
9
9
|
const examples = this.section("EXAMPLES", this.renderList([
|
|
10
10
|
["highspot --help"],
|
|
11
11
|
['highspot search "GoGuardian Teacher" --limit 5'],
|
|
12
|
-
["highspot
|
|
13
|
-
["highspot
|
|
12
|
+
["highspot get it_abc123 --meta-only"],
|
|
13
|
+
["highspot get it_abc123 --output ./discover-guide.pdf"],
|
|
14
14
|
["highspot me --json"],
|
|
15
15
|
], { indentation: 2, spacer: "\n", stripAnsi: this.opts.stripAnsi }));
|
|
16
16
|
return `${base}\n\n${auth}\n\n${config}\n\n${examples}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "highspot-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Agent-first CLI for the Highspot API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Advait Shinde",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"highspot-cli": "dist/bin/highspot.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
|
|
22
22
|
"dev": "npm run build && node dist/bin/highspot.js",
|
|
23
23
|
"lint": "biome check .",
|
|
24
24
|
"format": "biome format --write .",
|
package/dist/commands/content.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
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
|
-
}
|
package/dist/commands/item.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|