@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
package/dist/globals.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const GLOBAL_OPTIONS = {
|
|
2
|
+
json: { type: 'boolean', desc: 'Emit only the JSON envelope on stdout (also CS_OUTPUT=json)' },
|
|
3
|
+
quiet: { type: 'boolean', short: 'q', desc: 'Suppress human chrome (spinners, headers)' },
|
|
4
|
+
'no-color': { type: 'boolean', desc: 'Disable ANSI color (also NO_COLOR)' },
|
|
5
|
+
yes: { type: 'boolean', short: 'y', desc: 'Assume "yes" for destructive confirmations' },
|
|
6
|
+
token: { type: 'string', desc: 'API bearer token (discouraged on a TTY)', placeholder: '<t>' },
|
|
7
|
+
'token-file': { type: 'string', desc: 'Read API token from a file', placeholder: '<path>' },
|
|
8
|
+
'token-stdin': { type: 'boolean', desc: 'Read API token from stdin' },
|
|
9
|
+
'base-url': { type: 'string', desc: 'Override API base URL', placeholder: '<url>' },
|
|
10
|
+
context: { type: 'string', desc: 'Use a named configuration context', placeholder: '<name>' },
|
|
11
|
+
help: { type: 'boolean', short: 'h', desc: 'Show help' },
|
|
12
|
+
version: { type: 'boolean', short: 'V', desc: 'Show version' },
|
|
13
|
+
};
|
|
14
|
+
/** Keys that are global control flags, not passed down to commands as data. */
|
|
15
|
+
export const GLOBAL_KEYS = new Set(Object.keys(GLOBAL_OPTIONS));
|
|
16
|
+
export function resolveGlobals(values, io) {
|
|
17
|
+
const json = Boolean(values['json']) || io.env['CS_OUTPUT'] === 'json';
|
|
18
|
+
const noColor = Boolean(values['no-color']) || io.env['NO_COLOR'] !== undefined && io.env['NO_COLOR'] !== '';
|
|
19
|
+
const color = io.isTTY && !noColor && !json;
|
|
20
|
+
return {
|
|
21
|
+
json,
|
|
22
|
+
quiet: Boolean(values['quiet']),
|
|
23
|
+
color,
|
|
24
|
+
yes: Boolean(values['yes']),
|
|
25
|
+
token: str(values['token']),
|
|
26
|
+
tokenFile: str(values['token-file']),
|
|
27
|
+
tokenStdin: Boolean(values['token-stdin']),
|
|
28
|
+
baseUrl: str(values['base-url']),
|
|
29
|
+
context: str(values['context']),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function str(v) {
|
|
33
|
+
if (typeof v === 'string')
|
|
34
|
+
return v;
|
|
35
|
+
if (Array.isArray(v))
|
|
36
|
+
return v[v.length - 1];
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
package/dist/io.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IO abstraction so the whole CLI is testable without touching real stdout/
|
|
3
|
+
* stderr/env/filesystem. Tests inject capturing writers and a mock fetch.
|
|
4
|
+
*/
|
|
5
|
+
export interface IO {
|
|
6
|
+
stdout(s: string): void;
|
|
7
|
+
stderr(s: string): void;
|
|
8
|
+
env: Record<string, string | undefined>;
|
|
9
|
+
cwd: string;
|
|
10
|
+
/** Injectable fetch threaded into the SDK client (mock-fetch in tests). */
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
/** Whether stdout is an interactive TTY (drives color + prompts). */
|
|
13
|
+
isTTY: boolean;
|
|
14
|
+
/** Override config file path (tests point this at a temp dir). */
|
|
15
|
+
configPath?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Build the default IO bound to the real process. */
|
|
18
|
+
export declare function defaultIO(): IO;
|
|
19
|
+
/** Build an IO that captures stdout/stderr into strings, for tests. */
|
|
20
|
+
export interface CaptureIO extends IO {
|
|
21
|
+
out: {
|
|
22
|
+
value: string;
|
|
23
|
+
};
|
|
24
|
+
err: {
|
|
25
|
+
value: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function captureIO(overrides?: Partial<IO>): CaptureIO;
|
package/dist/io.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Build the default IO bound to the real process. */
|
|
2
|
+
export function defaultIO() {
|
|
3
|
+
return {
|
|
4
|
+
stdout: (s) => process.stdout.write(s),
|
|
5
|
+
stderr: (s) => process.stderr.write(s),
|
|
6
|
+
env: process.env,
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function captureIO(overrides = {}) {
|
|
12
|
+
const out = { value: '' };
|
|
13
|
+
const err = { value: '' };
|
|
14
|
+
return {
|
|
15
|
+
stdout: (s) => {
|
|
16
|
+
out.value += s;
|
|
17
|
+
},
|
|
18
|
+
stderr: (s) => {
|
|
19
|
+
err.value += s;
|
|
20
|
+
},
|
|
21
|
+
env: {},
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
isTTY: false,
|
|
24
|
+
out,
|
|
25
|
+
err,
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { RunCtx } from '../types.js';
|
|
2
|
+
export interface BatchTask {
|
|
3
|
+
file: string;
|
|
4
|
+
carrier: string;
|
|
5
|
+
period: string;
|
|
6
|
+
workspace?: string;
|
|
7
|
+
replace?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface BatchOutcome {
|
|
10
|
+
file: string;
|
|
11
|
+
carrier: string;
|
|
12
|
+
period: string;
|
|
13
|
+
jobId?: string;
|
|
14
|
+
fileId?: string;
|
|
15
|
+
mode?: string;
|
|
16
|
+
status: 'uploaded' | 'replaced' | 'skipped' | 'failed' | 'planned' | 'unmatched';
|
|
17
|
+
jobStatus?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface BatchSummary {
|
|
21
|
+
total: number;
|
|
22
|
+
uploaded: number;
|
|
23
|
+
replaced: number;
|
|
24
|
+
skipped: number;
|
|
25
|
+
failed: number;
|
|
26
|
+
planned: number;
|
|
27
|
+
unmatched: number;
|
|
28
|
+
outcomes: BatchOutcome[];
|
|
29
|
+
}
|
|
30
|
+
export interface BatchOptions {
|
|
31
|
+
target: string;
|
|
32
|
+
manifest?: string;
|
|
33
|
+
pattern?: string;
|
|
34
|
+
concurrency?: number;
|
|
35
|
+
workspace?: string;
|
|
36
|
+
replace?: boolean;
|
|
37
|
+
wait?: boolean;
|
|
38
|
+
dryRun?: boolean;
|
|
39
|
+
continueOnError?: boolean;
|
|
40
|
+
strict?: boolean;
|
|
41
|
+
timeoutMs?: number;
|
|
42
|
+
intervalMs?: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildTasks(opts: BatchOptions): {
|
|
45
|
+
tasks: BatchTask[];
|
|
46
|
+
unmatched: string[];
|
|
47
|
+
};
|
|
48
|
+
/** Tiny RFC-4180-ish CSV reader (handles quotes + embedded commas/newlines). */
|
|
49
|
+
export declare function parseCsv(text: string): Record<string, unknown>[];
|
|
50
|
+
/** Bounded worker pool preserving input order in the results array. */
|
|
51
|
+
export declare function runPool<T, R>(items: T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
52
|
+
export declare function runBatch(ctx: RunCtx, opts: BatchOptions): Promise<BatchSummary>;
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive-op gate (plan §5): require --yes in non-interactive contexts;
|
|
3
|
+
* prompt on a TTY. Commands call this before retract/purge/remove/delete.
|
|
4
|
+
*/
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { UsageError, CliError, EXIT } from '../errors.js';
|
|
7
|
+
export async function confirmDestructive(ctx, action) {
|
|
8
|
+
if (ctx.globals.yes)
|
|
9
|
+
return;
|
|
10
|
+
if (!ctx.io.isTTY) {
|
|
11
|
+
throw new UsageError(`refusing to ${action} without confirmation; re-run with --yes/-y`);
|
|
12
|
+
}
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
14
|
+
try {
|
|
15
|
+
const answer = (await rl.question(`About to ${action}. Continue? [y/N] `)).trim().toLowerCase();
|
|
16
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
17
|
+
throw new CliError('aborted by user', EXIT.ERROR);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
rl.close();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function sniffMime(path: string): string;
|
|
2
|
+
/** Read a file from disk into a `File` with its basename + sniffed mime. */
|
|
3
|
+
export declare function loadFile(path: string): File;
|
|
4
|
+
/** First 8 hex chars of the SHA-256 of a file's contents (idempotency keys). */
|
|
5
|
+
export declare function sha8(path: string): string;
|
|
6
|
+
export declare function sha8OfBuffer(buf: Uint8Array): string;
|
package/dist/lib/file.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File loading (plan §9.3). The server derives `originalFilename` from the
|
|
3
|
+
* multipart part name, so the CLI must wrap bytes in a `File` carrying the
|
|
4
|
+
* basename — never a bare Blob. Also: mime sniffing + short content hash for
|
|
5
|
+
* idempotency keys.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
8
|
+
import { basename, extname } from 'node:path';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { CliError, UsageError, EXIT } from '../errors.js';
|
|
11
|
+
const MIME = {
|
|
12
|
+
'.csv': 'text/csv',
|
|
13
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
14
|
+
'.xls': 'application/vnd.ms-excel',
|
|
15
|
+
};
|
|
16
|
+
export function sniffMime(path) {
|
|
17
|
+
const ext = extname(path).toLowerCase();
|
|
18
|
+
const mime = MIME[ext];
|
|
19
|
+
if (!mime) {
|
|
20
|
+
throw new UsageError(`unsupported file type: ${ext || '(none)'} (expected .csv or .xlsx) for ${basename(path)}`);
|
|
21
|
+
}
|
|
22
|
+
return mime;
|
|
23
|
+
}
|
|
24
|
+
/** Read a file from disk into a `File` with its basename + sniffed mime. */
|
|
25
|
+
export function loadFile(path) {
|
|
26
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
27
|
+
throw new CliError(`file not found: ${path}`, EXIT.NOTFOUND);
|
|
28
|
+
}
|
|
29
|
+
const type = sniffMime(path);
|
|
30
|
+
const buf = readFileSync(path);
|
|
31
|
+
// Wrap in a Uint8Array view so the global File constructor accepts it.
|
|
32
|
+
return new File([new Uint8Array(buf)], basename(path), { type });
|
|
33
|
+
}
|
|
34
|
+
/** First 8 hex chars of the SHA-256 of a file's contents (idempotency keys). */
|
|
35
|
+
export function sha8(path) {
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
throw new CliError(`file not found: ${path}`, EXIT.NOTFOUND);
|
|
38
|
+
const hash = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
39
|
+
return hash.slice(0, 8);
|
|
40
|
+
}
|
|
41
|
+
export function sha8OfBuffer(buf) {
|
|
42
|
+
return createHash('sha256').update(buf).digest('hex').slice(0, 8);
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read structured input from a file (`-f`) or stdin. Used by config create/test
|
|
3
|
+
* which take a JSON config document.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { CliError, UsageError, EXIT } from '../errors.js';
|
|
7
|
+
/** Read JSON from a path, or stdin (fd 0) when no path is given. */
|
|
8
|
+
export function readJsonInput(path) {
|
|
9
|
+
let text;
|
|
10
|
+
if (path) {
|
|
11
|
+
try {
|
|
12
|
+
text = readFileSync(path, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new CliError(`could not read file: ${path}`, EXIT.NOTFOUND);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
try {
|
|
20
|
+
text = readFileSync(0, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new UsageError('provide a config via -f <file.json> or pipe JSON on stdin');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!text.trim()) {
|
|
27
|
+
throw new UsageError('empty config input (expected JSON)');
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
throw new UsageError(`invalid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-pagination helpers (plan §9.5) powering `--all`. Offset endpoints follow
|
|
3
|
+
* `pagination.hasMore`; cursor endpoints (listFiles) follow `nextCursor`. Both
|
|
4
|
+
* cap total pages to avoid runaway loops and report truncation to the caller.
|
|
5
|
+
*/
|
|
6
|
+
export interface PageLike<T> {
|
|
7
|
+
data: T[];
|
|
8
|
+
pagination?: {
|
|
9
|
+
limit?: number;
|
|
10
|
+
offset?: number;
|
|
11
|
+
nextCursor?: number | null;
|
|
12
|
+
hasMore?: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface CollectResult<T> {
|
|
16
|
+
items: T[];
|
|
17
|
+
pages: number;
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function collectOffset<T>(fn: (params: {
|
|
21
|
+
limit: number;
|
|
22
|
+
offset: number;
|
|
23
|
+
}) => Promise<PageLike<T>>, opts?: {
|
|
24
|
+
limit?: number;
|
|
25
|
+
maxPages?: number;
|
|
26
|
+
}): Promise<CollectResult<T>>;
|
|
27
|
+
export declare function collectCursor<T>(fn: (params: {
|
|
28
|
+
limit: number;
|
|
29
|
+
cursor?: number;
|
|
30
|
+
}) => Promise<PageLike<T>>, opts?: {
|
|
31
|
+
limit?: number;
|
|
32
|
+
maxPages?: number;
|
|
33
|
+
}): Promise<CollectResult<T>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 100;
|
|
2
|
+
const DEFAULT_MAX_PAGES = 1000;
|
|
3
|
+
export async function collectOffset(fn, opts = {}) {
|
|
4
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
5
|
+
const maxPages = opts.maxPages ?? DEFAULT_MAX_PAGES;
|
|
6
|
+
const items = [];
|
|
7
|
+
let offset = 0;
|
|
8
|
+
let pages = 0;
|
|
9
|
+
let truncated = false;
|
|
10
|
+
for (;;) {
|
|
11
|
+
const page = await fn({ limit, offset });
|
|
12
|
+
items.push(...page.data);
|
|
13
|
+
pages++;
|
|
14
|
+
const hasMore = page.pagination?.hasMore ?? false;
|
|
15
|
+
if (!hasMore || page.data.length === 0)
|
|
16
|
+
break;
|
|
17
|
+
if (pages >= maxPages) {
|
|
18
|
+
truncated = true;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
offset += limit;
|
|
22
|
+
}
|
|
23
|
+
return { items, pages, truncated };
|
|
24
|
+
}
|
|
25
|
+
export async function collectCursor(fn, opts = {}) {
|
|
26
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
27
|
+
const maxPages = opts.maxPages ?? DEFAULT_MAX_PAGES;
|
|
28
|
+
const items = [];
|
|
29
|
+
let cursor;
|
|
30
|
+
let pages = 0;
|
|
31
|
+
let truncated = false;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const page = await fn(cursor !== undefined ? { limit, cursor } : { limit });
|
|
34
|
+
items.push(...page.data);
|
|
35
|
+
pages++;
|
|
36
|
+
const next = page.pagination?.nextCursor;
|
|
37
|
+
const hasMore = page.pagination?.hasMore ?? next != null;
|
|
38
|
+
if (!hasMore || next == null)
|
|
39
|
+
break;
|
|
40
|
+
if (pages >= maxPages) {
|
|
41
|
+
truncated = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
cursor = next;
|
|
45
|
+
}
|
|
46
|
+
return { items, pages, truncated };
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Period {
|
|
2
|
+
periodYear: number;
|
|
3
|
+
periodMonth: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function parsePeriod(input: string): Period;
|
|
6
|
+
export declare function validatePeriod(year: number, month: number, raw?: string): Period;
|
|
7
|
+
/** Resolve a period from either --period or --year/--month. */
|
|
8
|
+
export declare function periodFromOptions(opts: {
|
|
9
|
+
period?: string;
|
|
10
|
+
year?: number;
|
|
11
|
+
month?: number;
|
|
12
|
+
}): Period;
|
|
13
|
+
export declare function formatPeriod(year: number, month: number): string;
|
|
14
|
+
/** Infer a "YYYY-MM" period from a filename, or undefined if none is present. */
|
|
15
|
+
export declare function inferPeriodFromFilename(name: string): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Period parsing/formatting (plan §9.2). The API splits periods into
|
|
3
|
+
* `periodYear` + `periodMonth`; the CLI accepts `--period YYYY-MM` or the
|
|
4
|
+
* `--year`/`--month` pair.
|
|
5
|
+
*/
|
|
6
|
+
import { UsageError } from '../errors.js';
|
|
7
|
+
export function parsePeriod(input) {
|
|
8
|
+
const m = /^(\d{4})-(\d{1,2})$/.exec(input.trim());
|
|
9
|
+
if (!m)
|
|
10
|
+
throw new UsageError(`invalid --period: "${input}" (expected YYYY-MM, e.g. 2026-05)`);
|
|
11
|
+
return validatePeriod(Number(m[1]), Number(m[2]), input);
|
|
12
|
+
}
|
|
13
|
+
export function validatePeriod(year, month, raw) {
|
|
14
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
|
15
|
+
throw new UsageError(`invalid period month: ${month} (expected 1–12)`);
|
|
16
|
+
}
|
|
17
|
+
if (!Number.isInteger(year) || year < 2000 || year > 2100) {
|
|
18
|
+
throw new UsageError(`invalid period year: ${year}${raw ? ` (from "${raw}")` : ''}`);
|
|
19
|
+
}
|
|
20
|
+
return { periodYear: year, periodMonth: month };
|
|
21
|
+
}
|
|
22
|
+
/** Resolve a period from either --period or --year/--month. */
|
|
23
|
+
export function periodFromOptions(opts) {
|
|
24
|
+
if (opts.period)
|
|
25
|
+
return parsePeriod(opts.period);
|
|
26
|
+
if (opts.year !== undefined && opts.month !== undefined) {
|
|
27
|
+
return validatePeriod(opts.year, opts.month);
|
|
28
|
+
}
|
|
29
|
+
throw new UsageError('a period is required: pass --period YYYY-MM (or --year and --month)');
|
|
30
|
+
}
|
|
31
|
+
export function formatPeriod(year, month) {
|
|
32
|
+
return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}`;
|
|
33
|
+
}
|
|
34
|
+
/** Infer a "YYYY-MM" period from a filename, or undefined if none is present. */
|
|
35
|
+
export function inferPeriodFromFilename(name) {
|
|
36
|
+
const m = /(\d{4})-(\d{2})/.exec(name);
|
|
37
|
+
if (!m)
|
|
38
|
+
return undefined;
|
|
39
|
+
const month = Number(m[2]);
|
|
40
|
+
if (month < 1 || month > 12)
|
|
41
|
+
return undefined;
|
|
42
|
+
return `${m[1]}-${m[2]}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job poller (plan §9.4). Polls getJob until a terminal state, surfacing each
|
|
3
|
+
* tick to stderr. Throws TimeoutError (→ exit 124) past the deadline. `now`
|
|
4
|
+
* and `sleep` are injectable so tests run without real time.
|
|
5
|
+
*/
|
|
6
|
+
import type { CommissionSightClient, JobSummary } from '@commissionsight/sdk';
|
|
7
|
+
export interface WaitOptions {
|
|
8
|
+
timeoutMs: number;
|
|
9
|
+
intervalMs: number;
|
|
10
|
+
onTick?: (job: JobSummary) => void;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
sleep?: (ms: number) => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function waitForJob(client: CommissionSightClient, jobId: string, opts: WaitOptions): Promise<JobSummary>;
|
package/dist/lib/poll.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TimeoutError } from '../errors.js';
|
|
2
|
+
const TERMINAL = new Set(['completed', 'failed']);
|
|
3
|
+
export async function waitForJob(client, jobId, opts) {
|
|
4
|
+
const now = opts.now ?? (() => Date.now());
|
|
5
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6
|
+
const start = now();
|
|
7
|
+
for (;;) {
|
|
8
|
+
const job = await client.getJob(jobId);
|
|
9
|
+
opts.onTick?.(job);
|
|
10
|
+
if (TERMINAL.has(job.status))
|
|
11
|
+
return job;
|
|
12
|
+
if (now() - start >= opts.timeoutMs) {
|
|
13
|
+
throw new TimeoutError(`timed out after ${Math.round(opts.timeoutMs / 1000)}s waiting for job ${jobId} (last status: ${job.status})`);
|
|
14
|
+
}
|
|
15
|
+
await sleep(opts.intervalMs);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference resolvers (plan §9.1). Carrier: id|slug|name → id. Workspace:
|
|
3
|
+
* id|name → id, with the "workspace required when enabled" rule. Both cache
|
|
4
|
+
* their list on the RunCtx so a batch resolves once.
|
|
5
|
+
*/
|
|
6
|
+
import type { RunCtx } from '../types.js';
|
|
7
|
+
interface Carrier {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
}
|
|
12
|
+
interface WorkspaceList {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
workspaces: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
isDefault: boolean;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
export declare function resolveCarrierEntry(ctx: RunCtx, ref: string): Promise<Carrier>;
|
|
21
|
+
export declare function resolveCarrier(ctx: RunCtx, ref: string): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a workspace id (or undefined for single-workspace accounts).
|
|
24
|
+
* When the account has multi-workspace enabled and no ref/default is set, this
|
|
25
|
+
* fails with exit 6 and actionable guidance.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveWorkspace(ctx: RunCtx, ref?: string): Promise<string | undefined>;
|
|
28
|
+
/** Expose the cached workspace metadata (used by `workspace list/use`). */
|
|
29
|
+
export declare function getWorkspaces(ctx: RunCtx): Promise<WorkspaceList>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { UsageError, CliError, EXIT } from '../errors.js';
|
|
2
|
+
import { activeContext } from '../config/store.js';
|
|
3
|
+
async function carriersCached(ctx) {
|
|
4
|
+
const hit = ctx.cache.get('carriers');
|
|
5
|
+
if (hit)
|
|
6
|
+
return hit;
|
|
7
|
+
const page = await ctx.client().listCarriers();
|
|
8
|
+
const list = page.data;
|
|
9
|
+
ctx.cache.set('carriers', list);
|
|
10
|
+
return list;
|
|
11
|
+
}
|
|
12
|
+
async function workspacesCached(ctx) {
|
|
13
|
+
const hit = ctx.cache.get('workspaces');
|
|
14
|
+
if (hit)
|
|
15
|
+
return hit;
|
|
16
|
+
const ws = await ctx.client().listWorkspaces();
|
|
17
|
+
ctx.cache.set('workspaces', ws);
|
|
18
|
+
return ws;
|
|
19
|
+
}
|
|
20
|
+
/** Looks like an opaque server id rather than a slug/name. */
|
|
21
|
+
function looksLikeId(ref) {
|
|
22
|
+
return /_/.test(ref) && /^[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9]{6,}$/.test(ref);
|
|
23
|
+
}
|
|
24
|
+
export async function resolveCarrierEntry(ctx, ref) {
|
|
25
|
+
const carriers = await carriersCached(ctx);
|
|
26
|
+
const byId = carriers.find((c) => c.id === ref);
|
|
27
|
+
if (byId)
|
|
28
|
+
return byId;
|
|
29
|
+
const lower = ref.toLowerCase();
|
|
30
|
+
const bySlug = carriers.filter((c) => c.slug.toLowerCase() === lower);
|
|
31
|
+
if (bySlug.length === 1)
|
|
32
|
+
return bySlug[0];
|
|
33
|
+
if (bySlug.length > 1) {
|
|
34
|
+
throw new UsageError(`ambiguous carrier slug "${ref}"`, bySlug.map((c) => `${c.name} (${c.slug}) → ${c.id}`));
|
|
35
|
+
}
|
|
36
|
+
const byName = carriers.filter((c) => c.name.toLowerCase() === lower);
|
|
37
|
+
if (byName.length === 1)
|
|
38
|
+
return byName[0];
|
|
39
|
+
if (byName.length > 1) {
|
|
40
|
+
throw new UsageError(`ambiguous carrier name "${ref}"`, byName.map((c) => `${c.name} (${c.slug}) → ${c.id}`));
|
|
41
|
+
}
|
|
42
|
+
// Unknown but id-shaped → pass through and let the API validate.
|
|
43
|
+
if (looksLikeId(ref))
|
|
44
|
+
return { id: ref, name: ref, slug: ref };
|
|
45
|
+
throw new CliError(`carrier not found: ${ref} (known: ${carriers.map((c) => c.slug).join(', ') || 'none'})`, EXIT.NOTFOUND);
|
|
46
|
+
}
|
|
47
|
+
export async function resolveCarrier(ctx, ref) {
|
|
48
|
+
return (await resolveCarrierEntry(ctx, ref)).id;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a workspace id (or undefined for single-workspace accounts).
|
|
52
|
+
* When the account has multi-workspace enabled and no ref/default is set, this
|
|
53
|
+
* fails with exit 6 and actionable guidance.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolveWorkspace(ctx, ref) {
|
|
56
|
+
const ws = await workspacesCached(ctx);
|
|
57
|
+
if (ref) {
|
|
58
|
+
const byId = ws.workspaces.find((w) => w.id === ref);
|
|
59
|
+
if (byId)
|
|
60
|
+
return byId.id;
|
|
61
|
+
const lower = ref.toLowerCase();
|
|
62
|
+
const byName = ws.workspaces.filter((w) => w.name.toLowerCase() === lower);
|
|
63
|
+
if (byName.length === 1)
|
|
64
|
+
return byName[0].id;
|
|
65
|
+
if (byName.length > 1) {
|
|
66
|
+
throw new UsageError(`ambiguous workspace name "${ref}"`, byName.map((w) => `${w.name} → ${w.id}`));
|
|
67
|
+
}
|
|
68
|
+
throw new CliError(`workspace not found: ${ref}`, EXIT.NOTFOUND);
|
|
69
|
+
}
|
|
70
|
+
if (!ws.enabled)
|
|
71
|
+
return undefined; // single-workspace account — nothing to target
|
|
72
|
+
// Multi-workspace enabled: fall back to the context default.
|
|
73
|
+
const def = activeContext(ctx.io, ctx.globals.context).context?.defaultWorkspaceId;
|
|
74
|
+
if (def)
|
|
75
|
+
return def;
|
|
76
|
+
throw new CliError('workspace required (this account has multiple workspaces). Set one with `cs workspace use <name|id>` or pass --workspace', EXIT.VALIDATION);
|
|
77
|
+
}
|
|
78
|
+
/** Expose the cached workspace metadata (used by `workspace list/use`). */
|
|
79
|
+
export async function getWorkspaces(ctx) {
|
|
80
|
+
return workspacesCached(ctx);
|
|
81
|
+
}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(argv: string[]): Promise<void>;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process entrypoint. `bin/cs.mjs` imports `run` and passes argv. This is the
|
|
3
|
+
* only place that calls process.exit; commands and the router never do.
|
|
4
|
+
*/
|
|
5
|
+
import { run as routerRun } from './router.js';
|
|
6
|
+
export async function run(argv) {
|
|
7
|
+
let code;
|
|
8
|
+
try {
|
|
9
|
+
code = await routerRun(argv);
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
// Last-resort guard; the router normally handles all errors.
|
|
13
|
+
process.stderr.write(`fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
14
|
+
code = 1;
|
|
15
|
+
}
|
|
16
|
+
process.exit(code);
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency ANSI coloring. Color is enabled only when stdout is a TTY,
|
|
3
|
+
* NO_COLOR is unset, and --no-color was not passed (decided in the router and
|
|
4
|
+
* passed in as `enabled`).
|
|
5
|
+
*/
|
|
6
|
+
declare const CODES: {
|
|
7
|
+
readonly reset: 0;
|
|
8
|
+
readonly bold: 1;
|
|
9
|
+
readonly dim: 2;
|
|
10
|
+
readonly red: 31;
|
|
11
|
+
readonly green: 32;
|
|
12
|
+
readonly yellow: 33;
|
|
13
|
+
readonly blue: 34;
|
|
14
|
+
readonly magenta: 35;
|
|
15
|
+
readonly cyan: 36;
|
|
16
|
+
readonly gray: 90;
|
|
17
|
+
};
|
|
18
|
+
export type ColorName = keyof typeof CODES;
|
|
19
|
+
export interface Painter {
|
|
20
|
+
(name: ColorName, s: string): string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function makePainter(enabled: boolean): Painter;
|
|
24
|
+
/** Map a domain status word to a color. */
|
|
25
|
+
export declare function statusColor(status: string): ColorName;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zero-dependency ANSI coloring. Color is enabled only when stdout is a TTY,
|
|
3
|
+
* NO_COLOR is unset, and --no-color was not passed (decided in the router and
|
|
4
|
+
* passed in as `enabled`).
|
|
5
|
+
*/
|
|
6
|
+
const CODES = {
|
|
7
|
+
reset: 0,
|
|
8
|
+
bold: 1,
|
|
9
|
+
dim: 2,
|
|
10
|
+
red: 31,
|
|
11
|
+
green: 32,
|
|
12
|
+
yellow: 33,
|
|
13
|
+
blue: 34,
|
|
14
|
+
magenta: 35,
|
|
15
|
+
cyan: 36,
|
|
16
|
+
gray: 90,
|
|
17
|
+
};
|
|
18
|
+
export function makePainter(enabled) {
|
|
19
|
+
const paint = ((name, s) => {
|
|
20
|
+
if (!enabled)
|
|
21
|
+
return s;
|
|
22
|
+
return `[${CODES[name]}m${s}[0m`;
|
|
23
|
+
});
|
|
24
|
+
paint.enabled = enabled;
|
|
25
|
+
return paint;
|
|
26
|
+
}
|
|
27
|
+
/** Map a domain status word to a color. */
|
|
28
|
+
export function statusColor(status) {
|
|
29
|
+
const s = status.toLowerCase();
|
|
30
|
+
if (['green', 'completed', 'ok', 'active', 'success'].includes(s))
|
|
31
|
+
return 'green';
|
|
32
|
+
if (['yellow', 'watch', 'processing', 'queued', 'pending'].includes(s))
|
|
33
|
+
return 'yellow';
|
|
34
|
+
if (['red', 'alert', 'failed', 'suspended', 'error'].includes(s))
|
|
35
|
+
return 'red';
|
|
36
|
+
return 'gray';
|
|
37
|
+
}
|