insightfulpipe 0.1.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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/SECURITY.md +23 -0
- package/bin/cli.js +13 -0
- package/package.json +38 -0
- package/src/api.js +217 -0
- package/src/cli/commands/auth.js +30 -0
- package/src/cli/commands/config.js +62 -0
- package/src/cli/commands/discovery.js +353 -0
- package/src/cli/commands/execution.js +50 -0
- package/src/cli/errors.js +22 -0
- package/src/cli/input.js +139 -0
- package/src/cli/options.js +88 -0
- package/src/cli/program.js +48 -0
- package/src/cli/query-contexts.js +222 -0
- package/src/config.js +194 -0
- package/src/keyring.js +65 -0
- package/src/output.js +33 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Refactored the CLI into modular command registration under `src/cli/`
|
|
6
|
+
- Split shared CLI helpers into focused modules for errors, input, options, and query-context workflows
|
|
7
|
+
- Aligned command names with the server surface using `query_data`, `execute_action`, and `query_contexts`
|
|
8
|
+
- Added workspace, brand, prompt, doctor, and discovery commands
|
|
9
|
+
- Added environment variable overrides and configurable request timeouts
|
|
10
|
+
- Hardened API error handling for invalid URLs, transport failures, request timeouts, and HTML error pages
|
|
11
|
+
- Added automated tests for routing, validation, confirmation behavior, JSON output, health checks, and timeout handling
|
|
12
|
+
- Added opt-in live smoke tests and repository documentation
|
|
13
|
+
- Added a CI workflow for automated test and package verification
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) InsightfulPipe
|
|
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,140 @@
|
|
|
1
|
+
# InsightfulPipe CLI
|
|
2
|
+
|
|
3
|
+
Terminal access to InsightfulPipe raw APIs.
|
|
4
|
+
|
|
5
|
+
Recommended workflow:
|
|
6
|
+
|
|
7
|
+
1. Discover accounts and metadata with `query_contexts`
|
|
8
|
+
2. Inspect supported actions with `query_contexts available_actions`
|
|
9
|
+
3. Inspect exact request bodies with `query_contexts actions_details`
|
|
10
|
+
4. Execute read operations with `query_data`
|
|
11
|
+
5. Execute write operations with `execute_action`
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Node.js 18+
|
|
16
|
+
- An InsightfulPipe API token starting with `ip_sk_`
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run locally with:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node bin/cli.js --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authentication
|
|
31
|
+
|
|
32
|
+
Store a token in `~/.insightfulpipe/config.json`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node bin/cli.js auth
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You can also pipe the token instead of typing it interactively:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
printf '%s\n' "$INSIGHTFULPIPE_TOKEN" | node bin/cli.js auth
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The CLI also reads these environment variables:
|
|
45
|
+
|
|
46
|
+
- `INSIGHTFULPIPE_TOKEN`
|
|
47
|
+
- `INSIGHTFULPIPE_API_URL`
|
|
48
|
+
- `INSIGHTFULPIPE_TIMEOUT_MS`
|
|
49
|
+
|
|
50
|
+
## Core Commands
|
|
51
|
+
|
|
52
|
+
### Discovery
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
node bin/cli.js whoami
|
|
56
|
+
node bin/cli.js platforms
|
|
57
|
+
node bin/cli.js workspaces
|
|
58
|
+
node bin/cli.js accounts --platform google-analytics
|
|
59
|
+
node bin/cli.js sources --platform google-search-console
|
|
60
|
+
node bin/cli.js brands --workspace 34
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Query Workflow
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node bin/cli.js query_contexts accounts
|
|
67
|
+
node bin/cli.js query_contexts available_actions --platform google-analytics
|
|
68
|
+
node bin/cli.js query_contexts actions_details --platform google-analytics --actions get_report
|
|
69
|
+
node bin/cli.js query_data google-analytics --body '{"action":"get_report","workspace_id":34,"brand_id":343,"property_id":"510157516","dimensions":["date"],"metrics":["sessions"],"start_date":"2026-03-01","end_date":"2026-03-07"}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Write operations
|
|
73
|
+
|
|
74
|
+
`execute_action` is the canonical write command. The shorter `action` alias is still available.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
node bin/cli.js execute_action facebook-pages --file payload.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
In non-interactive environments, pass `--yes` to confirm explicitly:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
node bin/cli.js execute_action facebook-pages --file payload.json --yes
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Operational Checks
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node bin/cli.js doctor
|
|
90
|
+
node bin/cli.js --json doctor
|
|
91
|
+
node bin/cli.js --json config
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Prompts
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
node bin/cli.js prompts google-ads
|
|
98
|
+
node bin/cli.js prompts google-ads --id 12
|
|
99
|
+
node bin/cli.js prompts google-ads --custom --workspace 34 --id 18
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
Show the current config:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
node bin/cli.js config
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Set a custom API base URL:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
node bin/cli.js config --set-url https://app.insightfulpipe.com
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Local HTTP endpoints are allowed only with `--allow-insecure`.
|
|
117
|
+
|
|
118
|
+
Set a request timeout:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node bin/cli.js config --set-timeout 15000
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use `--json` with discovery, configuration, and health commands when you want machine-readable output.
|
|
125
|
+
|
|
126
|
+
## Testing
|
|
127
|
+
|
|
128
|
+
Run the local test suite:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm test
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Run opt-in live smoke tests against the real backend:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
INSIGHTFULPIPE_LIVE_TOKEN=ip_sk_xxx npm run test:live
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The live smoke test creates an isolated temporary home directory and does not overwrite your real `~/.insightfulpipe` config.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Scope
|
|
4
|
+
|
|
5
|
+
This repository contains the local CLI only. Backend authorization, platform permissions, and data access are enforced by the InsightfulPipe API.
|
|
6
|
+
|
|
7
|
+
## Reporting a Vulnerability
|
|
8
|
+
|
|
9
|
+
Report security issues privately to the InsightfulPipe team. Do not open public issues for token leakage, privilege escalation, SSRF, authentication bypass, or sensitive data exposure.
|
|
10
|
+
|
|
11
|
+
## Token Handling
|
|
12
|
+
|
|
13
|
+
- API tokens are stored in `~/.insightfulpipe/config.json`
|
|
14
|
+
- `INSIGHTFULPIPE_TOKEN` can be used instead of storing a token on disk
|
|
15
|
+
- Interactive `auth` input is hidden from terminal echo
|
|
16
|
+
- The CLI masks stored tokens in `config` output
|
|
17
|
+
- Non-HTTPS API base URLs require explicit `--allow-insecure`
|
|
18
|
+
|
|
19
|
+
## Operational Guidance
|
|
20
|
+
|
|
21
|
+
- Prefer piping tokens through stdin in automation instead of placing them in shell history
|
|
22
|
+
- Use a temporary `HOME` when running smoke tests against production
|
|
23
|
+
- Treat `execute_action` as a privileged operation and require explicit confirmation in non-interactive flows
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { buildProgram } from '../src/cli/program.js';
|
|
4
|
+
|
|
5
|
+
const program = buildProgram();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
await program.parseAsync();
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
console.error(`Error: ${message}`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "insightfulpipe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "InsightfulPipe CLI — query your marketing data from the terminal",
|
|
5
|
+
"bin": {
|
|
6
|
+
"insightfulpipe": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"src/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"SECURITY.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node bin/cli.js",
|
|
19
|
+
"test": "node --test",
|
|
20
|
+
"test:unit": "node --test test/cli.test.js",
|
|
21
|
+
"test:live": "node --test test/live-smoke.test.js"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"insightfulpipe",
|
|
28
|
+
"marketing",
|
|
29
|
+
"analytics",
|
|
30
|
+
"cli",
|
|
31
|
+
"mcp"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@napi-rs/keyring": "^1.2.0",
|
|
36
|
+
"commander": "^13.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { getApiUrl, getRequestTimeoutMs, getToken } from './config.js';
|
|
2
|
+
|
|
3
|
+
let _insecureWarned = false;
|
|
4
|
+
|
|
5
|
+
function warnIfInsecure() {
|
|
6
|
+
if (_insecureWarned) return;
|
|
7
|
+
const url = getApiUrl();
|
|
8
|
+
if (!url.startsWith('https://')) {
|
|
9
|
+
console.error('Warning: API URL is not using HTTPS. Traffic including your token is unencrypted.');
|
|
10
|
+
_insecureWarned = true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PLATFORM_SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
15
|
+
|
|
16
|
+
export function validatePlatformSlug(slug) {
|
|
17
|
+
if (!PLATFORM_SLUG_RE.test(slug) || slug.includes('--')) {
|
|
18
|
+
console.error(`Error: Invalid platform slug "${slug}". Use lowercase letters, numbers, and hyphens only.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireAuth() {
|
|
24
|
+
const token = getToken();
|
|
25
|
+
if (!token) {
|
|
26
|
+
console.error('Error: Not authenticated. Run: insightfulpipe auth');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildApiUrl(path) {
|
|
33
|
+
const baseUrl = getApiUrl();
|
|
34
|
+
|
|
35
|
+
let base;
|
|
36
|
+
try {
|
|
37
|
+
base = new URL(baseUrl);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(`Configured API URL is invalid: ${baseUrl}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
43
|
+
return new URL(`/api/raw${normalizedPath}`, base).toString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function looksLikeHtml(text) {
|
|
47
|
+
const trimmed = text.trim().toLowerCase();
|
|
48
|
+
return trimmed.startsWith('<!doctype html') || trimmed.startsWith('<html');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function summarizeTextError(text, res) {
|
|
52
|
+
if (looksLikeHtml(text)) {
|
|
53
|
+
const titleMatch = text.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
54
|
+
const headingMatch = text.match(/<h1[^>]*>([^<]+)<\/h1>/i) || text.match(/<h2[^>]*>([^<]+)<\/h2>/i);
|
|
55
|
+
const summary = titleMatch?.[1]?.trim() || headingMatch?.[1]?.trim() || `HTTP ${res.status}`;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
error: true,
|
|
59
|
+
status: res.status,
|
|
60
|
+
message: `Server returned an HTML error page (${summary}).`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
error: true,
|
|
66
|
+
status: res.status,
|
|
67
|
+
message: text,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function parseJsonResponse(res) {
|
|
72
|
+
const text = await res.text();
|
|
73
|
+
|
|
74
|
+
if (!text.trim()) {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(text);
|
|
80
|
+
} catch {
|
|
81
|
+
if (res.ok) {
|
|
82
|
+
return {
|
|
83
|
+
error: true,
|
|
84
|
+
status: res.status,
|
|
85
|
+
message: 'Received invalid JSON from server.',
|
|
86
|
+
details: text,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return summarizeTextError(text, res);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function apiRequest(method, path, body) {
|
|
95
|
+
warnIfInsecure();
|
|
96
|
+
const token = requireAuth();
|
|
97
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
98
|
+
|
|
99
|
+
let url;
|
|
100
|
+
try {
|
|
101
|
+
url = buildApiUrl(path);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
error: true,
|
|
105
|
+
message: error.message,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const options = {
|
|
110
|
+
method,
|
|
111
|
+
headers: {
|
|
112
|
+
'Authorization': `Bearer ${token}`,
|
|
113
|
+
'Accept': 'application/json',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (body !== undefined) {
|
|
118
|
+
options.headers['Content-Type'] = 'application/json';
|
|
119
|
+
options.body = JSON.stringify(body);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let res;
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
125
|
+
try {
|
|
126
|
+
res = await fetch(url, { ...options, signal: controller.signal });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
return {
|
|
130
|
+
error: true,
|
|
131
|
+
message:
|
|
132
|
+
error?.name === 'AbortError'
|
|
133
|
+
? `Network request timed out after ${timeoutMs} ms`
|
|
134
|
+
: `Network request failed: ${error.message}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
|
|
139
|
+
const data = await parseJsonResponse(res);
|
|
140
|
+
|
|
141
|
+
if (data && data.error) {
|
|
142
|
+
return data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
return {
|
|
147
|
+
error: true,
|
|
148
|
+
status: res.status,
|
|
149
|
+
...(data && typeof data === 'object' ? data : { message: String(data) }),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return data;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function apiGet(path) {
|
|
157
|
+
return apiRequest('GET', path);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function apiPost(path, body) {
|
|
161
|
+
return apiRequest('POST', path, body);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function accountSummary() {
|
|
165
|
+
warnIfInsecure();
|
|
166
|
+
const token = requireAuth();
|
|
167
|
+
const timeoutMs = getRequestTimeoutMs();
|
|
168
|
+
let url;
|
|
169
|
+
try {
|
|
170
|
+
url = new URL('/oauth/api/account-summary/', new URL(getApiUrl())).toString();
|
|
171
|
+
} catch {
|
|
172
|
+
return {
|
|
173
|
+
error: true,
|
|
174
|
+
message: `Configured API URL is invalid: ${getApiUrl()}`,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let res;
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
181
|
+
try {
|
|
182
|
+
res = await fetch(url, {
|
|
183
|
+
method: 'GET',
|
|
184
|
+
headers: {
|
|
185
|
+
'Authorization': `Bearer ${token}`,
|
|
186
|
+
'Accept': 'application/json',
|
|
187
|
+
},
|
|
188
|
+
signal: controller.signal,
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
return {
|
|
193
|
+
error: true,
|
|
194
|
+
message:
|
|
195
|
+
error?.name === 'AbortError'
|
|
196
|
+
? `Network request timed out after ${timeoutMs} ms`
|
|
197
|
+
: `Network request failed: ${error.message}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
clearTimeout(timeout);
|
|
201
|
+
|
|
202
|
+
const data = await parseJsonResponse(res);
|
|
203
|
+
|
|
204
|
+
if (data && data.error) {
|
|
205
|
+
return data;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
return {
|
|
210
|
+
error: true,
|
|
211
|
+
status: res.status,
|
|
212
|
+
...(data && typeof data === 'object' ? data : { message: String(data) }),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return data;
|
|
217
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getConfigSnapshot, saveConfig } from '../../config.js';
|
|
2
|
+
import { fail } from '../errors.js';
|
|
3
|
+
import { promptSecret } from '../input.js';
|
|
4
|
+
|
|
5
|
+
export function registerAuthCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('auth')
|
|
8
|
+
.description('Save your InsightfulPipe API key')
|
|
9
|
+
.option('--insecure-storage', 'Save token to config file instead of OS keyring')
|
|
10
|
+
.action(async (opts) => {
|
|
11
|
+
const token = await promptSecret('Enter your API token: ');
|
|
12
|
+
|
|
13
|
+
if (!token) {
|
|
14
|
+
fail('No token provided.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!token.startsWith('ip_sk_')) {
|
|
18
|
+
fail('Token must start with ip_sk_.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = { ...getConfigSnapshot().rawValues, token };
|
|
22
|
+
const { tokenInKeyring } = saveConfig(config, { insecure: opts.insecureStorage });
|
|
23
|
+
|
|
24
|
+
if (tokenInKeyring) {
|
|
25
|
+
console.log('Authenticated successfully. Token saved to OS keyring.');
|
|
26
|
+
} else {
|
|
27
|
+
console.log('Authenticated successfully. Token saved to config file.');
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getConfigSnapshot, normalizeTimeoutMs, saveConfig } from '../../config.js';
|
|
2
|
+
import { printJson, wantsJson } from '../../output.js';
|
|
3
|
+
import { validateConfiguredUrl } from '../options.js';
|
|
4
|
+
|
|
5
|
+
export function registerConfigCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('config')
|
|
8
|
+
.description('Show current configuration')
|
|
9
|
+
.option('--set-url <url>', 'Set API URL')
|
|
10
|
+
.option('--set-timeout <ms>', 'Set request timeout in milliseconds')
|
|
11
|
+
.option('--allow-insecure', 'Allow http:// URLs for local development only')
|
|
12
|
+
.action((opts, command) => {
|
|
13
|
+
const snapshot = getConfigSnapshot();
|
|
14
|
+
const nextConfig = { ...snapshot.rawValues };
|
|
15
|
+
|
|
16
|
+
if (opts.setUrl) {
|
|
17
|
+
nextConfig.api_url = validateConfiguredUrl(opts.setUrl, opts.allowInsecure);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (opts.setTimeout) {
|
|
21
|
+
nextConfig.timeout_ms = normalizeTimeoutMs(opts.setTimeout, '--set-timeout');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (opts.setUrl || opts.setTimeout) {
|
|
25
|
+
saveConfig(nextConfig);
|
|
26
|
+
if (wantsJson(command)) {
|
|
27
|
+
printJson({
|
|
28
|
+
updated: true,
|
|
29
|
+
values: {
|
|
30
|
+
api_url: nextConfig.api_url ?? snapshot.values.api_url,
|
|
31
|
+
timeout_ms: nextConfig.timeout_ms ?? snapshot.values.timeout_ms,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`API URL: ${nextConfig.api_url ?? snapshot.values.api_url}`);
|
|
38
|
+
console.log(`Timeout: ${nextConfig.timeout_ms ?? snapshot.values.timeout_ms} ms`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const output = {
|
|
43
|
+
config_file: snapshot.configFile,
|
|
44
|
+
values: {
|
|
45
|
+
api_url: snapshot.values.api_url,
|
|
46
|
+
token: snapshot.values.token ? `${snapshot.values.token.slice(0, 10)}...` : null,
|
|
47
|
+
timeout_ms: snapshot.values.timeout_ms,
|
|
48
|
+
},
|
|
49
|
+
sources: snapshot.sources,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (wantsJson(command)) {
|
|
53
|
+
printJson(output);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`Config file: ${output.config_file}`);
|
|
58
|
+
console.log(`API URL: ${output.values.api_url} (${output.sources.api_url})`);
|
|
59
|
+
console.log(`Token: ${output.values.token || 'Not set'} (${output.sources.token})`);
|
|
60
|
+
console.log(`Timeout: ${output.values.timeout_ms} ms (${output.sources.timeout_ms})`);
|
|
61
|
+
});
|
|
62
|
+
}
|