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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { getConfigSnapshot, getToken } from '../../config.js';
|
|
2
|
+
import { apiGet } from '../../api.js';
|
|
3
|
+
import { printJson, printTable, wantsJson } from '../../output.js';
|
|
4
|
+
import {
|
|
5
|
+
buildQueryString,
|
|
6
|
+
parseIdList,
|
|
7
|
+
parseIntegerOption,
|
|
8
|
+
toPromptDataSource,
|
|
9
|
+
} from '../options.js';
|
|
10
|
+
import { fail, handleApiError } from '../errors.js';
|
|
11
|
+
import {
|
|
12
|
+
fetchBrandMetadata,
|
|
13
|
+
fetchConnectedSources,
|
|
14
|
+
fetchHelperPayload,
|
|
15
|
+
runQueryContexts,
|
|
16
|
+
sourceDisplayName,
|
|
17
|
+
sourceIdentifier,
|
|
18
|
+
} from '../query-contexts.js';
|
|
19
|
+
|
|
20
|
+
export function registerDiscoveryCommands(program) {
|
|
21
|
+
program
|
|
22
|
+
.command('whoami')
|
|
23
|
+
.description('Show the authenticated user')
|
|
24
|
+
.action(async (opts, command) => {
|
|
25
|
+
const data = handleApiError(await apiGet('/user-info/'));
|
|
26
|
+
|
|
27
|
+
if (wantsJson(command)) {
|
|
28
|
+
printJson(data);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`User: ${data.user.email}`);
|
|
33
|
+
console.log(`Name: ${data.user.full_name || '-'}`);
|
|
34
|
+
console.log(`Workspaces: ${data.total_workspaces}`);
|
|
35
|
+
for (const workspace of data.workspaces || []) {
|
|
36
|
+
const subscription = workspace.subscription || {};
|
|
37
|
+
const planName = subscription.plan_name || '-';
|
|
38
|
+
const planStatus = subscription.status || (subscription.is_active ? 'active' : '-');
|
|
39
|
+
console.log(` - ${workspace.name} (${workspace.role}; ${planName}; ${planStatus})`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('platforms')
|
|
45
|
+
.description('List available platforms')
|
|
46
|
+
.option('--prod', 'Only show production-ready platforms')
|
|
47
|
+
.action(async (opts, command) => {
|
|
48
|
+
const suffix = buildQueryString({ prod: opts.prod ? 'true' : undefined });
|
|
49
|
+
const data = handleApiError(await apiGet(`/available-platforms/${suffix}`));
|
|
50
|
+
|
|
51
|
+
if (wantsJson(command)) {
|
|
52
|
+
printJson(data);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
printTable(data.platforms || [], [
|
|
57
|
+
{ key: 'slug', label: 'Platform' },
|
|
58
|
+
{ key: 'name', label: 'Name' },
|
|
59
|
+
{ key: 'category', label: 'Category' },
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.command('query_contexts')
|
|
65
|
+
.argument('[request]', 'One of: accounts, brands, available_actions, actions_details')
|
|
66
|
+
.description('Retrieve accounts, brands, and action schemas')
|
|
67
|
+
.option('-r, --request <request>', 'Request type if you prefer flags over the positional argument')
|
|
68
|
+
.option('-p, --platform <platform>', 'Platform slug')
|
|
69
|
+
.option('--workspace-id <id>', 'Workspace ID')
|
|
70
|
+
.option('--brand-id <id>', 'Brand ID')
|
|
71
|
+
.option('-a, --actions <actions>', 'Comma-separated action names')
|
|
72
|
+
.action(async (requestArg, opts) => {
|
|
73
|
+
const requestName = (requestArg || opts.request || '').trim();
|
|
74
|
+
if (!requestName) {
|
|
75
|
+
fail(
|
|
76
|
+
'query_contexts requires a request.',
|
|
77
|
+
'Use one of: accounts, brands, available_actions, actions_details.'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const data = await runQueryContexts(requestName, opts);
|
|
82
|
+
printJson(data);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
program
|
|
86
|
+
.command('workspaces')
|
|
87
|
+
.description('List accessible workspaces')
|
|
88
|
+
.action(async (opts, command) => {
|
|
89
|
+
const data = handleApiError(await apiGet('/workspaces/'));
|
|
90
|
+
|
|
91
|
+
if (wantsJson(command)) {
|
|
92
|
+
printJson(data);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
printTable(data.workspaces || [], [
|
|
97
|
+
{ key: 'id', label: 'ID' },
|
|
98
|
+
{ key: 'slug', label: 'Slug' },
|
|
99
|
+
{ key: 'name', label: 'Name' },
|
|
100
|
+
{ key: 'role', label: 'Role' },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command('brands')
|
|
106
|
+
.description('List brand metadata for a workspace')
|
|
107
|
+
.requiredOption('-w, --workspace <id>', 'Workspace ID')
|
|
108
|
+
.option('--brand-ids <ids>', 'Comma-separated brand IDs')
|
|
109
|
+
.action(async (opts, command) => {
|
|
110
|
+
const workspaceId = parseIntegerOption('workspace', opts.workspace);
|
|
111
|
+
const brandIds = opts.brandIds ? parseIdList(opts.brandIds, 'brand ID') : undefined;
|
|
112
|
+
const data = await fetchBrandMetadata(workspaceId, brandIds);
|
|
113
|
+
const brands = Array.isArray(data.brands) ? data.brands : [];
|
|
114
|
+
|
|
115
|
+
if (wantsJson(command)) {
|
|
116
|
+
printJson(data);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (brands.length === 1 || brandIds) {
|
|
121
|
+
printJson(brands.length === 1 ? brands[0] : brands);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
printTable(brands, [
|
|
126
|
+
{ key: 'id', label: 'ID' },
|
|
127
|
+
{ key: 'name', label: 'Name' },
|
|
128
|
+
{ key: 'website_url', label: 'Website' },
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command('accounts')
|
|
134
|
+
.description('List connected accounts')
|
|
135
|
+
.option('-p, --platform <platform>', 'Filter by platform')
|
|
136
|
+
.option('--prod', 'Only include production-ready sources')
|
|
137
|
+
.action(async (opts, command) => {
|
|
138
|
+
const data = await fetchConnectedSources(opts.platform, opts.prod);
|
|
139
|
+
const sources = Array.isArray(data.sources) ? data.sources : [];
|
|
140
|
+
|
|
141
|
+
if (wantsJson(command)) {
|
|
142
|
+
printJson(data);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (sources.length === 0) {
|
|
147
|
+
console.log('No connected accounts.');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rows = sources.map((source) => ({
|
|
152
|
+
platform: source.platform || '-',
|
|
153
|
+
account: sourceDisplayName(source),
|
|
154
|
+
workspace: source.workspace_name || '-',
|
|
155
|
+
access: source.access_level || '-',
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
printTable(rows, [
|
|
159
|
+
{ key: 'platform', label: 'Platform' },
|
|
160
|
+
{ key: 'account', label: 'Account' },
|
|
161
|
+
{ key: 'workspace', label: 'Workspace' },
|
|
162
|
+
{ key: 'access', label: 'Access' },
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
program
|
|
167
|
+
.command('sources')
|
|
168
|
+
.description('List connected data sources with IDs')
|
|
169
|
+
.option('-p, --platform <platform>', 'Filter by platform')
|
|
170
|
+
.option('--prod', 'Only include production-ready sources')
|
|
171
|
+
.action(async (opts, command) => {
|
|
172
|
+
const data = await fetchConnectedSources(opts.platform, opts.prod);
|
|
173
|
+
const sources = Array.isArray(data.sources) ? data.sources : [];
|
|
174
|
+
|
|
175
|
+
if (wantsJson(command)) {
|
|
176
|
+
printJson(data);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rows = sources.map((source) => ({
|
|
181
|
+
platform: source.platform || '-',
|
|
182
|
+
name: sourceDisplayName(source),
|
|
183
|
+
id: sourceIdentifier(source),
|
|
184
|
+
workspace: source.workspace_name || '-',
|
|
185
|
+
brand: source.brand_name || '-',
|
|
186
|
+
access: source.access_level || '-',
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
printTable(rows, [
|
|
190
|
+
{ key: 'platform', label: 'Platform' },
|
|
191
|
+
{ key: 'name', label: 'Name' },
|
|
192
|
+
{ key: 'id', label: 'ID' },
|
|
193
|
+
{ key: 'workspace', label: 'Workspace' },
|
|
194
|
+
{ key: 'brand', label: 'Brand' },
|
|
195
|
+
{ key: 'access', label: 'Access' },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
program
|
|
200
|
+
.command('helper')
|
|
201
|
+
.argument('<platform>', 'Platform slug (e.g., google-analytics)')
|
|
202
|
+
.description('Show available actions and schema for a platform')
|
|
203
|
+
.option('-a, --actions <actions>', 'Comma-separated action names')
|
|
204
|
+
.action(async (platform, opts) => {
|
|
205
|
+
const actions = opts.actions ? opts.actions.split(',').map((value) => value.trim()).filter(Boolean) : undefined;
|
|
206
|
+
const data = await fetchHelperPayload(platform, actions);
|
|
207
|
+
printJson(data);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
program
|
|
211
|
+
.command('prompts')
|
|
212
|
+
.argument('<platform>', 'Platform slug (e.g., google-ads)')
|
|
213
|
+
.description('List prompt templates or fetch prompt details')
|
|
214
|
+
.option('--workspace <id>', 'Workspace ID for custom prompts')
|
|
215
|
+
.option('--type <type>', 'Prompt type filter')
|
|
216
|
+
.option('--id <id>', 'Prompt ID to fetch details')
|
|
217
|
+
.option('--custom', 'Fetch a custom prompt detail (requires --workspace and --id)')
|
|
218
|
+
.action(async (platform, opts, command) => {
|
|
219
|
+
const dataSource = toPromptDataSource(platform);
|
|
220
|
+
|
|
221
|
+
if (opts.id) {
|
|
222
|
+
const promptId = parseIntegerOption('prompt ID', opts.id);
|
|
223
|
+
|
|
224
|
+
if (opts.custom) {
|
|
225
|
+
if (!opts.workspace) {
|
|
226
|
+
fail('--workspace is required with --custom and --id.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const data = handleApiError(
|
|
230
|
+
await apiGet(
|
|
231
|
+
`/prompts/custom/details/${buildQueryString({
|
|
232
|
+
prompt_id: promptId,
|
|
233
|
+
workspace_id: parseIntegerOption('workspace', opts.workspace),
|
|
234
|
+
})}`
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
if (wantsJson(command)) {
|
|
238
|
+
printJson(data);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log(data.prompt_detail || '');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const data = handleApiError(
|
|
246
|
+
await apiGet(
|
|
247
|
+
`/prompts/details/${buildQueryString({
|
|
248
|
+
prompt_id: promptId,
|
|
249
|
+
type: opts.type,
|
|
250
|
+
})}`
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
if (wantsJson(command)) {
|
|
254
|
+
printJson(data);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
console.log(data.prompt_detail || '');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const data = handleApiError(
|
|
262
|
+
await apiGet(
|
|
263
|
+
`/prompts/menu/${buildQueryString({
|
|
264
|
+
data_source: dataSource,
|
|
265
|
+
workspace_id: opts.workspace ? parseIntegerOption('workspace', opts.workspace) : undefined,
|
|
266
|
+
type: opts.type,
|
|
267
|
+
})}`
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (wantsJson(command)) {
|
|
272
|
+
printJson(data);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
printTable(data.prompts || [], [
|
|
277
|
+
{ key: 'id', label: 'ID' },
|
|
278
|
+
{ key: 'prompt_name', label: 'Prompt' },
|
|
279
|
+
{ key: 'is_custom', label: 'Custom' },
|
|
280
|
+
{ key: 'data_source', label: 'Data Source' },
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
program
|
|
285
|
+
.command('doctor')
|
|
286
|
+
.description('Check configuration and API connectivity')
|
|
287
|
+
.action(async (opts, command) => {
|
|
288
|
+
const snapshot = getConfigSnapshot();
|
|
289
|
+
const token = getToken();
|
|
290
|
+
const checks = [
|
|
291
|
+
{
|
|
292
|
+
name: 'api_url',
|
|
293
|
+
status: 'ok',
|
|
294
|
+
details: snapshot.values.api_url,
|
|
295
|
+
source: snapshot.sources.api_url,
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: 'token',
|
|
299
|
+
status: token ? 'ok' : 'error',
|
|
300
|
+
details: token ? 'Configured' : 'Missing',
|
|
301
|
+
source: snapshot.sources.token,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: 'timeout',
|
|
305
|
+
status: 'ok',
|
|
306
|
+
details: `${snapshot.values.timeout_ms} ms`,
|
|
307
|
+
source: snapshot.sources.timeout_ms,
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
let connectivity = {
|
|
312
|
+
status: 'skipped',
|
|
313
|
+
details: 'Skipped because no token is configured.',
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
if (token) {
|
|
317
|
+
const data = await apiGet('/user-info/');
|
|
318
|
+
if (data?.error) {
|
|
319
|
+
connectivity = {
|
|
320
|
+
status: 'error',
|
|
321
|
+
details: data.message || data.details || 'Request failed',
|
|
322
|
+
};
|
|
323
|
+
} else {
|
|
324
|
+
connectivity = {
|
|
325
|
+
status: 'ok',
|
|
326
|
+
details: data.user?.email || 'Authenticated',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const status = [...checks, connectivity].some((item) => item.status === 'error') ? 'error' : 'ok';
|
|
332
|
+
const payload = {
|
|
333
|
+
status,
|
|
334
|
+
config_file: snapshot.configFile,
|
|
335
|
+
checks,
|
|
336
|
+
connectivity,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (wantsJson(command)) {
|
|
340
|
+
printJson(payload);
|
|
341
|
+
} else {
|
|
342
|
+
console.log(`Config file: ${payload.config_file}`);
|
|
343
|
+
for (const item of payload.checks) {
|
|
344
|
+
console.log(`${item.name}: ${item.status} (${item.details}; ${item.source})`);
|
|
345
|
+
}
|
|
346
|
+
console.log(`connectivity: ${payload.connectivity.status} (${payload.connectivity.details})`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (status === 'error') {
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { apiPost } from '../../api.js';
|
|
2
|
+
import { printJson } from '../../output.js';
|
|
3
|
+
import {
|
|
4
|
+
confirmWriteAction,
|
|
5
|
+
ensureJsonObject,
|
|
6
|
+
readJsonInput,
|
|
7
|
+
} from '../input.js';
|
|
8
|
+
import { handleApiError } from '../errors.js';
|
|
9
|
+
import { withPlatform } from '../options.js';
|
|
10
|
+
|
|
11
|
+
export function registerExecutionCommands(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('query_data')
|
|
14
|
+
.alias('query')
|
|
15
|
+
.argument('<platform>', 'Platform slug (e.g., google-analytics, shopify)')
|
|
16
|
+
.option('-b, --body <json>', 'Request body as inline JSON')
|
|
17
|
+
.option('-f, --file <path>', 'Request body from a JSON file')
|
|
18
|
+
.description('Execute a read-only data query for a platform')
|
|
19
|
+
.action(async (platform, opts) => {
|
|
20
|
+
const body = ensureJsonObject(await readJsonInput(opts, true), 'Request body');
|
|
21
|
+
const data = handleApiError(await apiPost('/', withPlatform(platform, body)));
|
|
22
|
+
printJson(data);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.command('execute_action')
|
|
27
|
+
.alias('action')
|
|
28
|
+
.argument('<platform>', 'Platform slug (e.g., facebook-pages, google-ads)')
|
|
29
|
+
.option('-b, --body <json>', 'Request body as inline JSON')
|
|
30
|
+
.option('-f, --file <path>', 'Request body from a JSON file')
|
|
31
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
32
|
+
.description('Execute a write or mutating action for a platform')
|
|
33
|
+
.action(async (platform, opts) => {
|
|
34
|
+
const body = ensureJsonObject(await readJsonInput(opts, true), 'Request body');
|
|
35
|
+
await confirmWriteAction(platform, body, opts.yes);
|
|
36
|
+
const data = handleApiError(await apiPost('/', withPlatform(platform, body)));
|
|
37
|
+
printJson(data);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
program
|
|
41
|
+
.command('raw')
|
|
42
|
+
.option('-b, --body <json>', 'Request body as inline JSON')
|
|
43
|
+
.option('-f, --file <path>', 'Request body from a JSON file')
|
|
44
|
+
.description('Send a raw request to the universal dispatcher')
|
|
45
|
+
.action(async (opts) => {
|
|
46
|
+
const body = ensureJsonObject(await readJsonInput(opts, true), 'Request body');
|
|
47
|
+
const data = handleApiError(await apiPost('/', body));
|
|
48
|
+
printJson(data);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function fail(message, tip) {
|
|
2
|
+
console.error(`Error: ${message}`);
|
|
3
|
+
if (tip) {
|
|
4
|
+
console.error(`Tip: ${tip}`);
|
|
5
|
+
}
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function handleApiError(data) {
|
|
10
|
+
if (!data || !data.error) {
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const message =
|
|
15
|
+
data.message ||
|
|
16
|
+
data.details ||
|
|
17
|
+
data.error ||
|
|
18
|
+
data.api_error ||
|
|
19
|
+
(data.status ? `Request failed with status ${data.status}` : 'Request failed');
|
|
20
|
+
|
|
21
|
+
fail(message, data.tip);
|
|
22
|
+
}
|
package/src/cli/input.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { fail } from './errors.js';
|
|
4
|
+
|
|
5
|
+
function readJson(value, sourceLabel) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(value);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
fail(`Invalid JSON from ${sourceLabel}: ${error.message}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ensureJsonObject(value, sourceLabel) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
15
|
+
fail(`${sourceLabel} must be a JSON object.`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readStdin() {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
process.stdin.setEncoding('utf-8');
|
|
23
|
+
for await (const chunk of process.stdin) {
|
|
24
|
+
chunks.push(chunk);
|
|
25
|
+
}
|
|
26
|
+
return chunks.join('');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readJsonInput(opts, allowStdin = true) {
|
|
30
|
+
if (opts.body && opts.file) {
|
|
31
|
+
fail('Provide either --body or --file, not both.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (opts.file) {
|
|
35
|
+
let contents;
|
|
36
|
+
try {
|
|
37
|
+
contents = readFileSync(opts.file, 'utf-8');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
fail(`Unable to read ${opts.file}: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
return readJson(contents, `file ${opts.file}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (opts.body) {
|
|
45
|
+
return readJson(opts.body, '--body');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!allowStdin) {
|
|
49
|
+
fail('Provide a request body via --body, --file, or stdin.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const input = await readStdin();
|
|
53
|
+
if (!input.trim()) {
|
|
54
|
+
fail('Provide a request body via --body, --file, or stdin.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return readJson(input, 'stdin');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function promptInput(promptText) {
|
|
61
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
rl.question(promptText, (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
resolve(answer.trim());
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function promptSecret(promptText) {
|
|
71
|
+
if (!process.stdin.isTTY) {
|
|
72
|
+
return (await readStdin()).trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const stdin = process.stdin;
|
|
76
|
+
stdin.resume();
|
|
77
|
+
stdin.setEncoding('utf-8');
|
|
78
|
+
stdin.setRawMode(true);
|
|
79
|
+
process.stderr.write(promptText);
|
|
80
|
+
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
let secret = '';
|
|
83
|
+
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
stdin.off('data', onData);
|
|
86
|
+
stdin.setRawMode(false);
|
|
87
|
+
stdin.pause();
|
|
88
|
+
process.stderr.write('\n');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const onData = (chunk) => {
|
|
92
|
+
for (const input of String(chunk)) {
|
|
93
|
+
if (input === '\u0003') {
|
|
94
|
+
cleanup();
|
|
95
|
+
process.exit(130);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (input === '\r' || input === '\n') {
|
|
99
|
+
cleanup();
|
|
100
|
+
resolve(secret.trim());
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (input === '\u007f' || input === '\b' || input === '\x08') {
|
|
105
|
+
if (secret.length > 0) {
|
|
106
|
+
secret = secret.slice(0, -1);
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (input === '\u0004') {
|
|
112
|
+
cleanup();
|
|
113
|
+
resolve(secret.trim());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
secret += input;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
stdin.on('data', onData);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function confirmWriteAction(platform, body, skipConfirmation) {
|
|
126
|
+
if (skipConfirmation) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!process.stdin.isTTY) {
|
|
131
|
+
fail('Write actions require confirmation in non-interactive mode. Re-run with --yes.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const actionName = body.action || 'unknown-action';
|
|
135
|
+
const answer = await promptInput(`Execute write action "${actionName}" on "${platform}"? [y/N] `);
|
|
136
|
+
if (!/^(y|yes)$/i.test(answer)) {
|
|
137
|
+
fail('Write action cancelled.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { validatePlatformSlug } from '../api.js';
|
|
2
|
+
import { fail } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export function parseIntegerOption(name, value) {
|
|
5
|
+
const parsed = Number(value);
|
|
6
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
7
|
+
fail(`${name} must be a positive integer.`);
|
|
8
|
+
}
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseIdList(value, optionName) {
|
|
13
|
+
const parts = String(value)
|
|
14
|
+
.split(',')
|
|
15
|
+
.map((part) => part.trim())
|
|
16
|
+
.filter(Boolean);
|
|
17
|
+
|
|
18
|
+
if (parts.length === 0) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return parts.map((part) => parseIntegerOption(optionName, part));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function parseStringList(value) {
|
|
26
|
+
return String(value)
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((part) => part.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildQueryString(params) {
|
|
33
|
+
const searchParams = new URLSearchParams();
|
|
34
|
+
|
|
35
|
+
for (const [key, value] of Object.entries(params)) {
|
|
36
|
+
if (value === undefined || value === null || value === '') {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
searchParams.set(key, String(value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const query = searchParams.toString();
|
|
43
|
+
return query ? `?${query}` : '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function validateConfiguredUrl(url, allowInsecure) {
|
|
47
|
+
let urlObj;
|
|
48
|
+
try {
|
|
49
|
+
urlObj = new URL(url);
|
|
50
|
+
} catch {
|
|
51
|
+
fail('API URL must be a valid absolute URL.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (urlObj.protocol !== 'https:' && urlObj.protocol !== 'http:') {
|
|
55
|
+
fail('API URL must use http:// or https://.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (urlObj.protocol !== 'https:' && !allowInsecure) {
|
|
59
|
+
fail('API URL must use HTTPS. Use --allow-insecure for local development.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
urlObj.hostname !== 'localhost' &&
|
|
64
|
+
urlObj.hostname !== '127.0.0.1' &&
|
|
65
|
+
!urlObj.hostname.endsWith('.insightfulpipe.com')
|
|
66
|
+
) {
|
|
67
|
+
console.error(
|
|
68
|
+
'Warning: API URL points to a non-InsightfulPipe domain. Your API token will be sent to this server.'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return urlObj.toString().replace(/\/+$/, '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function withPlatform(platform, body) {
|
|
76
|
+
validatePlatformSlug(platform);
|
|
77
|
+
|
|
78
|
+
if (body.platform && body.platform !== platform) {
|
|
79
|
+
fail(`Body platform "${body.platform}" does not match command platform "${platform}".`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { ...body, platform };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function toPromptDataSource(platform) {
|
|
86
|
+
validatePlatformSlug(platform);
|
|
87
|
+
return platform.replace(/-/g, '_');
|
|
88
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { registerAuthCommand } from './commands/auth.js';
|
|
3
|
+
import { registerConfigCommand } from './commands/config.js';
|
|
4
|
+
import { registerDiscoveryCommands } from './commands/discovery.js';
|
|
5
|
+
import { registerExecutionCommands } from './commands/execution.js';
|
|
6
|
+
|
|
7
|
+
export function buildProgram() {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('insightfulpipe')
|
|
12
|
+
.description(
|
|
13
|
+
'InsightfulPipe CLI — query your marketing data from the terminal.\n\n' +
|
|
14
|
+
'Run "insightfulpipe platforms" to see all supported platforms.\n\n' +
|
|
15
|
+
'Query workflow:\n' +
|
|
16
|
+
' 1. insightfulpipe accounts --platform <platform>\n' +
|
|
17
|
+
' Get workspace_id, brand_id, and source identifiers (e.g. property_id, site_url).\n\n' +
|
|
18
|
+
' 2. insightfulpipe helper <platform>\n' +
|
|
19
|
+
' List all actions available for the platform.\n\n' +
|
|
20
|
+
' 3. insightfulpipe helper <platform> --actions <action1>,<action2>\n' +
|
|
21
|
+
' Get the exact request body schema. Never guess the body — always check here first.\n\n' +
|
|
22
|
+
' 4. insightfulpipe query <platform> -b \'{"action":"...","workspace_id":...}\'\n' +
|
|
23
|
+
' Execute a read query. Replace "xxx" in the schema with real values from step 1.\n\n' +
|
|
24
|
+
' insightfulpipe action <platform> -b \'{"action":"..."}\' --yes\n' +
|
|
25
|
+
' Execute a write operation (create, update, delete, publish, send).\n' +
|
|
26
|
+
' Requires --yes in non-interactive mode.\n\n' +
|
|
27
|
+
'Other useful commands:\n' +
|
|
28
|
+
' insightfulpipe platforms All supported platforms\n' +
|
|
29
|
+
' insightfulpipe sources --platform X Connected sources with IDs\n' +
|
|
30
|
+
' insightfulpipe prompts <platform> Prompt templates for a platform\n' +
|
|
31
|
+
' insightfulpipe brands --workspace ID Brand metadata\n' +
|
|
32
|
+
' insightfulpipe whoami Current authenticated user\n' +
|
|
33
|
+
' insightfulpipe doctor Check auth and connectivity\n\n' +
|
|
34
|
+
'Authentication:\n' +
|
|
35
|
+
' insightfulpipe auth Save your API token (starts with ip_sk_)\n' +
|
|
36
|
+
' Or set INSIGHTFULPIPE_TOKEN env var.'
|
|
37
|
+
)
|
|
38
|
+
.version('0.1.0')
|
|
39
|
+
.option('--json', 'Print JSON output when supported')
|
|
40
|
+
.showHelpAfterError();
|
|
41
|
+
|
|
42
|
+
registerAuthCommand(program);
|
|
43
|
+
registerDiscoveryCommands(program);
|
|
44
|
+
registerExecutionCommands(program);
|
|
45
|
+
registerConfigCommand(program);
|
|
46
|
+
|
|
47
|
+
return program;
|
|
48
|
+
}
|