@commissionsight/cli 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/LICENSE +21 -0
- package/README.md +248 -0
- package/bin/cs.mjs +2 -0
- package/dist/commands/admin.d.ts +7 -0
- package/dist/commands/admin.js +409 -0
- package/dist/commands/auth.d.ts +7 -0
- package/dist/commands/auth.js +107 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +68 -0
- package/dist/commands/billing.d.ts +6 -0
- package/dist/commands/billing.js +75 -0
- package/dist/commands/carrier.d.ts +6 -0
- package/dist/commands/carrier.js +111 -0
- package/dist/commands/completion.d.ts +6 -0
- package/dist/commands/completion.js +56 -0
- package/dist/commands/context.d.ts +6 -0
- package/dist/commands/context.js +73 -0
- package/dist/commands/file.d.ts +6 -0
- package/dist/commands/file.js +97 -0
- package/dist/commands/job.d.ts +2 -0
- package/dist/commands/job.js +186 -0
- package/dist/commands/member.d.ts +5 -0
- package/dist/commands/member.js +91 -0
- package/dist/commands/meta.d.ts +7 -0
- package/dist/commands/meta.js +36 -0
- package/dist/commands/rate.d.ts +5 -0
- package/dist/commands/rate.js +69 -0
- package/dist/commands/registry.d.ts +14 -0
- package/dist/commands/registry.js +56 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +168 -0
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +21 -0
- package/dist/commands/team.d.ts +5 -0
- package/dist/commands/team.js +61 -0
- package/dist/commands/upload.d.ts +85 -0
- package/dist/commands/upload.js +111 -0
- package/dist/commands/webhook.d.ts +5 -0
- package/dist/commands/webhook.js +56 -0
- package/dist/commands/workspace.d.ts +8 -0
- package/dist/commands/workspace.js +65 -0
- package/dist/config/schema.d.ts +21 -0
- package/dist/config/schema.js +33 -0
- package/dist/config/store.d.ts +17 -0
- package/dist/config/store.js +74 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.js +100 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.js +70 -0
- package/dist/globals.d.ts +10 -0
- package/dist/globals.js +38 -0
- package/dist/io.d.ts +28 -0
- package/dist/io.js +28 -0
- package/dist/lib/batch.d.ts +52 -0
- package/dist/lib/batch.js +0 -0
- package/dist/lib/confirm.d.ts +2 -0
- package/dist/lib/confirm.js +23 -0
- package/dist/lib/file.d.ts +6 -0
- package/dist/lib/file.js +43 -0
- package/dist/lib/input.d.ts +2 -0
- package/dist/lib/input.js +35 -0
- package/dist/lib/paginate.d.ts +33 -0
- package/dist/lib/paginate.js +47 -0
- package/dist/lib/period.d.ts +15 -0
- package/dist/lib/period.js +43 -0
- package/dist/lib/poll.d.ts +14 -0
- package/dist/lib/poll.js +17 -0
- package/dist/lib/resolve.d.ts +30 -0
- package/dist/lib/resolve.js +81 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +17 -0
- package/dist/output/color.d.ts +26 -0
- package/dist/output/color.js +37 -0
- package/dist/output/csv.d.ts +25 -0
- package/dist/output/csv.js +119 -0
- package/dist/output/envelope.d.ts +29 -0
- package/dist/output/envelope.js +66 -0
- package/dist/output/help.d.ts +7 -0
- package/dist/output/help.js +57 -0
- package/dist/output/print.d.ts +14 -0
- package/dist/output/print.js +70 -0
- package/dist/output/schema-tree.d.ts +32 -0
- package/dist/output/schema-tree.js +33 -0
- package/dist/router.d.ts +6 -0
- package/dist/router.js +267 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.js +39 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +41 -0
- package/package.json +53 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload (plan §6.3, the centerpiece): `cs upload <file>` → uploadFile, with
|
|
3
|
+
* optional job wait + scored-results dump. The bulk `cs upload batch` lives in
|
|
4
|
+
* batch.ts (Phase 5). Workspace is required when the account has multi-workspace
|
|
5
|
+
* enabled and no default is set.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync } from 'node:fs';
|
|
8
|
+
import { basename } from 'node:path';
|
|
9
|
+
import { cover } from './registry.js';
|
|
10
|
+
import { loadFile, sha8 } from '../lib/file.js';
|
|
11
|
+
import { resolveCarrierEntry, resolveWorkspace } from '../lib/resolve.js';
|
|
12
|
+
import { periodFromOptions, formatPeriod } from '../lib/period.js';
|
|
13
|
+
import { waitForJob } from '../lib/poll.js';
|
|
14
|
+
import { emitCSV, RESULT_ROW_COLUMNS } from '../output/csv.js';
|
|
15
|
+
import { loadConfig } from '../config/store.js';
|
|
16
|
+
import { UsageError, CliError, EXIT } from '../errors.js';
|
|
17
|
+
import { optStr, optBool, optNum } from '../util.js';
|
|
18
|
+
export const UPLOAD_OPTIONS = {
|
|
19
|
+
carrier: { type: 'string', desc: 'Carrier id|slug|name (required)', placeholder: '<c>' },
|
|
20
|
+
period: { type: 'string', desc: 'Statement period YYYY-MM', placeholder: '<YYYY-MM>' },
|
|
21
|
+
year: { type: 'string', desc: 'Period year (with --month)', placeholder: '<n>' },
|
|
22
|
+
month: { type: 'string', desc: 'Period month (with --year)', placeholder: '<n>' },
|
|
23
|
+
workspace: { type: 'string', desc: 'Target workspace id|name', placeholder: '<w>' },
|
|
24
|
+
replace: { type: 'boolean', desc: 'Replace an existing carrier+period atomically' },
|
|
25
|
+
webhook: { type: 'string', desc: 'Webhook URL for job events', placeholder: '<url>' },
|
|
26
|
+
'idempotency-key': { type: 'string', desc: 'Override the derived idempotency key', placeholder: '<k>' },
|
|
27
|
+
wait: { type: 'boolean', desc: 'Poll the job to a terminal state' },
|
|
28
|
+
timeout: { type: 'string', desc: 'Wait timeout seconds (default 300)', placeholder: '<s>' },
|
|
29
|
+
interval: { type: 'string', desc: 'Poll interval seconds (default 2)', placeholder: '<s>' },
|
|
30
|
+
results: { type: 'boolean', desc: 'Dump scored rows on completion (implies --wait)' },
|
|
31
|
+
status: { type: 'string', desc: 'Filter results by status', choices: ['green', 'yellow', 'red'], placeholder: '<s>' },
|
|
32
|
+
'owed-only': { type: 'boolean', desc: 'Only rows with commission owed' },
|
|
33
|
+
chargeback: { type: 'boolean', desc: 'Only chargeback rows' },
|
|
34
|
+
csv: { type: 'boolean', desc: 'Emit results as CSV (with --results)' },
|
|
35
|
+
output: { type: 'string', short: 'o', desc: 'Write CSV to a file', placeholder: '<file>' },
|
|
36
|
+
};
|
|
37
|
+
export function uploadCommands() {
|
|
38
|
+
cover('uploadFile', 'cs upload');
|
|
39
|
+
cover('getJobResults', 'cs job results / cs upload --results');
|
|
40
|
+
return [
|
|
41
|
+
{
|
|
42
|
+
path: ['upload'],
|
|
43
|
+
summary: 'Upload a statement for a carrier + period',
|
|
44
|
+
args: [{ name: 'file', required: true }],
|
|
45
|
+
options: { ...UPLOAD_OPTIONS },
|
|
46
|
+
run: runUpload,
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
async function runUpload(ctx, parsed) {
|
|
51
|
+
const filePath = parsed.args[0];
|
|
52
|
+
const o = parsed.options;
|
|
53
|
+
const carrierRef = optStr(o['carrier']);
|
|
54
|
+
if (!carrierRef)
|
|
55
|
+
throw new UsageError('--carrier <id|slug|name> is required');
|
|
56
|
+
const file = loadFile(filePath);
|
|
57
|
+
const carrier = await resolveCarrierEntry(ctx, carrierRef);
|
|
58
|
+
const period = periodFromOptions({
|
|
59
|
+
period: optStr(o['period']),
|
|
60
|
+
year: optNum(o['year']),
|
|
61
|
+
month: optNum(o['month']),
|
|
62
|
+
});
|
|
63
|
+
const workspaceId = await resolveWorkspace(ctx, optStr(o['workspace']));
|
|
64
|
+
const contextName = ctx.globals.context || loadConfig(ctx.io).current;
|
|
65
|
+
const periodStr = formatPeriod(period.periodYear, period.periodMonth);
|
|
66
|
+
const idempotencyKey = optStr(o['idempotency-key']) ??
|
|
67
|
+
`${contextName}-${carrier.slug}-${periodStr}-${sha8(filePath)}`;
|
|
68
|
+
const replace = optBool(o['replace']);
|
|
69
|
+
const webhookUrl = optStr(o['webhook']);
|
|
70
|
+
const res = await ctx.client().uploadFile({
|
|
71
|
+
file,
|
|
72
|
+
carrierId: carrier.id,
|
|
73
|
+
periodYear: period.periodYear,
|
|
74
|
+
periodMonth: period.periodMonth,
|
|
75
|
+
idempotencyKey,
|
|
76
|
+
...(replace ? { replace: true } : {}),
|
|
77
|
+
...(webhookUrl ? { webhookUrl } : {}),
|
|
78
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
79
|
+
});
|
|
80
|
+
ctx.log(`uploaded ${basename(filePath)} → job ${res.jobId} (${res.mode ?? res.status})`);
|
|
81
|
+
const wantResults = optBool(o['results']);
|
|
82
|
+
const wantWait = optBool(o['wait']) || wantResults;
|
|
83
|
+
let job;
|
|
84
|
+
if (wantWait) {
|
|
85
|
+
const timeoutMs = (optNum(o['timeout']) ?? 300) * 1000;
|
|
86
|
+
const intervalMs = (optNum(o['interval']) ?? 2) * 1000;
|
|
87
|
+
job = await waitForJob(ctx.client(), res.jobId, {
|
|
88
|
+
timeoutMs,
|
|
89
|
+
intervalMs,
|
|
90
|
+
onTick: (j) => ctx.log(` job ${res.jobId}: ${j.status}`),
|
|
91
|
+
});
|
|
92
|
+
if (job.status === 'failed') {
|
|
93
|
+
const hint = job.exceptionRowCount
|
|
94
|
+
? ` — ${job.exceptionRowCount} rejected row(s); see 'cs job exceptions ${res.jobId}'`
|
|
95
|
+
: '';
|
|
96
|
+
throw new CliError(`job failed: ${job.error ?? 'unknown error'}${hint}`, EXIT.ERROR);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (wantResults) {
|
|
100
|
+
const results = await ctx.client().getJobResults(res.jobId, {
|
|
101
|
+
...(optStr(o['status']) ? { status: optStr(o['status']) } : {}),
|
|
102
|
+
...(optBool(o['owed-only']) ? { owedOnly: true } : {}),
|
|
103
|
+
...(optBool(o['chargeback']) ? { chargeback: true } : {}),
|
|
104
|
+
});
|
|
105
|
+
if (optBool(o['csv']) && !ctx.globals.json) {
|
|
106
|
+
return emitCSV(ctx.io, results.data, RESULT_ROW_COLUMNS, optStr(o['output']), (p, d) => writeFileSync(p, d));
|
|
107
|
+
}
|
|
108
|
+
return { upload: res, period: results.period, count: results.data.length, results: results.data };
|
|
109
|
+
}
|
|
110
|
+
return { upload: res, ...(job ? { job } : {}) };
|
|
111
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { cover } from './registry.js';
|
|
2
|
+
import { confirmDestructive } from '../lib/confirm.js';
|
|
3
|
+
import { UsageError } from '../errors.js';
|
|
4
|
+
import { optStr, optList } from '../util.js';
|
|
5
|
+
const VALID_EVENTS = ['job.completed', 'job.failed'];
|
|
6
|
+
export function webhookCommands() {
|
|
7
|
+
cover('listWebhooks', 'cs webhook list');
|
|
8
|
+
cover('createWebhook', 'cs webhook create');
|
|
9
|
+
cover('deleteWebhook', 'cs webhook delete');
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
path: ['webhook', 'list'],
|
|
13
|
+
summary: 'List webhooks',
|
|
14
|
+
async run(ctx) {
|
|
15
|
+
return (await ctx.client().listWebhooks()).data;
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: ['webhook', 'create'],
|
|
20
|
+
summary: 'Create a webhook (prints the signing secret once)',
|
|
21
|
+
options: {
|
|
22
|
+
url: { type: 'string', desc: 'Delivery URL (required)', placeholder: '<url>' },
|
|
23
|
+
events: { type: 'string', desc: 'Comma list: job.completed,job.failed', placeholder: '<list>' },
|
|
24
|
+
},
|
|
25
|
+
async run(ctx, parsed) {
|
|
26
|
+
const url = optStr(parsed.options['url']);
|
|
27
|
+
if (!url)
|
|
28
|
+
throw new UsageError('--url is required');
|
|
29
|
+
const events = optList(parsed.options['events']);
|
|
30
|
+
if (events.length === 0)
|
|
31
|
+
throw new UsageError(`--events is required (${VALID_EVENTS.join(',')})`);
|
|
32
|
+
for (const e of events) {
|
|
33
|
+
if (!VALID_EVENTS.includes(e)) {
|
|
34
|
+
throw new UsageError(`invalid event: ${e} (allowed: ${VALID_EVENTS.join(', ')})`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const res = await ctx.client().createWebhook({
|
|
38
|
+
url,
|
|
39
|
+
events: events,
|
|
40
|
+
});
|
|
41
|
+
ctx.log('the signing secret is shown once below and will not be retrievable again');
|
|
42
|
+
return res;
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: ['webhook', 'delete'],
|
|
47
|
+
summary: 'Delete a webhook (destructive)',
|
|
48
|
+
args: [{ name: 'id', required: true }],
|
|
49
|
+
async run(ctx, parsed) {
|
|
50
|
+
await confirmDestructive(ctx, `delete webhook ${parsed.args[0]}`);
|
|
51
|
+
await ctx.client().deleteWebhook(parsed.args[0]);
|
|
52
|
+
return { deleted: parsed.args[0] };
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace commands (plan §6.1). `create` is gated behind a capability check
|
|
3
|
+
* (§0.1): SDK 2.2.0 has no createWorkspace, so it fails with the exact,
|
|
4
|
+
* actionable message until a newer SDK ships the method — at which point it
|
|
5
|
+
* lights up with no CLI change.
|
|
6
|
+
*/
|
|
7
|
+
import type { Cmd } from '../types.js';
|
|
8
|
+
export declare function workspaceCommands(): Cmd[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cover } from './registry.js';
|
|
2
|
+
import { getWorkspaces } from '../lib/resolve.js';
|
|
3
|
+
import { upsertContext, loadConfig } from '../config/store.js';
|
|
4
|
+
import { CliError, UsageError, EXIT } from '../errors.js';
|
|
5
|
+
/** Exact capability-gap message from plan §0.1. */
|
|
6
|
+
const CREATE_UNAVAILABLE = 'workspace create is unavailable: SDK method createWorkspace not found — upgrade @commissionsight/sdk or confirm POST /workspaces (see docs.commissionsight.com)';
|
|
7
|
+
export function workspaceCommands() {
|
|
8
|
+
cover('listWorkspaces', 'cs workspace list');
|
|
9
|
+
cover('createWorkspace', 'cs workspace create');
|
|
10
|
+
return [
|
|
11
|
+
{
|
|
12
|
+
path: ['workspace', 'list'],
|
|
13
|
+
summary: 'List workspaces for the account',
|
|
14
|
+
async run(ctx) {
|
|
15
|
+
const ws = await ctx.client().listWorkspaces();
|
|
16
|
+
return { enabled: ws.enabled, workspaces: ws.workspaces };
|
|
17
|
+
},
|
|
18
|
+
render(data, ctx) {
|
|
19
|
+
const d = data;
|
|
20
|
+
ctx.io.stdout(`multi-workspace: ${d.enabled ? 'enabled' : 'disabled'}\n`);
|
|
21
|
+
for (const w of d.workspaces) {
|
|
22
|
+
ctx.io.stdout(` ${w.isDefault ? '*' : ' '} ${w.id} ${w.name}\n`);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: ['workspace', 'create'],
|
|
28
|
+
summary: 'Create a workspace (requires SDK createWorkspace; see §0.1)',
|
|
29
|
+
args: [{ name: 'name', required: true }],
|
|
30
|
+
async run(ctx, parsed) {
|
|
31
|
+
const name = parsed.args[0];
|
|
32
|
+
const client = ctx.client();
|
|
33
|
+
if (typeof client.createWorkspace !== 'function') {
|
|
34
|
+
throw new CliError(CREATE_UNAVAILABLE, EXIT.ERROR);
|
|
35
|
+
}
|
|
36
|
+
return client.createWorkspace(name);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
path: ['workspace', 'use'],
|
|
41
|
+
summary: 'Set the default workspace for the active context',
|
|
42
|
+
args: [{ name: 'nameOrId', required: true }],
|
|
43
|
+
async run(ctx, parsed) {
|
|
44
|
+
const ref = parsed.args[0];
|
|
45
|
+
const ws = await getWorkspaces(ctx);
|
|
46
|
+
const lower = ref.toLowerCase();
|
|
47
|
+
const match = ws.workspaces.find((w) => w.id === ref) ??
|
|
48
|
+
ws.workspaces.find((w) => w.name.toLowerCase() === lower);
|
|
49
|
+
if (!match) {
|
|
50
|
+
throw new UsageError(`unknown workspace: ${ref}`, ws.workspaces.map((w) => `${w.name} → ${w.id}`));
|
|
51
|
+
}
|
|
52
|
+
const contextName = ctx.globals.context || loadConfig(ctx.io).current;
|
|
53
|
+
upsertContext(ctx.io, contextName, {
|
|
54
|
+
defaultWorkspaceId: match.id,
|
|
55
|
+
defaultWorkspaceName: match.name,
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
context: contextName,
|
|
59
|
+
defaultWorkspaceId: match.id,
|
|
60
|
+
defaultWorkspaceName: match.name,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file schema (plan §4.2) + validation. One file holds many named
|
|
3
|
+
* contexts (environments); `current` selects the active one.
|
|
4
|
+
*/
|
|
5
|
+
export interface Context {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
/** Omitted when the user relies on env/flags for the token. */
|
|
8
|
+
token?: string;
|
|
9
|
+
defaultWorkspaceId?: string;
|
|
10
|
+
defaultWorkspaceName?: string;
|
|
11
|
+
defaultCarrierId?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Config {
|
|
14
|
+
version: 1;
|
|
15
|
+
current: string;
|
|
16
|
+
contexts: Record<string, Context>;
|
|
17
|
+
}
|
|
18
|
+
export declare const DEFAULT_CONTEXT_NAME = "default";
|
|
19
|
+
export declare function emptyConfig(): Config;
|
|
20
|
+
/** Coerce unknown parsed JSON into a valid Config, dropping junk. */
|
|
21
|
+
export declare function validateConfig(raw: unknown): Config;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const DEFAULT_CONTEXT_NAME = 'default';
|
|
2
|
+
export function emptyConfig() {
|
|
3
|
+
return { version: 1, current: DEFAULT_CONTEXT_NAME, contexts: {} };
|
|
4
|
+
}
|
|
5
|
+
/** Coerce unknown parsed JSON into a valid Config, dropping junk. */
|
|
6
|
+
export function validateConfig(raw) {
|
|
7
|
+
if (!raw || typeof raw !== 'object')
|
|
8
|
+
return emptyConfig();
|
|
9
|
+
const r = raw;
|
|
10
|
+
const contexts = {};
|
|
11
|
+
if (r['contexts'] && typeof r['contexts'] === 'object') {
|
|
12
|
+
for (const [name, v] of Object.entries(r['contexts'])) {
|
|
13
|
+
if (!v || typeof v !== 'object')
|
|
14
|
+
continue;
|
|
15
|
+
const c = v;
|
|
16
|
+
contexts[name] = {
|
|
17
|
+
baseUrl: typeof c['baseUrl'] === 'string' ? c['baseUrl'] : '',
|
|
18
|
+
...(typeof c['token'] === 'string' ? { token: c['token'] } : {}),
|
|
19
|
+
...(typeof c['defaultWorkspaceId'] === 'string'
|
|
20
|
+
? { defaultWorkspaceId: c['defaultWorkspaceId'] }
|
|
21
|
+
: {}),
|
|
22
|
+
...(typeof c['defaultWorkspaceName'] === 'string'
|
|
23
|
+
? { defaultWorkspaceName: c['defaultWorkspaceName'] }
|
|
24
|
+
: {}),
|
|
25
|
+
...(typeof c['defaultCarrierId'] === 'string'
|
|
26
|
+
? { defaultCarrierId: c['defaultCarrierId'] }
|
|
27
|
+
: {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const current = typeof r['current'] === 'string' ? r['current'] : DEFAULT_CONTEXT_NAME;
|
|
32
|
+
return { version: 1, current, contexts };
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IO } from '../io.js';
|
|
2
|
+
import { type Config, type Context } from './schema.js';
|
|
3
|
+
export declare function configFilePath(io: IO): string;
|
|
4
|
+
export declare function loadConfig(io: IO): Config;
|
|
5
|
+
export declare function saveConfig(io: IO, config: Config): void;
|
|
6
|
+
/** The active context name (CLI --context flag wins, else config.current). */
|
|
7
|
+
export declare function activeContextName(io: IO, config: Config, override?: string): string;
|
|
8
|
+
/** Get (name, context) for the active context, or null if it doesn't exist. */
|
|
9
|
+
export declare function activeContext(io: IO, override?: string): {
|
|
10
|
+
name: string;
|
|
11
|
+
context: Context | undefined;
|
|
12
|
+
config: Config;
|
|
13
|
+
};
|
|
14
|
+
/** Mutate + persist a context by name, creating it if absent. */
|
|
15
|
+
export declare function upsertContext(io: IO, name: string, patch: Partial<Context>, opts?: {
|
|
16
|
+
setCurrent?: boolean;
|
|
17
|
+
}): Config;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read/write the config file (plan §4.2). Directory created 0700, file 0600.
|
|
3
|
+
* On write we re-assert perms and warn (to stderr) if they had been loosened.
|
|
4
|
+
* All paths flow through the injected IO so tests sandbox to a temp dir.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from 'node:fs';
|
|
7
|
+
import { homedir, platform } from 'node:os';
|
|
8
|
+
import { dirname, join } from 'node:path';
|
|
9
|
+
import { emptyConfig, validateConfig, DEFAULT_CONTEXT_NAME } from './schema.js';
|
|
10
|
+
export function configFilePath(io) {
|
|
11
|
+
if (io.configPath)
|
|
12
|
+
return io.configPath;
|
|
13
|
+
const base = io.env['XDG_CONFIG_HOME'] || join(homedir(), '.config');
|
|
14
|
+
return join(base, 'commissionsight', 'config.json');
|
|
15
|
+
}
|
|
16
|
+
export function loadConfig(io) {
|
|
17
|
+
const path = configFilePath(io);
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return emptyConfig();
|
|
20
|
+
try {
|
|
21
|
+
return validateConfig(JSON.parse(readFileSync(path, 'utf8')));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return emptyConfig();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const isPosix = platform() !== 'win32';
|
|
28
|
+
export function saveConfig(io, config) {
|
|
29
|
+
const path = configFilePath(io);
|
|
30
|
+
const dir = dirname(path);
|
|
31
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
// Warn if an existing file had looser-than-0600 perms (POSIX only).
|
|
33
|
+
if (isPosix && existsSync(path)) {
|
|
34
|
+
try {
|
|
35
|
+
const mode = statSync(path).mode & 0o777;
|
|
36
|
+
if (mode & 0o077) {
|
|
37
|
+
io.stderr(`warning: ${path} had loose permissions (${mode.toString(8)}); tightening to 600\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* ignore */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
writeFileSync(path, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
45
|
+
if (isPosix) {
|
|
46
|
+
try {
|
|
47
|
+
chmodSync(dir, 0o700);
|
|
48
|
+
chmodSync(path, 0o600);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* best effort */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The active context name (CLI --context flag wins, else config.current). */
|
|
56
|
+
export function activeContextName(io, config, override) {
|
|
57
|
+
return override || config.current || DEFAULT_CONTEXT_NAME;
|
|
58
|
+
}
|
|
59
|
+
/** Get (name, context) for the active context, or null if it doesn't exist. */
|
|
60
|
+
export function activeContext(io, override) {
|
|
61
|
+
const config = loadConfig(io);
|
|
62
|
+
const name = activeContextName(io, config, override);
|
|
63
|
+
return { name, context: config.contexts[name], config };
|
|
64
|
+
}
|
|
65
|
+
/** Mutate + persist a context by name, creating it if absent. */
|
|
66
|
+
export function upsertContext(io, name, patch, opts = {}) {
|
|
67
|
+
const config = loadConfig(io);
|
|
68
|
+
const existing = config.contexts[name] ?? { baseUrl: '' };
|
|
69
|
+
config.contexts[name] = { ...existing, ...patch };
|
|
70
|
+
if (opts.setCurrent)
|
|
71
|
+
config.current = name;
|
|
72
|
+
saveConfig(io, config);
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { CommissionSightClient } from '@commissionsight/sdk';
|
|
2
|
+
import type { Globals, RunCtx } from './types.js';
|
|
3
|
+
import type { IO } from './io.js';
|
|
4
|
+
export declare const DEFAULT_BASE_URL = "https://api.commissionsight.com/v1";
|
|
5
|
+
/** Resolved configuration slice the client factory needs from the config file. */
|
|
6
|
+
export interface ConfigResolution {
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveToken(globals: Globals, io: IO): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a token from explicit sources only (flag > file > stdin > env),
|
|
13
|
+
* ignoring the config file. Used by `auth login` to capture the token being
|
|
14
|
+
* stored without re-reading stdin via the general resolver.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveExplicitToken(globals: Globals, io: IO): string | undefined;
|
|
17
|
+
/** Build a client for an explicit base URL + token (verification flows). */
|
|
18
|
+
export declare function clientFor(baseUrl: string, token: string | undefined, io: IO): CommissionSightClient;
|
|
19
|
+
export declare function resolveBaseUrl(globals: Globals, io: IO): string;
|
|
20
|
+
export declare function makeClient(globals: Globals, io: IO): CommissionSightClient;
|
|
21
|
+
/** Build a RunCtx with a lazily-constructed, memoized client. */
|
|
22
|
+
export declare function makeRunCtx(globals: Globals, io: IO): RunCtx;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK client factory (plan §8). Resolves base URL + token by precedence and
|
|
3
|
+
* constructs the SDK client with an injectable fetch (mock in tests).
|
|
4
|
+
*
|
|
5
|
+
* Token precedence (highest first, plan §4.1):
|
|
6
|
+
* 1. --token 2. --token-file / --token-stdin
|
|
7
|
+
* 3. COMMISSIONSIGHT_TOKEN env 4. active context token (config file)
|
|
8
|
+
*
|
|
9
|
+
* The config-file layer is wired in Phase 2 via `resolveFromConfig`; until then
|
|
10
|
+
* it returns undefined and resolution falls through to env/flags.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { CommissionSightClient } from '@commissionsight/sdk';
|
|
14
|
+
import { UsageError } from './errors.js';
|
|
15
|
+
import { activeContext } from './config/store.js';
|
|
16
|
+
export const DEFAULT_BASE_URL = 'https://api.commissionsight.com/v1';
|
|
17
|
+
/** Read base URL + token from the active config context. */
|
|
18
|
+
function configResolver(globals, io) {
|
|
19
|
+
const { context } = activeContext(io, globals.context);
|
|
20
|
+
if (!context)
|
|
21
|
+
return {};
|
|
22
|
+
return {
|
|
23
|
+
...(context.baseUrl ? { baseUrl: context.baseUrl } : {}),
|
|
24
|
+
...(context.token ? { token: context.token } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function resolveToken(globals, io) {
|
|
28
|
+
// Explicit sources (flag > file > stdin > env) win; else the config context.
|
|
29
|
+
return resolveExplicitToken(globals, io) ?? configResolver(globals, io).token;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a token from explicit sources only (flag > file > stdin > env),
|
|
33
|
+
* ignoring the config file. Used by `auth login` to capture the token being
|
|
34
|
+
* stored without re-reading stdin via the general resolver.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveExplicitToken(globals, io) {
|
|
37
|
+
if (globals.token)
|
|
38
|
+
return globals.token;
|
|
39
|
+
if (globals.tokenFile) {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(globals.tokenFile, 'utf8').trim();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new UsageError(`could not read token file: ${globals.tokenFile}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (globals.tokenStdin) {
|
|
48
|
+
try {
|
|
49
|
+
return readFileSync(0, 'utf8').trim();
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new UsageError('could not read token from stdin');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const envTok = io.env['COMMISSIONSIGHT_TOKEN'];
|
|
56
|
+
return envTok ? envTok.trim() : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Build a client for an explicit base URL + token (verification flows). */
|
|
59
|
+
export function clientFor(baseUrl, token, io) {
|
|
60
|
+
return new CommissionSightClient({
|
|
61
|
+
baseUrl,
|
|
62
|
+
...(token ? { token } : {}),
|
|
63
|
+
...(io.fetch ? { fetch: io.fetch } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function resolveBaseUrl(globals, io) {
|
|
67
|
+
return (globals.baseUrl ??
|
|
68
|
+
io.env['COMMISSIONSIGHT_BASE_URL'] ??
|
|
69
|
+
configResolver(globals, io).baseUrl ??
|
|
70
|
+
DEFAULT_BASE_URL);
|
|
71
|
+
}
|
|
72
|
+
export function makeClient(globals, io) {
|
|
73
|
+
const baseUrl = resolveBaseUrl(globals, io);
|
|
74
|
+
const token = resolveToken(globals, io);
|
|
75
|
+
return new CommissionSightClient({
|
|
76
|
+
baseUrl,
|
|
77
|
+
...(token ? { token } : {}),
|
|
78
|
+
...(io.fetch ? { fetch: io.fetch } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Build a RunCtx with a lazily-constructed, memoized client. */
|
|
82
|
+
export function makeRunCtx(globals, io) {
|
|
83
|
+
let client = null;
|
|
84
|
+
return {
|
|
85
|
+
globals,
|
|
86
|
+
io,
|
|
87
|
+
cache: new Map(),
|
|
88
|
+
client() {
|
|
89
|
+
if (!client)
|
|
90
|
+
client = makeClient(globals, io);
|
|
91
|
+
return client;
|
|
92
|
+
},
|
|
93
|
+
log(msg) {
|
|
94
|
+
// Logs go to stderr, suppressed by --quiet and by --json (chrome-free).
|
|
95
|
+
if (globals.quiet || globals.json)
|
|
96
|
+
return;
|
|
97
|
+
io.stderr(msg + '\n');
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types and the ApiError.status → exit-code mapping (plan §5.2).
|
|
3
|
+
* Commands throw; the router maps to an exit code and the JSON envelope.
|
|
4
|
+
*/
|
|
5
|
+
import { ApiError } from '@commissionsight/sdk';
|
|
6
|
+
export declare const EXIT: {
|
|
7
|
+
readonly OK: 0;
|
|
8
|
+
readonly ERROR: 1;
|
|
9
|
+
readonly USAGE: 2;
|
|
10
|
+
readonly AUTH: 3;
|
|
11
|
+
readonly NOTFOUND: 4;
|
|
12
|
+
readonly CONFLICT: 5;
|
|
13
|
+
readonly VALIDATION: 6;
|
|
14
|
+
readonly RATELIMIT: 7;
|
|
15
|
+
readonly TIMEOUT: 124;
|
|
16
|
+
};
|
|
17
|
+
/** Bad flags/args/usage — exit 2. */
|
|
18
|
+
export declare class UsageError extends Error {
|
|
19
|
+
readonly exitCode: 2;
|
|
20
|
+
/** Optional candidate list (e.g. ambiguous carrier names). */
|
|
21
|
+
readonly candidates?: string[];
|
|
22
|
+
constructor(message: string, candidates?: string[]);
|
|
23
|
+
}
|
|
24
|
+
/** A poll/wait exceeded its timeout — exit 124. */
|
|
25
|
+
export declare class TimeoutError extends Error {
|
|
26
|
+
readonly exitCode: 124;
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
/** Generic CLI error with an explicit exit code (e.g. capability gaps). */
|
|
30
|
+
export declare class CliError extends Error {
|
|
31
|
+
readonly exitCode: number;
|
|
32
|
+
constructor(message: string, exitCode?: number);
|
|
33
|
+
}
|
|
34
|
+
/** Map any thrown value to a process exit code. */
|
|
35
|
+
export declare function exitCodeFor(err: unknown): number;
|
|
36
|
+
export declare function exitCodeForStatus(status: number): number;
|
|
37
|
+
export { ApiError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types and the ApiError.status → exit-code mapping (plan §5.2).
|
|
3
|
+
* Commands throw; the router maps to an exit code and the JSON envelope.
|
|
4
|
+
*/
|
|
5
|
+
import { ApiError } from '@commissionsight/sdk';
|
|
6
|
+
export const EXIT = {
|
|
7
|
+
OK: 0,
|
|
8
|
+
ERROR: 1,
|
|
9
|
+
USAGE: 2,
|
|
10
|
+
AUTH: 3,
|
|
11
|
+
NOTFOUND: 4,
|
|
12
|
+
CONFLICT: 5,
|
|
13
|
+
VALIDATION: 6,
|
|
14
|
+
RATELIMIT: 7,
|
|
15
|
+
TIMEOUT: 124,
|
|
16
|
+
};
|
|
17
|
+
/** Bad flags/args/usage — exit 2. */
|
|
18
|
+
export class UsageError extends Error {
|
|
19
|
+
exitCode = EXIT.USAGE;
|
|
20
|
+
/** Optional candidate list (e.g. ambiguous carrier names). */
|
|
21
|
+
candidates;
|
|
22
|
+
constructor(message, candidates) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'UsageError';
|
|
25
|
+
this.candidates = candidates;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A poll/wait exceeded its timeout — exit 124. */
|
|
29
|
+
export class TimeoutError extends Error {
|
|
30
|
+
exitCode = EXIT.TIMEOUT;
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'TimeoutError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Generic CLI error with an explicit exit code (e.g. capability gaps). */
|
|
37
|
+
export class CliError extends Error {
|
|
38
|
+
exitCode;
|
|
39
|
+
constructor(message, exitCode = EXIT.ERROR) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'CliError';
|
|
42
|
+
this.exitCode = exitCode;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Map any thrown value to a process exit code. */
|
|
46
|
+
export function exitCodeFor(err) {
|
|
47
|
+
if (err instanceof UsageError)
|
|
48
|
+
return EXIT.USAGE;
|
|
49
|
+
if (err instanceof TimeoutError)
|
|
50
|
+
return EXIT.TIMEOUT;
|
|
51
|
+
if (err instanceof CliError)
|
|
52
|
+
return err.exitCode;
|
|
53
|
+
if (err instanceof ApiError)
|
|
54
|
+
return exitCodeForStatus(err.status);
|
|
55
|
+
return EXIT.ERROR;
|
|
56
|
+
}
|
|
57
|
+
export function exitCodeForStatus(status) {
|
|
58
|
+
if (status === 401 || status === 403)
|
|
59
|
+
return EXIT.AUTH;
|
|
60
|
+
if (status === 404)
|
|
61
|
+
return EXIT.NOTFOUND;
|
|
62
|
+
if (status === 409)
|
|
63
|
+
return EXIT.CONFLICT;
|
|
64
|
+
if (status === 400 || status === 422)
|
|
65
|
+
return EXIT.VALIDATION;
|
|
66
|
+
if (status === 429)
|
|
67
|
+
return EXIT.RATELIMIT;
|
|
68
|
+
return EXIT.ERROR;
|
|
69
|
+
}
|
|
70
|
+
export { ApiError };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global flag specs (allowed on any command, parsed before dispatch) and the
|
|
3
|
+
* resolver that turns parsed values + env into the typed `Globals` object.
|
|
4
|
+
*/
|
|
5
|
+
import type { OptSpec, Globals } from './types.js';
|
|
6
|
+
import type { IO } from './io.js';
|
|
7
|
+
export declare const GLOBAL_OPTIONS: Record<string, OptSpec>;
|
|
8
|
+
/** Keys that are global control flags, not passed down to commands as data. */
|
|
9
|
+
export declare const GLOBAL_KEYS: Set<string>;
|
|
10
|
+
export declare function resolveGlobals(values: Record<string, string | boolean | string[] | undefined>, io: IO): Globals;
|