@nyrra/prismantix-cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @nyrra/prismantix-cli
2
+
3
+ Public PrismantiX export API client.
4
+
5
+ ## Usage
6
+
7
+ Run the guided export flow:
8
+
9
+ ```sh
10
+ npx -y @nyrra/prismantix-cli
11
+ ```
12
+
13
+ The guided flow reads `PRISMANTIX_API_KEY` when it is set. If it is missing, the CLI prompts for the key with masked input. The default API base is `https://api.prismantix.com`.
14
+
15
+ ## Non-Interactive Download
16
+
17
+ Queue a full client export, poll until completion, write the manifest and CSV files locally, and print machine-readable JSON to stdout:
18
+
19
+ ```sh
20
+ export PRISMANTIX_API_KEY="sk_..."
21
+ npx -y @nyrra/prismantix-cli download --client LLY --yes --json
22
+ ```
23
+
24
+ Download an existing export without queueing a new one:
25
+
26
+ ```sh
27
+ export PRISMANTIX_API_KEY="sk_..."
28
+ npx -y @nyrra/prismantix-cli download --export-id <export-id> --json
29
+ ```
30
+
31
+ Useful options:
32
+
33
+ ```sh
34
+ npx -y @nyrra/prismantix-cli download \
35
+ --api-base "https://api.prismantix.com" \
36
+ --client LLY \
37
+ --yes \
38
+ --output ./prismantix-exports \
39
+ --json
40
+ ```
41
+
42
+ `--json` keeps stdout machine-readable. Progress and errors are written to stderr, and the API key is never printed.
package/bin/dev.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
package/bin/dev.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
+
3
+ import { execute } from "@oclif/core";
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ await execute({
8
+ args: args.length === 0 ? ["guided"] : args,
9
+ development: true,
10
+ dir: import.meta.url,
11
+ });
package/bin/run.cmd ADDED
@@ -0,0 +1,3 @@
1
+ @echo off
2
+
3
+ node "%~dp0\run" %*
package/bin/run.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execute } from "@oclif/core";
4
+
5
+ const args = process.argv.slice(2);
6
+
7
+ await execute({
8
+ args: args.length === 0 ? ["guided"] : args,
9
+ dir: import.meta.url,
10
+ });
package/dist/api.d.ts ADDED
@@ -0,0 +1,179 @@
1
+ import { z } from "zod";
2
+ declare const exportFileSchema: z.ZodObject<{
3
+ downloadUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4
+ fileName: z.ZodString;
5
+ mimeType: z.ZodOptional<z.ZodString>;
6
+ rowCount: z.ZodOptional<z.ZodNumber>;
7
+ sha256: z.ZodOptional<z.ZodString>;
8
+ sizeBytes: z.ZodOptional<z.ZodNumber>;
9
+ tableName: z.ZodString;
10
+ }, z.core.$strip>;
11
+ declare const exportCreatedSchema: z.ZodObject<{
12
+ clientId: z.ZodString;
13
+ exportId: z.ZodString;
14
+ manifestUrl: z.ZodOptional<z.ZodString>;
15
+ status: z.ZodOptional<z.ZodLiteral<"queued">>;
16
+ statusUrl: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$strip>;
18
+ declare const exportStatusSchema: z.ZodObject<{
19
+ clientId: z.ZodString;
20
+ completedAt: z.ZodOptional<z.ZodNumber>;
21
+ createdAt: z.ZodOptional<z.ZodNumber>;
22
+ errorMessage: z.ZodOptional<z.ZodString>;
23
+ exportId: z.ZodString;
24
+ failedAt: z.ZodOptional<z.ZodNumber>;
25
+ files: z.ZodDefault<z.ZodArray<z.ZodObject<{
26
+ downloadUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
27
+ fileName: z.ZodString;
28
+ mimeType: z.ZodOptional<z.ZodString>;
29
+ rowCount: z.ZodOptional<z.ZodNumber>;
30
+ sha256: z.ZodOptional<z.ZodString>;
31
+ sizeBytes: z.ZodOptional<z.ZodNumber>;
32
+ tableName: z.ZodString;
33
+ }, z.core.$strip>>>;
34
+ manifestUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
35
+ startedAt: z.ZodOptional<z.ZodNumber>;
36
+ status: z.ZodEnum<{
37
+ queued: "queued";
38
+ running: "running";
39
+ completed: "completed";
40
+ failed: "failed";
41
+ }>;
42
+ totalFiles: z.ZodOptional<z.ZodNumber>;
43
+ totalRows: z.ZodOptional<z.ZodNumber>;
44
+ }, z.core.$strip>;
45
+ declare const exportManifestSchema: z.ZodPipe<z.ZodObject<{
46
+ clientId: z.ZodString;
47
+ completedAt: z.ZodOptional<z.ZodNumber>;
48
+ createdAt: z.ZodOptional<z.ZodNumber>;
49
+ exportId: z.ZodString;
50
+ files: z.ZodArray<z.ZodObject<{
51
+ downloadUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
52
+ fileName: z.ZodString;
53
+ mimeType: z.ZodOptional<z.ZodString>;
54
+ rowCount: z.ZodOptional<z.ZodNumber>;
55
+ sha256: z.ZodOptional<z.ZodString>;
56
+ sizeBytes: z.ZodOptional<z.ZodNumber>;
57
+ tableName: z.ZodString;
58
+ columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
59
+ storageKey: z.ZodOptional<z.ZodString>;
60
+ }, z.core.$strip>>;
61
+ format: z.ZodOptional<z.ZodLiteral<"csv">>;
62
+ medium: z.ZodOptional<z.ZodLiteral<"csv_bundle">>;
63
+ mode: z.ZodOptional<z.ZodLiteral<"current_client_snapshot">>;
64
+ schemaVersion: z.ZodOptional<z.ZodString>;
65
+ totalRows: z.ZodOptional<z.ZodNumber>;
66
+ }, z.core.$strip>, z.ZodTransform<{
67
+ totalRows: number;
68
+ clientId: string;
69
+ exportId: string;
70
+ files: {
71
+ fileName: string;
72
+ tableName: string;
73
+ downloadUrl?: string | null | undefined;
74
+ mimeType?: string | undefined;
75
+ rowCount?: number | undefined;
76
+ sha256?: string | undefined;
77
+ sizeBytes?: number | undefined;
78
+ columns?: string[] | undefined;
79
+ storageKey?: string | undefined;
80
+ }[];
81
+ completedAt?: number | undefined;
82
+ createdAt?: number | undefined;
83
+ format?: "csv" | undefined;
84
+ medium?: "csv_bundle" | undefined;
85
+ mode?: "current_client_snapshot" | undefined;
86
+ schemaVersion?: string | undefined;
87
+ }, {
88
+ clientId: string;
89
+ exportId: string;
90
+ files: {
91
+ fileName: string;
92
+ tableName: string;
93
+ downloadUrl?: string | null | undefined;
94
+ mimeType?: string | undefined;
95
+ rowCount?: number | undefined;
96
+ sha256?: string | undefined;
97
+ sizeBytes?: number | undefined;
98
+ columns?: string[] | undefined;
99
+ storageKey?: string | undefined;
100
+ }[];
101
+ completedAt?: number | undefined;
102
+ createdAt?: number | undefined;
103
+ format?: "csv" | undefined;
104
+ medium?: "csv_bundle" | undefined;
105
+ mode?: "current_client_snapshot" | undefined;
106
+ schemaVersion?: string | undefined;
107
+ totalRows?: number | undefined;
108
+ }>>;
109
+ export type ExportCreated = z.infer<typeof exportCreatedSchema>;
110
+ export type ExportFile = z.infer<typeof exportFileSchema>;
111
+ export type ExportManifest = z.output<typeof exportManifestSchema>;
112
+ export type ExportManifestFile = ExportManifest["files"][number];
113
+ export type ExportStatus = z.infer<typeof exportStatusSchema>;
114
+ export interface PrismantixApiClientOptions {
115
+ readonly apiBaseUrl?: string;
116
+ readonly apiKey: string;
117
+ readonly fetchImpl?: typeof fetch;
118
+ }
119
+ export declare class PrismantixApiClient {
120
+ private readonly apiBaseUrl;
121
+ private readonly apiKey;
122
+ private readonly fetchImpl;
123
+ constructor(options: PrismantixApiClientOptions);
124
+ createExport(clientId: string): Promise<{
125
+ clientId: string;
126
+ exportId: string;
127
+ manifestUrl?: string | undefined;
128
+ status?: "queued" | undefined;
129
+ statusUrl?: string | undefined;
130
+ }>;
131
+ getExportStatus(exportId: string): Promise<{
132
+ clientId: string;
133
+ exportId: string;
134
+ files: {
135
+ fileName: string;
136
+ tableName: string;
137
+ downloadUrl?: string | null | undefined;
138
+ mimeType?: string | undefined;
139
+ rowCount?: number | undefined;
140
+ sha256?: string | undefined;
141
+ sizeBytes?: number | undefined;
142
+ }[];
143
+ status: "queued" | "running" | "completed" | "failed";
144
+ completedAt?: number | undefined;
145
+ createdAt?: number | undefined;
146
+ errorMessage?: string | undefined;
147
+ failedAt?: number | undefined;
148
+ manifestUrl?: string | null | undefined;
149
+ startedAt?: number | undefined;
150
+ totalFiles?: number | undefined;
151
+ totalRows?: number | undefined;
152
+ }>;
153
+ getManifest(exportId: string): Promise<{
154
+ totalRows: number;
155
+ clientId: string;
156
+ exportId: string;
157
+ files: {
158
+ fileName: string;
159
+ tableName: string;
160
+ downloadUrl?: string | null | undefined;
161
+ mimeType?: string | undefined;
162
+ rowCount?: number | undefined;
163
+ sha256?: string | undefined;
164
+ sizeBytes?: number | undefined;
165
+ columns?: string[] | undefined;
166
+ storageKey?: string | undefined;
167
+ }[];
168
+ completedAt?: number | undefined;
169
+ createdAt?: number | undefined;
170
+ format?: "csv" | undefined;
171
+ medium?: "csv_bundle" | undefined;
172
+ mode?: "current_client_snapshot" | undefined;
173
+ schemaVersion?: string | undefined;
174
+ }>;
175
+ getFile(exportId: string, tableName: string): Promise<Response>;
176
+ private request;
177
+ }
178
+ export {};
179
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,QAAA,MAAM,gBAAgB;;;;;;;;iBAQpB,CAAC;AAEH,QAAA,MAAM,mBAAmB;;;;;;iBAMvB,CAAC;AAEH,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;iBAatB,CAAC;AAOH,QAAA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBtB,CAAC;AAEL,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC1D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACnE,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAClC;AAED,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,OAAO,EAAE,0BAA0B;IAQzC,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IAS7B,eAAe,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;IAQhC,WAAW,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;IAQ5B,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;YAUnC,OAAO;CAuBrB"}
package/dist/api.js ADDED
@@ -0,0 +1,120 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_API_BASE_URL } from "./constants.js";
3
+ import { CliUserError } from "./errors.js";
4
+ const exportFileSchema = z.object({
5
+ downloadUrl: z.string().url().nullable().optional(),
6
+ fileName: z.string(),
7
+ mimeType: z.string().optional(),
8
+ rowCount: z.number().optional(),
9
+ sha256: z.string().optional(),
10
+ sizeBytes: z.number().optional(),
11
+ tableName: z.string(),
12
+ });
13
+ const exportCreatedSchema = z.object({
14
+ clientId: z.string(),
15
+ exportId: z.string(),
16
+ manifestUrl: z.string().url().optional(),
17
+ status: z.literal("queued").optional(),
18
+ statusUrl: z.string().url().optional(),
19
+ });
20
+ const exportStatusSchema = z.object({
21
+ clientId: z.string(),
22
+ completedAt: z.number().optional(),
23
+ createdAt: z.number().optional(),
24
+ errorMessage: z.string().optional(),
25
+ exportId: z.string(),
26
+ failedAt: z.number().optional(),
27
+ files: z.array(exportFileSchema).default([]),
28
+ manifestUrl: z.string().url().nullable().optional(),
29
+ startedAt: z.number().optional(),
30
+ status: z.enum(["queued", "running", "completed", "failed"]),
31
+ totalFiles: z.number().optional(),
32
+ totalRows: z.number().optional(),
33
+ });
34
+ const manifestFileSchema = exportFileSchema.extend({
35
+ columns: z.array(z.string()).optional(),
36
+ storageKey: z.string().optional(),
37
+ });
38
+ const exportManifestSchema = z
39
+ .object({
40
+ clientId: z.string(),
41
+ completedAt: z.number().optional(),
42
+ createdAt: z.number().optional(),
43
+ exportId: z.string(),
44
+ files: z.array(manifestFileSchema),
45
+ format: z.literal("csv").optional(),
46
+ medium: z.literal("csv_bundle").optional(),
47
+ mode: z.literal("current_client_snapshot").optional(),
48
+ schemaVersion: z.string().optional(),
49
+ totalRows: z.number().optional(),
50
+ })
51
+ .transform((manifest) => ({
52
+ ...manifest,
53
+ totalRows: manifest.totalRows ??
54
+ manifest.files.reduce((sum, file) => sum + (file.rowCount ?? 0), 0),
55
+ }));
56
+ export class PrismantixApiClient {
57
+ apiBaseUrl;
58
+ apiKey;
59
+ fetchImpl;
60
+ constructor(options) {
61
+ this.apiBaseUrl = trimTrailingSlash(options.apiBaseUrl ?? DEFAULT_API_BASE_URL);
62
+ this.apiKey = options.apiKey;
63
+ this.fetchImpl = options.fetchImpl ?? fetch;
64
+ }
65
+ async createExport(clientId) {
66
+ const response = await this.request(`/v1/clients/${encodeURIComponent(clientId)}/exports`, { method: "POST" });
67
+ return exportCreatedSchema.parse(await response.json());
68
+ }
69
+ async getExportStatus(exportId) {
70
+ const response = await this.request(`/v1/exports/${encodeURIComponent(exportId)}`);
71
+ return exportStatusSchema.parse(await response.json());
72
+ }
73
+ async getManifest(exportId) {
74
+ const response = await this.request(`/v1/exports/${encodeURIComponent(exportId)}/manifest`);
75
+ return exportManifestSchema.parse(await response.json());
76
+ }
77
+ async getFile(exportId, tableName) {
78
+ return this.request(`/v1/exports/${encodeURIComponent(exportId)}/files/${encodeURIComponent(tableName)}`, {}, { accept: "*/*" });
79
+ }
80
+ async request(path, init = {}, options = {}) {
81
+ const headers = new Headers(init.headers);
82
+ headers.set("Authorization", `Bearer ${this.apiKey}`);
83
+ headers.set("Accept", options.accept ?? "application/json");
84
+ const response = await this.fetchImpl(`${this.apiBaseUrl}${path}`, {
85
+ ...init,
86
+ headers,
87
+ });
88
+ if (response.ok) {
89
+ return response;
90
+ }
91
+ throw new CliUserError(codeForStatus(response.status), await readApiError(response));
92
+ }
93
+ }
94
+ async function readApiError(response) {
95
+ try {
96
+ const payload = (await response.json());
97
+ return (payload.message ?? payload.error ?? `API returned ${response.status}.`);
98
+ }
99
+ catch {
100
+ return `API returned ${response.status}.`;
101
+ }
102
+ }
103
+ function codeForStatus(status) {
104
+ if (status === 401) {
105
+ return "unauthorized";
106
+ }
107
+ if (status === 403) {
108
+ return "forbidden";
109
+ }
110
+ if (status === 404) {
111
+ return "not_found";
112
+ }
113
+ if (status === 409) {
114
+ return "export_not_ready";
115
+ }
116
+ return "api_error";
117
+ }
118
+ function trimTrailingSlash(value) {
119
+ return value.replace(/\/+$/, "");
120
+ }
@@ -0,0 +1,22 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Download extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ "api-base": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
+ client: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ "export-id": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ "poll-interval": import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
12
+ table: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ tables: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
15
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ };
17
+ run(): Promise<void>;
18
+ private fail;
19
+ private printProgress;
20
+ private printSummary;
21
+ }
22
+ //# sourceMappingURL=download.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/commands/download.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAS,MAAM,aAAa,CAAC;AAa7C,MAAM,CAAC,OAAO,OAAO,QAAS,SAAQ,OAAO;IAC5C,OAAgB,WAAW,SACqD;IAEhF,OAAgB,QAAQ,WAGtB;IAEF,OAAgB,KAAK;;;;;;;;;;;MAwCnB;IAEW,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAmDjC,OAAO,CAAC,IAAI;IAeZ,OAAO,CAAC,aAAa;IA0BrB,OAAO,CAAC,YAAY;CAYpB"}
@@ -0,0 +1,136 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { DEFAULT_API_BASE_URL, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, } from "../constants.js";
3
+ import { CliUserError, toCliUserError } from "../errors.js";
4
+ import { runDownloadWorkflow, } from "../workflow.js";
5
+ export default class Download extends Command {
6
+ static description = "Create or download a PrismantiX client export without opening the guided UI.";
7
+ static examples = [
8
+ "<%= config.bin %> download --client LLY --yes --json",
9
+ "<%= config.bin %> download --export-id export_123 --json",
10
+ ];
11
+ static flags = {
12
+ "api-base": Flags.string({
13
+ description: "PrismantiX API base URL.",
14
+ env: "PRISMANTIX_API_BASE",
15
+ }),
16
+ client: Flags.string({
17
+ description: "Client ID to export when queueing a new export.",
18
+ }),
19
+ "export-id": Flags.string({
20
+ description: "Existing export ID to download.",
21
+ env: "PRISMANTIX_EXPORT_ID",
22
+ }),
23
+ json: Flags.boolean({
24
+ description: "Print machine-readable JSON to stdout.",
25
+ }),
26
+ output: Flags.string({
27
+ description: "Output directory.",
28
+ env: "PRISMANTIX_OUTPUT_DIR",
29
+ }),
30
+ "poll-interval": Flags.integer({
31
+ default: DEFAULT_POLL_INTERVAL_MS,
32
+ description: "Polling interval in milliseconds.",
33
+ env: "PRISMANTIX_POLL_INTERVAL_MS",
34
+ }),
35
+ table: Flags.string({
36
+ description: "Table to download; repeat for multiple tables.",
37
+ multiple: true,
38
+ }),
39
+ tables: Flags.string({
40
+ description: "Comma-separated list of tables to download.",
41
+ }),
42
+ timeout: Flags.integer({
43
+ default: DEFAULT_TIMEOUT_MS,
44
+ description: "Polling timeout in milliseconds.",
45
+ env: "PRISMANTIX_TIMEOUT_MS",
46
+ }),
47
+ yes: Flags.boolean({
48
+ char: "y",
49
+ description: "Confirm queueing a new export.",
50
+ }),
51
+ };
52
+ async run() {
53
+ const { flags } = await this.parse(Download);
54
+ try {
55
+ validatePositiveInteger(flags["poll-interval"], "--poll-interval");
56
+ validatePositiveInteger(flags.timeout, "--timeout");
57
+ const clientId = flags.client?.trim();
58
+ const exportId = flags["export-id"]?.trim();
59
+ if (!exportId && !clientId) {
60
+ throw new CliUserError("missing_input", "Pass --client to create an export or --export-id to download one.");
61
+ }
62
+ if (!exportId && !flags.yes) {
63
+ throw new CliUserError("missing_consent", "Pass --yes to confirm queueing a new export.");
64
+ }
65
+ const result = await runDownloadWorkflow({
66
+ apiBaseUrl: flags["api-base"] ?? DEFAULT_API_BASE_URL,
67
+ apiKey: process.env.PRISMANTIX_API_KEY,
68
+ clientId,
69
+ exportId,
70
+ outputDir: flags.output,
71
+ pollIntervalMs: flags["poll-interval"],
72
+ tables: collectTables(flags.table, flags.tables),
73
+ timeoutMs: flags.timeout,
74
+ }, {
75
+ onEvent: (event) => this.printProgress(event),
76
+ });
77
+ if (flags.json) {
78
+ this.log(JSON.stringify(result, null, 2));
79
+ return;
80
+ }
81
+ this.printSummary(result);
82
+ }
83
+ catch (error) {
84
+ this.fail(toCliUserError(error), flags.json);
85
+ }
86
+ }
87
+ fail(error, json) {
88
+ if (json) {
89
+ this.logToStderr(JSON.stringify({
90
+ code: error.code,
91
+ error: error.message,
92
+ ok: false,
93
+ }));
94
+ this.exit(error.exitCode);
95
+ }
96
+ this.error(error.message, { code: error.code, exit: error.exitCode });
97
+ }
98
+ printProgress(event) {
99
+ switch (event.type) {
100
+ case "created":
101
+ this.logToStderr(`Queued export ${event.exportId} for ${event.clientId}.`);
102
+ break;
103
+ case "poll":
104
+ this.logToStderr(`Export ${event.status.exportId}: ${event.status.status}.`);
105
+ break;
106
+ case "manifest":
107
+ this.logToStderr(`Manifest ready: ${event.manifest.files.length} file(s), ${event.manifest.totalRows} row(s).`);
108
+ break;
109
+ case "download-start":
110
+ this.logToStderr(`Downloading ${event.file.tableName}...`);
111
+ break;
112
+ case "download-complete":
113
+ this.logToStderr(`Downloaded ${event.file.tableName}.`);
114
+ break;
115
+ }
116
+ }
117
+ printSummary(result) {
118
+ this.log(`Downloaded PrismantiX export ${result.exportId}`);
119
+ this.log(`Output: ${result.outputDir}`);
120
+ this.log(`Manifest: ${result.manifestPath}`);
121
+ this.log(`Files: ${result.files.length}`);
122
+ for (const file of result.files) {
123
+ const rowLabel = file.rowCount === undefined ? "" : ` (${file.rowCount} row(s))`;
124
+ this.log(`- ${file.tableName}${rowLabel}: ${file.path}`);
125
+ }
126
+ }
127
+ }
128
+ function collectTables(tableFlags, tablesFlag) {
129
+ return [...(tableFlags ?? []), ...(tablesFlag ? [tablesFlag] : [])];
130
+ }
131
+ function validatePositiveInteger(value, flagName) {
132
+ if (value === undefined || (Number.isInteger(value) && value > 0)) {
133
+ return;
134
+ }
135
+ throw new CliUserError("invalid_argument", `${flagName} must be a positive integer.`);
136
+ }
@@ -0,0 +1,7 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Guided extends Command {
3
+ static description: string;
4
+ static hidden: boolean;
5
+ run(): Promise<void>;
6
+ }
7
+ //# sourceMappingURL=guided.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guided.d.ts","sourceRoot":"","sources":["../../src/commands/guided.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAGtC,MAAM,CAAC,OAAO,OAAO,MAAO,SAAQ,OAAO;IAC1C,OAAgB,WAAW,SACuB;IAClD,OAAgB,MAAM,UAAQ;IAEjB,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;CAUjC"}
@@ -0,0 +1,15 @@
1
+ import { Command } from "@oclif/core";
2
+ import { renderGuidedFlow } from "../ui/guided-flow.js";
3
+ export default class Guided extends Command {
4
+ static description = "Launch the interactive PrismantiX export flow.";
5
+ static hidden = true;
6
+ async run() {
7
+ const result = await renderGuidedFlow({
8
+ cwd: process.cwd(),
9
+ env: process.env,
10
+ });
11
+ if (!result.ok) {
12
+ this.exit(1);
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ export declare const DEFAULT_API_BASE_URL = "https://api.prismantix.com";
2
+ export declare const DEFAULT_OUTPUT_ROOT = "prismantix-exports";
3
+ export declare const DEFAULT_POLL_INTERVAL_MS = 1000;
4
+ export declare const DEFAULT_TIMEOUT_MS = 120000;
5
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,+BAA+B,CAAC;AACjE,eAAO,MAAM,mBAAmB,uBAAuB,CAAC;AACxD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,eAAO,MAAM,kBAAkB,SAAU,CAAC"}
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_API_BASE_URL = "https://api.prismantix.com";
2
+ export const DEFAULT_OUTPUT_ROOT = "prismantix-exports";
3
+ export const DEFAULT_POLL_INTERVAL_MS = 1000;
4
+ export const DEFAULT_TIMEOUT_MS = 120_000;
@@ -0,0 +1,7 @@
1
+ export declare class CliUserError extends Error {
2
+ readonly code: string;
3
+ readonly exitCode: number;
4
+ constructor(code: string, message: string, exitCode?: number);
5
+ }
6
+ export declare function toCliUserError(error: unknown): CliUserError;
7
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI;CAMvD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,gBAU5C"}
package/dist/errors.js ADDED
@@ -0,0 +1,19 @@
1
+ export class CliUserError extends Error {
2
+ code;
3
+ exitCode;
4
+ constructor(code, message, exitCode = 1) {
5
+ super(message);
6
+ this.name = "CliUserError";
7
+ this.code = code;
8
+ this.exitCode = exitCode;
9
+ }
10
+ }
11
+ export function toCliUserError(error) {
12
+ if (error instanceof CliUserError) {
13
+ return error;
14
+ }
15
+ if (error instanceof Error) {
16
+ return new CliUserError("unexpected_error", error.message);
17
+ }
18
+ return new CliUserError("unexpected_error", String(error));
19
+ }
@@ -0,0 +1,2 @@
1
+ export { run } from "@oclif/core";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from "@oclif/core";
package/dist/io.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function writeResponseBodyToFile(response: Response, path: string): Promise<number>;
2
+ //# sourceMappingURL=io.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"io.d.ts","sourceRoot":"","sources":["../src/io.ts"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAC5C,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,mBAuBZ"}
package/dist/io.js ADDED
@@ -0,0 +1,20 @@
1
+ import { createWriteStream } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { Readable, Transform } from "node:stream";
4
+ import { pipeline } from "node:stream/promises";
5
+ export async function writeResponseBodyToFile(response, path) {
6
+ if (!response.body) {
7
+ const body = Buffer.from(await response.arrayBuffer());
8
+ await writeFile(path, body);
9
+ return body.byteLength;
10
+ }
11
+ let sizeBytes = 0;
12
+ const byteCounter = new Transform({
13
+ transform(chunk, _encoding, callback) {
14
+ sizeBytes += chunk.byteLength;
15
+ callback(null, chunk);
16
+ },
17
+ });
18
+ await pipeline(Readable.fromWeb(response.body), byteCounter, createWriteStream(path));
19
+ return sizeBytes;
20
+ }
@@ -0,0 +1,20 @@
1
+ import { type DownloadResult, type ManifestSummary } from "../workflow.js";
2
+ export interface GuidedFlowResult {
3
+ readonly error?: string;
4
+ readonly ok: boolean;
5
+ readonly result?: DownloadResult;
6
+ }
7
+ export interface GuidedFlowProps {
8
+ readonly cwd?: string;
9
+ readonly env?: Record<string, string | undefined>;
10
+ readonly fetchImpl?: typeof fetch;
11
+ readonly now?: () => number;
12
+ readonly onComplete?: (result: GuidedFlowResult) => void;
13
+ readonly sleep?: (ms: number) => Promise<void>;
14
+ }
15
+ export declare function renderGuidedFlow(props?: GuidedFlowProps): Promise<GuidedFlowResult>;
16
+ export declare function GuidedFlow({ cwd, env, fetchImpl, now, onComplete, sleep, }: GuidedFlowProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function ManifestSummaryView({ summary, }: {
18
+ readonly summary: ManifestSummary;
19
+ }): import("react/jsx-runtime").JSX.Element;
20
+ //# sourceMappingURL=guided-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guided-flow.d.ts","sourceRoot":"","sources":["../../src/ui/guided-flow.tsx"],"names":[],"mappings":"AAgBA,OAAO,EACN,KAAK,cAAc,EACnB,KAAK,eAAe,EAIpB,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC/B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/C;AAID,wBAAsB,gBAAgB,CAAC,KAAK,GAAE,eAAoB,6BAcjE;AAED,wBAAgB,UAAU,CAAC,EAC1B,GAAG,EACH,GAAiB,EACjB,SAAS,EACT,GAAG,EACH,UAAU,EACV,KAAK,GACL,EAAE,eAAe,2CAyLjB;AAED,wBAAgB,mBAAmB,CAAC,EACnC,OAAO,GACP,EAAE;IACF,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;CAClC,2CAiBA"}
@@ -0,0 +1,156 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Alert, PasswordInput, Spinner, StatusMessage, TextInput, UnorderedList, } from "@inkjs/ui";
3
+ import { Box, render, Text, useApp } from "ink";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { DEFAULT_API_BASE_URL, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, } from "../constants.js";
6
+ import { toCliUserError } from "../errors.js";
7
+ import { runDownloadWorkflow, summarizeManifest, } from "../workflow.js";
8
+ export async function renderGuidedFlow(props = {}) {
9
+ let result = { ok: false };
10
+ const instance = render(_jsx(GuidedFlow, { ...props, onComplete: (nextResult) => {
11
+ result = nextResult;
12
+ props.onComplete?.(nextResult);
13
+ } }));
14
+ await instance.waitUntilExit();
15
+ return result;
16
+ }
17
+ export function GuidedFlow({ cwd, env = process.env, fetchImpl, now, onComplete, sleep, }) {
18
+ const { exit } = useApp();
19
+ const envApiKey = env.PRISMANTIX_API_KEY?.trim();
20
+ const envClientId = env.PRISMANTIX_CLIENT_ID?.trim().toUpperCase();
21
+ const [apiKey, setApiKey] = useState(envApiKey ?? "");
22
+ const [clientId, setClientId] = useState(envClientId ?? "");
23
+ const [step, setStep] = useState(envApiKey ? "client" : "api-key");
24
+ const [events, setEvents] = useState([]);
25
+ const [inputError, setInputError] = useState();
26
+ const [manifestSummary, setManifestSummary] = useState();
27
+ const [result, setResult] = useState();
28
+ const [fatalError, setFatalError] = useState();
29
+ const apiBaseUrl = env.PRISMANTIX_API_BASE ?? DEFAULT_API_BASE_URL;
30
+ const outputDir = env.PRISMANTIX_OUTPUT_DIR;
31
+ const pollIntervalMs = readPositiveInteger(env.PRISMANTIX_POLL_INTERVAL_MS, DEFAULT_POLL_INTERVAL_MS);
32
+ const timeoutMs = readPositiveInteger(env.PRISMANTIX_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
33
+ useEffect(() => {
34
+ if (step !== "running") {
35
+ return;
36
+ }
37
+ let cancelled = false;
38
+ runDownloadWorkflow({
39
+ apiBaseUrl,
40
+ apiKey,
41
+ clientId,
42
+ outputDir,
43
+ pollIntervalMs,
44
+ timeoutMs,
45
+ }, {
46
+ cwd,
47
+ fetchImpl,
48
+ now,
49
+ onEvent: (event) => {
50
+ if (cancelled) {
51
+ return;
52
+ }
53
+ if (event.type === "manifest") {
54
+ setManifestSummary(summarizeManifest(event.manifest));
55
+ }
56
+ setEvents((previousEvents) => [
57
+ ...previousEvents.slice(-7),
58
+ formatEvent(event),
59
+ ]);
60
+ },
61
+ sleep,
62
+ })
63
+ .then((downloadResult) => {
64
+ if (cancelled) {
65
+ return;
66
+ }
67
+ setResult(downloadResult);
68
+ setStep("done");
69
+ onComplete?.({ ok: true, result: downloadResult });
70
+ setTimeout(() => exit(), 0);
71
+ })
72
+ .catch((error) => {
73
+ if (cancelled) {
74
+ return;
75
+ }
76
+ const cliError = toCliUserError(error);
77
+ setFatalError(cliError.message);
78
+ setStep("error");
79
+ onComplete?.({ error: cliError.message, ok: false });
80
+ setTimeout(() => exit(), 0);
81
+ });
82
+ return () => {
83
+ cancelled = true;
84
+ };
85
+ }, [
86
+ apiBaseUrl,
87
+ apiKey,
88
+ clientId,
89
+ cwd,
90
+ exit,
91
+ fetchImpl,
92
+ now,
93
+ onComplete,
94
+ outputDir,
95
+ pollIntervalMs,
96
+ sleep,
97
+ step,
98
+ timeoutMs,
99
+ ]);
100
+ const header = useMemo(() => (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { bold: true, children: "PrismantiX Export" }), _jsxs(Text, { color: "gray", children: ["API: ", apiBaseUrl] })] })), [apiBaseUrl]);
101
+ if (step === "api-key") {
102
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [header, _jsx(Text, { children: "Enter PRISMANTIX_API_KEY" }), _jsx(PasswordInput, { placeholder: "API key", onSubmit: (value) => {
103
+ const nextApiKey = value.trim();
104
+ if (!nextApiKey) {
105
+ setInputError("API key is required.");
106
+ return;
107
+ }
108
+ setInputError(undefined);
109
+ setApiKey(nextApiKey);
110
+ setStep("client");
111
+ } }), inputError ? (_jsx(StatusMessage, { variant: "error", children: inputError })) : null] }));
112
+ }
113
+ if (step === "client") {
114
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [header, _jsxs(StatusMessage, { variant: "info", children: ["API key loaded from ", envApiKey ? "environment" : "prompt", "."] }), _jsx(Text, { children: "Enter client ID" }), _jsx(TextInput, { placeholder: clientId ? `Client ID (${clientId})` : "Client ID", onSubmit: (value) => {
115
+ const nextClientId = (value.trim() || clientId).toUpperCase();
116
+ if (!nextClientId) {
117
+ setInputError("Client ID is required.");
118
+ return;
119
+ }
120
+ setInputError(undefined);
121
+ setClientId(nextClientId);
122
+ setStep("running");
123
+ } }), inputError ? (_jsx(StatusMessage, { variant: "error", children: inputError })) : null] }));
124
+ }
125
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [header, step === "running" ? _jsx(Spinner, { label: "Creating export" }) : null, manifestSummary ? (_jsx(ManifestSummaryView, { summary: manifestSummary })) : null, _jsx(EventLog, { events: events }), step === "done" && result ? (_jsxs(StatusMessage, { variant: "success", children: ["Downloaded ", result.files.length, " file(s) to ", result.outputDir] })) : null, step === "error" && fatalError ? (_jsx(Alert, { variant: "error", children: fatalError })) : null] }));
126
+ }
127
+ export function ManifestSummaryView({ summary, }) {
128
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Manifest: ", summary.fileCount, " file(s), ", summary.totalRows, " total row(s)"] }), _jsx(UnorderedList, { children: summary.tables.map((table) => (_jsx(UnorderedList.Item, { children: _jsxs(Text, { children: [table.tableName, ": ", table.rowCount ?? 0, " row(s)"] }) }, table.tableName))) })] }));
129
+ }
130
+ function EventLog({ events }) {
131
+ if (events.length === 0) {
132
+ return null;
133
+ }
134
+ return (_jsx(Box, { flexDirection: "column", children: events.map((event) => (_jsx(Text, { color: "gray", children: event }, event))) }));
135
+ }
136
+ function formatEvent(event) {
137
+ switch (event.type) {
138
+ case "created":
139
+ return `Queued export ${event.exportId} for ${event.clientId}.`;
140
+ case "poll":
141
+ return `Export ${event.status.exportId}: ${event.status.status}.`;
142
+ case "manifest":
143
+ return `Manifest fetched for ${event.manifest.exportId}.`;
144
+ case "download-start":
145
+ return `Downloading ${event.file.tableName}.`;
146
+ case "download-complete":
147
+ return `Downloaded ${event.file.tableName}.`;
148
+ }
149
+ }
150
+ function readPositiveInteger(value, fallback) {
151
+ if (!value) {
152
+ return fallback;
153
+ }
154
+ const parsed = Number(value);
155
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
156
+ }
@@ -0,0 +1,64 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { type ExportManifest, type ExportManifestFile, type ExportStatus } from "./api.js";
3
+ export interface DownloadWorkflowOptions {
4
+ readonly apiBaseUrl?: string;
5
+ readonly apiKey?: string;
6
+ readonly clientId?: string;
7
+ readonly exportId?: string;
8
+ readonly outputDir?: string;
9
+ readonly pollIntervalMs?: number;
10
+ readonly tables?: readonly string[];
11
+ readonly timeoutMs?: number;
12
+ }
13
+ export interface DownloadWorkflowRuntime {
14
+ readonly cwd?: string;
15
+ readonly fetchImpl?: typeof fetch;
16
+ readonly mkdir?: typeof mkdir;
17
+ readonly now?: () => number;
18
+ readonly onEvent?: (event: WorkflowEvent) => void;
19
+ readonly sleep?: (ms: number) => Promise<void>;
20
+ readonly writeFile?: typeof writeFile;
21
+ readonly writeResponseBody?: (response: Response, path: string) => Promise<number>;
22
+ }
23
+ export type WorkflowEvent = {
24
+ readonly clientId: string;
25
+ readonly exportId: string;
26
+ readonly type: "created";
27
+ } | {
28
+ readonly status: ExportStatus;
29
+ readonly type: "poll";
30
+ } | {
31
+ readonly manifest: ExportManifest;
32
+ readonly type: "manifest";
33
+ } | {
34
+ readonly file: ExportManifestFile;
35
+ readonly type: "download-start";
36
+ } | {
37
+ readonly file: DownloadedFile;
38
+ readonly type: "download-complete";
39
+ };
40
+ export interface DownloadedFile {
41
+ readonly path: string;
42
+ readonly rowCount?: number;
43
+ readonly sha256?: string;
44
+ readonly sizeBytes: number;
45
+ readonly tableName: string;
46
+ }
47
+ export interface DownloadResult {
48
+ readonly clientId: string;
49
+ readonly exportId: string;
50
+ readonly files: readonly DownloadedFile[];
51
+ readonly manifestPath: string;
52
+ readonly outputDir: string;
53
+ }
54
+ export interface ManifestSummary {
55
+ readonly fileCount: number;
56
+ readonly tables: readonly {
57
+ readonly rowCount?: number;
58
+ readonly tableName: string;
59
+ }[];
60
+ readonly totalRows: number;
61
+ }
62
+ export declare function runDownloadWorkflow(options: DownloadWorkflowOptions, runtime?: DownloadWorkflowRuntime): Promise<DownloadResult>;
63
+ export declare function summarizeManifest(manifest: ExportManifest): ManifestSummary;
64
+ //# sourceMappingURL=workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { PrismantixApiClient, } from "./api.js";
4
+ import { DEFAULT_API_BASE_URL, DEFAULT_OUTPUT_ROOT, DEFAULT_POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS, } from "./constants.js";
5
+ import { CliUserError } from "./errors.js";
6
+ import { writeResponseBodyToFile } from "./io.js";
7
+ export async function runDownloadWorkflow(options, runtime = {}) {
8
+ const apiKey = options.apiKey?.trim();
9
+ if (!apiKey) {
10
+ throw new CliUserError("missing_api_key", "Set PRISMANTIX_API_KEY before running the CLI.");
11
+ }
12
+ const sleep = runtime.sleep ??
13
+ ((ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)));
14
+ const now = runtime.now ?? (() => Date.now());
15
+ const mkdirImpl = runtime.mkdir ?? mkdir;
16
+ const writeFileImpl = runtime.writeFile ?? writeFile;
17
+ const writeResponseBody = runtime.writeResponseBody ?? writeResponseBodyToFile;
18
+ const emit = runtime.onEvent ?? (() => undefined);
19
+ const api = new PrismantixApiClient({
20
+ apiBaseUrl: options.apiBaseUrl ?? DEFAULT_API_BASE_URL,
21
+ apiKey,
22
+ fetchImpl: runtime.fetchImpl,
23
+ });
24
+ let clientId = normalizeClientId(options.clientId);
25
+ let exportId = options.exportId?.trim();
26
+ if (!exportId) {
27
+ if (!clientId) {
28
+ throw new CliUserError("missing_client", "Pass --client when creating a new export.");
29
+ }
30
+ const created = await api.createExport(clientId);
31
+ clientId = created.clientId;
32
+ exportId = created.exportId;
33
+ emit({ clientId, exportId, type: "created" });
34
+ }
35
+ const status = await waitForCompletedExport({
36
+ api,
37
+ exportId,
38
+ now,
39
+ onPoll: (polledStatus) => emit({ status: polledStatus, type: "poll" }),
40
+ pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
41
+ sleep,
42
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
43
+ });
44
+ clientId = status.clientId;
45
+ const manifest = await api.getManifest(exportId);
46
+ emit({ manifest, type: "manifest" });
47
+ const outputDir = resolveOutputDir({
48
+ clientId,
49
+ cwd: runtime.cwd ?? process.cwd(),
50
+ exportId,
51
+ outputDir: options.outputDir,
52
+ });
53
+ await mkdirImpl(outputDir, { recursive: true });
54
+ const manifestPath = join(outputDir, "manifest.json");
55
+ await writeFileImpl(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, {
56
+ encoding: "utf8",
57
+ });
58
+ const selectedFiles = selectManifestFiles(manifest.files, options.tables ?? []);
59
+ if (selectedFiles.length === 0) {
60
+ throw new CliUserError("no_matching_files", `No export files matched: ${(options.tables ?? []).join(", ")}`);
61
+ }
62
+ const files = [];
63
+ for (const file of selectedFiles) {
64
+ emit({ file, type: "download-start" });
65
+ const destinationPath = join(outputDir, basename(file.fileName));
66
+ const response = await api.getFile(exportId, file.tableName);
67
+ const writtenSizeBytes = await writeResponseBody(response, destinationPath);
68
+ const downloadedFile = {
69
+ path: destinationPath,
70
+ rowCount: file.rowCount,
71
+ sha256: file.sha256,
72
+ sizeBytes: file.sizeBytes ?? writtenSizeBytes,
73
+ tableName: file.tableName,
74
+ };
75
+ files.push(downloadedFile);
76
+ emit({ file: downloadedFile, type: "download-complete" });
77
+ }
78
+ return {
79
+ clientId,
80
+ exportId,
81
+ files,
82
+ manifestPath,
83
+ outputDir,
84
+ };
85
+ }
86
+ export function summarizeManifest(manifest) {
87
+ return {
88
+ fileCount: manifest.files.length,
89
+ tables: manifest.files.map((file) => ({
90
+ rowCount: file.rowCount,
91
+ tableName: file.tableName,
92
+ })),
93
+ totalRows: manifest.totalRows,
94
+ };
95
+ }
96
+ function normalizeClientId(value) {
97
+ const normalized = value?.trim();
98
+ return normalized ? normalized.toUpperCase() : undefined;
99
+ }
100
+ async function waitForCompletedExport(args) {
101
+ const startedAt = args.now();
102
+ for (;;) {
103
+ const status = await args.api.getExportStatus(args.exportId);
104
+ args.onPoll(status);
105
+ if (status.status === "completed") {
106
+ return status;
107
+ }
108
+ if (status.status === "failed") {
109
+ throw new CliUserError("export_failed", status.errorMessage ?? `Export ${args.exportId} failed.`);
110
+ }
111
+ if (args.now() - startedAt >= args.timeoutMs) {
112
+ throw new CliUserError("timeout", `Timed out waiting for export ${args.exportId}.`);
113
+ }
114
+ await args.sleep(args.pollIntervalMs);
115
+ }
116
+ }
117
+ function resolveOutputDir(args) {
118
+ return resolve(args.cwd, args.outputDir ?? join(DEFAULT_OUTPUT_ROOT, args.clientId, args.exportId));
119
+ }
120
+ function selectManifestFiles(files, tables) {
121
+ const normalizedTables = tables
122
+ .flatMap((table) => table.split(","))
123
+ .map((table) => table.trim())
124
+ .filter(Boolean);
125
+ if (normalizedTables.length === 0 || normalizedTables.includes("all")) {
126
+ return [...files];
127
+ }
128
+ const allowedTables = new Set(normalizedTables);
129
+ return files.filter((file) => allowedTables.has(file.tableName));
130
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "commands": {
3
+ "download": {
4
+ "aliases": [],
5
+ "args": {},
6
+ "description": "Create or download a PrismantiX client export without opening the guided UI.",
7
+ "examples": [
8
+ "<%= config.bin %> download --client LLY --yes --json",
9
+ "<%= config.bin %> download --export-id export_123 --json"
10
+ ],
11
+ "flags": {
12
+ "api-base": {
13
+ "description": "PrismantiX API base URL.",
14
+ "env": "PRISMANTIX_API_BASE",
15
+ "name": "api-base",
16
+ "hasDynamicHelp": false,
17
+ "multiple": false,
18
+ "type": "option"
19
+ },
20
+ "client": {
21
+ "description": "Client ID to export when queueing a new export.",
22
+ "name": "client",
23
+ "hasDynamicHelp": false,
24
+ "multiple": false,
25
+ "type": "option"
26
+ },
27
+ "export-id": {
28
+ "description": "Existing export ID to download.",
29
+ "env": "PRISMANTIX_EXPORT_ID",
30
+ "name": "export-id",
31
+ "hasDynamicHelp": false,
32
+ "multiple": false,
33
+ "type": "option"
34
+ },
35
+ "json": {
36
+ "description": "Print machine-readable JSON to stdout.",
37
+ "name": "json",
38
+ "allowNo": false,
39
+ "type": "boolean"
40
+ },
41
+ "output": {
42
+ "description": "Output directory.",
43
+ "env": "PRISMANTIX_OUTPUT_DIR",
44
+ "name": "output",
45
+ "hasDynamicHelp": false,
46
+ "multiple": false,
47
+ "type": "option"
48
+ },
49
+ "poll-interval": {
50
+ "description": "Polling interval in milliseconds.",
51
+ "env": "PRISMANTIX_POLL_INTERVAL_MS",
52
+ "name": "poll-interval",
53
+ "default": 1000,
54
+ "hasDynamicHelp": false,
55
+ "multiple": false,
56
+ "type": "option"
57
+ },
58
+ "table": {
59
+ "description": "Table to download; repeat for multiple tables.",
60
+ "name": "table",
61
+ "hasDynamicHelp": false,
62
+ "multiple": true,
63
+ "type": "option"
64
+ },
65
+ "tables": {
66
+ "description": "Comma-separated list of tables to download.",
67
+ "name": "tables",
68
+ "hasDynamicHelp": false,
69
+ "multiple": false,
70
+ "type": "option"
71
+ },
72
+ "timeout": {
73
+ "description": "Polling timeout in milliseconds.",
74
+ "env": "PRISMANTIX_TIMEOUT_MS",
75
+ "name": "timeout",
76
+ "default": 120000,
77
+ "hasDynamicHelp": false,
78
+ "multiple": false,
79
+ "type": "option"
80
+ },
81
+ "yes": {
82
+ "char": "y",
83
+ "description": "Confirm queueing a new export.",
84
+ "name": "yes",
85
+ "allowNo": false,
86
+ "type": "boolean"
87
+ }
88
+ },
89
+ "hasDynamicHelp": false,
90
+ "hiddenAliases": [],
91
+ "id": "download",
92
+ "pluginAlias": "@nyrra/prismantix-cli",
93
+ "pluginName": "@nyrra/prismantix-cli",
94
+ "pluginType": "core",
95
+ "strict": true,
96
+ "enableJsonFlag": false,
97
+ "isESM": true,
98
+ "relativePath": [
99
+ "dist",
100
+ "commands",
101
+ "download.js"
102
+ ]
103
+ },
104
+ "guided": {
105
+ "aliases": [],
106
+ "args": {},
107
+ "description": "Launch the interactive PrismantiX export flow.",
108
+ "flags": {},
109
+ "hasDynamicHelp": false,
110
+ "hidden": true,
111
+ "hiddenAliases": [],
112
+ "id": "guided",
113
+ "pluginAlias": "@nyrra/prismantix-cli",
114
+ "pluginName": "@nyrra/prismantix-cli",
115
+ "pluginType": "core",
116
+ "strict": true,
117
+ "enableJsonFlag": false,
118
+ "isESM": true,
119
+ "relativePath": [
120
+ "dist",
121
+ "commands",
122
+ "guided.js"
123
+ ]
124
+ }
125
+ },
126
+ "version": "0.0.1"
127
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@nyrra/prismantix-cli",
3
+ "description": "PrismantiX API client CLI.",
4
+ "version": "0.0.1",
5
+ "author": "NYRRA Inc.",
6
+ "bin": {
7
+ "prismantix-cli": "bin/run.js"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/nyrra-labs/prismantix/issues"
11
+ },
12
+ "dependencies": {
13
+ "@inkjs/ui": "^2.0.0",
14
+ "@oclif/core": "^4",
15
+ "@oclif/plugin-help": "^6",
16
+ "ink": "^7.0.2",
17
+ "react": "^19.2.0",
18
+ "react-devtools-core": "^6.1.2",
19
+ "zod": "^4.2.1"
20
+ },
21
+ "devDependencies": {
22
+ "@oclif/test": "^4",
23
+ "@types/node": "^25",
24
+ "ink-testing-library": "^4.0.0",
25
+ "oclif": "^4",
26
+ "shx": "^0.3.3",
27
+ "typescript": "^5"
28
+ },
29
+ "engines": {
30
+ "node": ">=22.0.0"
31
+ },
32
+ "files": [
33
+ "./bin",
34
+ "./dist",
35
+ "./README.md",
36
+ "./oclif.manifest.json"
37
+ ],
38
+ "homepage": "https://github.com/nyrra-labs/prismantix",
39
+ "keywords": [
40
+ "prismantix",
41
+ "export",
42
+ "api",
43
+ "cli"
44
+ ],
45
+ "license": "SEE LICENSE IN LICENSE",
46
+ "main": "dist/index.js",
47
+ "type": "module",
48
+ "oclif": {
49
+ "bin": "prismantix-cli",
50
+ "dirname": "prismantix-cli",
51
+ "commands": "./dist/commands",
52
+ "plugins": [
53
+ "@oclif/plugin-help"
54
+ ],
55
+ "topicSeparator": " "
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/nyrra-labs/prismantix.git",
60
+ "directory": "apps/cli"
61
+ },
62
+ "scripts": {
63
+ "build": "shx rm -rf dist tsconfig.tsbuildinfo && tsc -b tsconfig.json --force && oclif manifest",
64
+ "postpack": "shx rm -f oclif.manifest.json",
65
+ "prepack": "pnpm run build",
66
+ "test": "pnpm run build && vitest run --config vitest.config.mts",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit"
68
+ },
69
+ "types": "dist/index.d.ts",
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "nx": {
74
+ "name": "prismantix-cli"
75
+ }
76
+ }