@pd4castr/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.
Files changed (3) hide show
  1. package/README.md +47 -0
  2. package/dist/index.js +699 -0
  3. package/package.json +76 -0
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # pd4castr CLI
2
+
3
+ CLI tool for creating, testing, and publishing pd4castr models.
4
+
5
+ Install via:
6
+
7
+ ```bash
8
+ npm install -g @pd4castr/cli
9
+ ```
10
+
11
+ ## Contributing
12
+
13
+ ### Quick Start
14
+
15
+ ```bash
16
+ # set this repository up for linking during develpment
17
+ yarn link
18
+
19
+ # run the project in watch mode
20
+ yarn dev
21
+
22
+ # from a model project, link the module
23
+ yarn link @pd4castr/cli
24
+
25
+ # from that project, execute a command
26
+ yarn pd4castr <command>
27
+ ```
28
+
29
+ ### Scripts
30
+
31
+ - `yarn build` - Build the project
32
+ - `yarn dev` - Watch mode for development
33
+ - `yarn cli <command>` - Run CLI commands against local build
34
+ - `yarn test` - Run tests once
35
+ - `yarn test:watch` - Run tests in watch mode
36
+ - `yarn lint` - Check for linting issues
37
+ - `yarn format` - Format code with Prettier
38
+ - `yarn type-check` - TypeScript type checking
39
+
40
+ ### Testing
41
+
42
+ As this project requires a lot of disk I/O and network reqeuests, we opt for 2 mocking solutions that keep us as close to the metal as possible:
43
+
44
+ - network requests are mocked uses [msw](https://mswjs.io/) - request handlers live in the [mocks/handlers](./src/mocks/handlers/) folder
45
+ - disk I/O (`fs`) is mocked using [`memfs`](https://github.com/streamich/memfs) which is handled by Vitest in our [`__mocks__/`](./src/__mocks__) folder
46
+
47
+ Both of these mocks are initialised globally via our [setup script](./vitest.setup.ts).
package/dist/index.js ADDED
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/program.ts
4
+ import { Command } from "commander";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@pd4castr/cli",
9
+ version: "0.0.1",
10
+ description: "CLI tool for creating, testing, and publishing pd4castr models",
11
+ main: "dist/index.js",
12
+ type: "module",
13
+ bin: {
14
+ pd4castr: "dist/index.js"
15
+ },
16
+ files: [
17
+ "dist/**/*"
18
+ ],
19
+ scripts: {
20
+ build: "tsup",
21
+ dev: "tsup --watch",
22
+ cli: "node dist/index.js",
23
+ test: "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ lint: "eslint .",
27
+ "lint:fix": "eslint . --fix",
28
+ format: "prettier --write .",
29
+ "format:check": "prettier --check .",
30
+ "type-check": "tsc --noEmit",
31
+ prepublishOnly: "yarn build"
32
+ },
33
+ keywords: [
34
+ "cli",
35
+ "pd4castr"
36
+ ],
37
+ license: "UNLICENSED",
38
+ repository: {
39
+ type: "git",
40
+ url: "git+https://github.com/pipelabs/pd4castr-cli.git"
41
+ },
42
+ bugs: {
43
+ url: "https://github.com/pipelabs/pd4castr-cli/issues"
44
+ },
45
+ homepage: "https://github.com/pipelabs/pd4castr-cli#readme",
46
+ devDependencies: {
47
+ "@types/express": "4.17.21",
48
+ "@types/node": "24.1.0",
49
+ "@types/supertest": "6.0.3",
50
+ "@typescript-eslint/eslint-plugin": "8.38.0",
51
+ "@typescript-eslint/parser": "8.38.0",
52
+ eslint: "9.32.0",
53
+ "eslint-config-prettier": "10.1.8",
54
+ "eslint-plugin-unicorn": "60.0.0",
55
+ "eslint-plugin-vitest": "0.5.4",
56
+ "jest-extended": "6.0.0",
57
+ memfs: "4.23.0",
58
+ msw: "2.10.4",
59
+ prettier: "3.6.2",
60
+ supertest: "7.1.4",
61
+ tsup: "8.5.0",
62
+ typescript: "5.8.3",
63
+ "typescript-eslint": "8.38.0",
64
+ vitest: "3.2.4"
65
+ },
66
+ dependencies: {
67
+ "@inquirer/prompts": "7.7.1",
68
+ auth0: "4.27.0",
69
+ commander: "14.0.0",
70
+ execa: "9.6.0",
71
+ express: "4.21.2",
72
+ ky: "1.8.2",
73
+ lilconfig: "3.1.3",
74
+ ora: "8.2.0",
75
+ tiged: "2.12.7",
76
+ "tiny-invariant": "1.3.3",
77
+ zod: "4.0.14"
78
+ },
79
+ engines: {
80
+ node: ">=20.0.0"
81
+ }
82
+ };
83
+
84
+ // src/program.ts
85
+ var program = new Command();
86
+ program.name("pd4castr").description("CLI tool for pd4castr").version(package_default.version);
87
+
88
+ // src/commands/init/handle-action.ts
89
+ import path from "path";
90
+ import * as inquirer from "@inquirer/prompts";
91
+ import tiged from "tiged";
92
+ import ora from "ora";
93
+
94
+ // src/commands/init/constants.ts
95
+ var templates = {
96
+ "python-barebones": {
97
+ name: "python-barebones",
98
+ repo: "pipelabs/pd4castr-model-examples",
99
+ path: "examples/python-barebones"
100
+ },
101
+ "python-demo": {
102
+ name: "python-demo",
103
+ repo: "pipelabs/pd4castr-model-examples",
104
+ path: "examples/python-demo"
105
+ }
106
+ };
107
+
108
+ // src/commands/init/utils/get-template-path.ts
109
+ function getTemplatePath(template) {
110
+ return `https://github.com/${template.repo}/${template.path}`;
111
+ }
112
+
113
+ // src/utils/is-existing-path.ts
114
+ import fs from "fs/promises";
115
+ async function isExistingPath(path8) {
116
+ try {
117
+ await fs.access(path8);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ // src/commands/init/utils/validate-name.ts
125
+ async function validateName(value) {
126
+ const exists = await isExistingPath(`./${value}`);
127
+ if (exists) {
128
+ return "A directory or file with this name already exists. Please choose a different name.";
129
+ }
130
+ return true;
131
+ }
132
+
133
+ // src/commands/init/handle-action.ts
134
+ async function handleAction() {
135
+ const projectName = await inquirer.input({
136
+ message: "Name your new model project",
137
+ default: "my-model",
138
+ validate: validateName
139
+ });
140
+ const template = await inquirer.select({
141
+ message: "Select a template",
142
+ choices: Object.values(templates).map((template2) => ({
143
+ name: template2.name,
144
+ value: template2.name
145
+ }))
146
+ });
147
+ const spinner = ora("Fetching template...").start();
148
+ try {
149
+ await fetchTemplate(template, projectName);
150
+ spinner.succeed("Template fetched successfully");
151
+ } catch {
152
+ spinner.fail("Error fetching template");
153
+ process.exit(1);
154
+ }
155
+ }
156
+ async function fetchTemplate(template, projectName) {
157
+ const templatePath = getTemplatePath(templates[template]);
158
+ const fetcher = tiged(templatePath, {
159
+ disableCache: true,
160
+ force: true
161
+ });
162
+ const destination = path.join(process.cwd(), projectName);
163
+ await fetcher.clone(destination);
164
+ }
165
+
166
+ // src/commands/init/index.ts
167
+ function registerInitCommand(program2) {
168
+ program2.command("init").description("Initialize a new model using a template.").action(handleAction);
169
+ }
170
+
171
+ // src/commands/login/handle-action.ts
172
+ import ora2 from "ora";
173
+ import { ZodError } from "zod";
174
+
175
+ // src/config/load-global-config.ts
176
+ import fs2 from "fs/promises";
177
+ import path2 from "path";
178
+ import os from "os";
179
+
180
+ // src/constants.ts
181
+ var AUTH0_DOMAIN = "pdview.au.auth0.com";
182
+ var AUTH0_CLIENT_ID = "Q5tQNF57cQlVXnVsqnU0hhgy92rVb03W";
183
+ var AUTH0_AUDIENCE = "https://api.pd4castr.com.au";
184
+ var GLOBAL_CONFIG_FILE = ".pd4castr";
185
+ var PROJECT_CONFIG_FILE = ".pd4castrrc.json";
186
+ var API_URL = "https://api.pd4castr.com.au";
187
+ var TEST_INPUT_DATA_DIR = "test_data";
188
+
189
+ // src/schemas/global-config-schema.ts
190
+ import { z } from "zod";
191
+ var globalConfigSchema = z.object({
192
+ accessToken: z.string().optional(),
193
+ accessTokenExpiresAt: z.number().optional()
194
+ });
195
+
196
+ // src/config/load-global-config.ts
197
+ async function loadGlobalConfig() {
198
+ const configPath = path2.join(os.homedir(), GLOBAL_CONFIG_FILE);
199
+ const configExists = await isExistingPath(configPath);
200
+ if (!configExists) {
201
+ await fs2.writeFile(configPath, JSON.stringify({}));
202
+ return {};
203
+ }
204
+ try {
205
+ const configFileContents = await fs2.readFile(configPath, "utf8");
206
+ const config = JSON.parse(configFileContents);
207
+ return globalConfigSchema.parse(config);
208
+ } catch {
209
+ return {};
210
+ }
211
+ }
212
+
213
+ // src/config/update-global-config.ts
214
+ import fs3 from "fs/promises";
215
+ import path3 from "path";
216
+ import os2 from "os";
217
+ async function updateGlobalConfig(updatedConfig) {
218
+ const configPath = path3.join(os2.homedir(), GLOBAL_CONFIG_FILE);
219
+ await fs3.writeFile(configPath, JSON.stringify(updatedConfig, void 0, 2));
220
+ }
221
+
222
+ // src/utils/log-zod-issues.ts
223
+ function logZodIssues(error) {
224
+ for (const issue of error.issues) {
225
+ console.log(` \u2718 ${issue.path.join(".")} - ${issue.message}`);
226
+ }
227
+ }
228
+
229
+ // src/commands/login/utils/is-authed.ts
230
+ function isAuthed(config) {
231
+ const isTokenExpired = config.accessTokenExpiresAt && config.accessTokenExpiresAt <= Date.now();
232
+ return Boolean(config.accessToken) && !isTokenExpired;
233
+ }
234
+
235
+ // src/commands/login/auth0-api.ts
236
+ import ky from "ky";
237
+ var auth0API = ky.create({ prefixUrl: `https://${AUTH0_DOMAIN}` });
238
+
239
+ // src/commands/login/utils/start-auth-flow.ts
240
+ var payload = {
241
+ client_id: AUTH0_CLIENT_ID,
242
+ audience: AUTH0_AUDIENCE,
243
+ scope: "openid profile"
244
+ };
245
+ async function startAuthFlow() {
246
+ const codeResponse = await auth0API.post("oauth/device/code", { json: payload }).json();
247
+ const authContext = {
248
+ deviceCode: codeResponse.device_code,
249
+ verificationURL: codeResponse.verification_uri_complete,
250
+ userCode: codeResponse.user_code,
251
+ checkInterval: codeResponse.interval
252
+ };
253
+ return authContext;
254
+ }
255
+
256
+ // src/commands/login/utils/complete-auth-flow.ts
257
+ import { HTTPError } from "ky";
258
+ var FAILED_AUTH_ERRORS = /* @__PURE__ */ new Set(["expired_token", "access_denied"]);
259
+ async function completeAuthFlow(authCtx) {
260
+ const payload2 = {
261
+ client_id: AUTH0_CLIENT_ID,
262
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
263
+ device_code: authCtx.deviceCode
264
+ };
265
+ async function fetchAuthResponse() {
266
+ try {
267
+ const response = await auth0API.post("oauth/token", { json: payload2 }).json();
268
+ const authPayload = {
269
+ accessToken: response.access_token,
270
+ expiresAt: Date.now() + response.expires_in * 1e3
271
+ };
272
+ return authPayload;
273
+ } catch (error) {
274
+ if (!(error instanceof HTTPError)) {
275
+ throw error;
276
+ }
277
+ const errorResponse = await error.response.json();
278
+ const isFailedAuthError = FAILED_AUTH_ERRORS.has(errorResponse.error);
279
+ if (isFailedAuthError) {
280
+ throw new Error(
281
+ `Login failed, please try again (${errorResponse.error_description}).`
282
+ );
283
+ }
284
+ const delay = authCtx.checkInterval * 1e3;
285
+ return new Promise(
286
+ (resolve) => setTimeout(() => resolve(fetchAuthResponse()), delay)
287
+ );
288
+ }
289
+ }
290
+ return fetchAuthResponse();
291
+ }
292
+
293
+ // src/commands/login/handle-action.ts
294
+ async function handleAction2() {
295
+ const spinner = ora2("Logging in to the pd4castr API...").start();
296
+ try {
297
+ const globalConfig = await loadGlobalConfig();
298
+ if (isAuthed(globalConfig)) {
299
+ spinner.succeed("Already logged in!");
300
+ return;
301
+ }
302
+ const authCtx = await startAuthFlow();
303
+ spinner.info(
304
+ `Please open the login link in your browser:
305
+ ${authCtx.verificationURL}`
306
+ );
307
+ spinner.info(`Your login code is:
308
+ ${authCtx.userCode}
309
+ `);
310
+ spinner.start("Waiting for login to complete...");
311
+ const authPayload = await completeAuthFlow(authCtx);
312
+ const updatedGlobalConfig = {
313
+ ...globalConfig,
314
+ accessToken: authPayload.accessToken,
315
+ accessTokenExpiresAt: authPayload.expiresAt
316
+ };
317
+ await updateGlobalConfig(updatedGlobalConfig);
318
+ spinner.succeed("Successfully logged in to the pd4castr API");
319
+ } catch (error) {
320
+ if (error instanceof ZodError) {
321
+ spinner.fail("Config validation failed");
322
+ logZodIssues(error);
323
+ } else if (error instanceof Error) {
324
+ spinner.fail(error.message);
325
+ }
326
+ process.exit(1);
327
+ }
328
+ }
329
+
330
+ // src/commands/login/index.ts
331
+ function registerLoginCommand(program2) {
332
+ program2.command("login").description("Logs in to the pd4castr API.").action(handleAction2);
333
+ }
334
+
335
+ // src/commands/test/handle-action.ts
336
+ import ora3 from "ora";
337
+ import express from "express";
338
+ import path5 from "path";
339
+ import { ExecaError } from "execa";
340
+ import { ZodError as ZodError2 } from "zod";
341
+
342
+ // src/config/load-project-config.ts
343
+ import { lilconfig } from "lilconfig";
344
+
345
+ // src/schemas/project-config-schema.ts
346
+ import { z as z2 } from "zod";
347
+ var aemoDataFetcherSchema = z2.object({
348
+ key: z2.string(),
349
+ type: z2.literal("AEMO_MMS"),
350
+ checkInterval: z2.int().positive(),
351
+ config: z2.object({
352
+ checkQuery: z2.string(),
353
+ fetchQuery: z2.string()
354
+ })
355
+ });
356
+ var dataFetcherSchema = z2.discriminatedUnion("type", [aemoDataFetcherSchema]);
357
+ var modelInputSchema = z2.object({
358
+ key: z2.string(),
359
+ inputSource: z2.string(),
360
+ dataFetcher: z2.string(),
361
+ trigger: z2.string()
362
+ });
363
+ var projectConfigSchema = z2.object({
364
+ dataFetchers: z2.array(dataFetcherSchema).default([]),
365
+ modelInputs: z2.array(modelInputSchema).default([])
366
+ });
367
+
368
+ // src/config/parse-project-config.ts
369
+ function parseProjectConfig(config) {
370
+ return projectConfigSchema.parse(config);
371
+ }
372
+
373
+ // src/config/load-project-config.ts
374
+ async function loadProjectConfig() {
375
+ const result = await lilconfig("pd4castr", {
376
+ searchPlaces: [PROJECT_CONFIG_FILE]
377
+ }).search();
378
+ if (!result?.config) {
379
+ throw new Error(
380
+ "No config found (docs: https://github.com/pipelabs/pd4castr-model-examples/blob/main/docs/005-config.md)."
381
+ );
382
+ }
383
+ return parseProjectConfig(result.config);
384
+ }
385
+
386
+ // src/utils/get-cwd.ts
387
+ function getCWD() {
388
+ if (!process.env.INIT_CWD) {
389
+ throw new Error("INIT_CWD environment variable is not set");
390
+ }
391
+ return process.env.INIT_CWD;
392
+ }
393
+
394
+ // src/commands/test/utils/build-docker-image.ts
395
+ import { execa } from "execa";
396
+ async function buildDockerImage(dockerImage) {
397
+ try {
398
+ await execa("docker", ["build", "-t", dockerImage, "."], {
399
+ cwd: getCWD(),
400
+ stdio: "pipe"
401
+ });
402
+ } catch (error) {
403
+ throw new Error("Failed to build docker image", { cause: error });
404
+ }
405
+ }
406
+
407
+ // src/commands/test/utils/create-input-handler.ts
408
+ import path4 from "path";
409
+ function createInputHandler(inputFilesPath, modelIOChecks) {
410
+ return (req, res) => {
411
+ if (!modelIOChecks.isValidInput(req.params.filename)) {
412
+ return res.status(404).json({ error: "File not found" });
413
+ }
414
+ modelIOChecks.trackInputHandled(req.params.filename);
415
+ const filePath = path4.join(getCWD(), inputFilesPath, req.params.filename);
416
+ return res.sendFile(filePath);
417
+ };
418
+ }
419
+
420
+ // src/commands/test/utils/create-output-handler.ts
421
+ function createOutputHandler(modelIOChecks) {
422
+ return (_, res) => {
423
+ modelIOChecks.trackOutputHandled();
424
+ return res.status(200).json({ message: "Output received" });
425
+ };
426
+ }
427
+
428
+ // src/utils/get-test-data-filename.ts
429
+ function getTestDataFilename(modelInput) {
430
+ return `${modelInput.inputSource}-${modelInput.key}.json`;
431
+ }
432
+
433
+ // src/commands/test/utils/get-test-data-filenames.ts
434
+ function getTestDataFilenames(config) {
435
+ const testDataFilenames = config.modelInputs.map(
436
+ (modelInput) => getTestDataFilename(modelInput)
437
+ );
438
+ return testDataFilenames;
439
+ }
440
+
441
+ // src/commands/test/utils/model-io-checks.ts
442
+ var ModelIOChecks = class {
443
+ inputsToDownload;
444
+ outputUploaded;
445
+ constructor(data) {
446
+ this.inputsToDownload = {};
447
+ this.outputUploaded = false;
448
+ for (const file of data.inputFiles) {
449
+ this.inputsToDownload[file] = false;
450
+ }
451
+ }
452
+ trackInputHandled(filename) {
453
+ if (this.inputsToDownload[filename] !== void 0) {
454
+ this.inputsToDownload[filename] = true;
455
+ }
456
+ }
457
+ trackOutputHandled() {
458
+ this.outputUploaded = true;
459
+ }
460
+ isOutputHandled() {
461
+ return this.outputUploaded;
462
+ }
463
+ isInputHandled(filename) {
464
+ return this.inputsToDownload[filename];
465
+ }
466
+ isInputsHandled() {
467
+ return Object.values(this.inputsToDownload).every(Boolean);
468
+ }
469
+ isValidInput(filename) {
470
+ return this.inputsToDownload[filename] !== void 0;
471
+ }
472
+ };
473
+
474
+ // src/commands/test/utils/run-model-container.ts
475
+ import { execa as execa2 } from "execa";
476
+
477
+ // src/commands/test/utils/get-input-env.ts
478
+ function getInputEnv(modelInput, webserverURL) {
479
+ const variableName = modelInput.key.toUpperCase();
480
+ const filename = getTestDataFilename(modelInput);
481
+ const inputFileURL = `${webserverURL}/input/${filename}`;
482
+ return `INPUT_${variableName}_URL=${inputFileURL}`;
483
+ }
484
+
485
+ // src/commands/test/utils/run-model-container.ts
486
+ async function runModelContainer(dockerImage, config, webserverURL) {
487
+ const inputEnvs = config.modelInputs.map(
488
+ (modelInput) => getInputEnv(modelInput, webserverURL)
489
+ );
490
+ const outputEnv = `OUTPUT_URL=${webserverURL}/output`;
491
+ const envs = [...inputEnvs, outputEnv];
492
+ try {
493
+ const args = [
494
+ "run",
495
+ "--rm",
496
+ "--network=host",
497
+ ...envs.flatMap((env) => ["--env", env]),
498
+ dockerImage
499
+ ];
500
+ await execa2("docker", args, { cwd: getCWD(), stdio: "pipe" });
501
+ } catch (error) {
502
+ throw new Error("Failed to run model container", { cause: error });
503
+ }
504
+ }
505
+
506
+ // src/commands/test/utils/start-web-server.ts
507
+ async function startWebServer(app, port) {
508
+ return new Promise((resolve) => {
509
+ const server = app.listen(port, () => {
510
+ resolve(server);
511
+ });
512
+ });
513
+ }
514
+
515
+ // src/commands/test/handle-action.ts
516
+ async function handleAction3(options) {
517
+ const spinner = ora3("Starting model tests...").start();
518
+ const app = express();
519
+ const server = await startWebServer(app, options.port);
520
+ try {
521
+ const projectConfig = await loadProjectConfig();
522
+ spinner.start("Building docker image");
523
+ await buildDockerImage(options.dockerImage);
524
+ spinner.succeed(`Built docker image (${options.dockerImage})`);
525
+ const inputFiles = getTestDataFilenames(projectConfig);
526
+ const modelIOChecks = new ModelIOChecks({ inputFiles });
527
+ spinner.succeed(`Found ${inputFiles.length} input data files`);
528
+ const handleInput = createInputHandler(options.inputData, modelIOChecks);
529
+ const handleOutput = createOutputHandler(modelIOChecks);
530
+ app.use("/data", express.static(path5.join(getCWD(), options.inputData)));
531
+ app.get("/input/:filename", handleInput);
532
+ app.put("/output", handleOutput);
533
+ spinner.start("Running model container");
534
+ const webserverURL = `http://localhost:${options.port}`;
535
+ await runModelContainer(options.dockerImage, projectConfig, webserverURL);
536
+ spinner.succeed("Model run complete");
537
+ for (const file of inputFiles) {
538
+ const status = modelIOChecks.isInputHandled(file) ? "\u2714" : "\u2718";
539
+ console.log(` ${status} Input Fetched - ${file}`);
540
+ }
541
+ const outputStatus = modelIOChecks.isOutputHandled() ? "\u2714" : "\u2718";
542
+ console.log(` ${outputStatus} Output Uploaded`);
543
+ if (modelIOChecks.isInputsHandled() && modelIOChecks.isOutputHandled()) {
544
+ spinner.succeed("Model I/O test passed");
545
+ } else {
546
+ spinner.fail("Model I/O test failed");
547
+ }
548
+ } catch (error) {
549
+ if (error instanceof ZodError2) {
550
+ spinner.fail("Config validation failed");
551
+ logZodIssues(error);
552
+ } else if (error instanceof Error) {
553
+ spinner.fail(error.message);
554
+ }
555
+ if (error instanceof Error && error.cause instanceof ExecaError) {
556
+ console.error(error.cause.stderr);
557
+ }
558
+ process.exit(1);
559
+ } finally {
560
+ server.close();
561
+ }
562
+ }
563
+
564
+ // src/commands/test/index.ts
565
+ function registerTestCommand(program2) {
566
+ program2.command("test").description(
567
+ "Test a model by verifying input and output is handled correctly."
568
+ ).option(
569
+ "-i, --input-data <path>",
570
+ "The path to the input data directory",
571
+ TEST_INPUT_DATA_DIR
572
+ ).option(
573
+ "-d, --docker-image <image>",
574
+ "The Docker image to execute for testing",
575
+ // TODO: determine a default value here intelligently when we
576
+ // implement our image tagging strategy
577
+ `pd4castr/my-model:${Date.now()}`
578
+ ).option("-p, --port <port>", "The port to run the webserver on", "9800").action(handleAction3);
579
+ }
580
+
581
+ // src/commands/fetch/handle-action.ts
582
+ import ora4 from "ora";
583
+ import { ZodError as ZodError3 } from "zod";
584
+
585
+ // src/utils/get-auth.ts
586
+ import invariant from "tiny-invariant";
587
+ async function getAuth() {
588
+ const config = await loadGlobalConfig();
589
+ if (!isAuthed(config)) {
590
+ throw new Error("Not authenticated. Please run `pd4castr login` to login.");
591
+ }
592
+ invariant(config.accessToken, "Access token is required");
593
+ invariant(config.accessTokenExpiresAt, "Access token expiry is required");
594
+ return {
595
+ accessToken: config.accessToken,
596
+ expiresAt: config.accessTokenExpiresAt
597
+ };
598
+ }
599
+
600
+ // src/commands/fetch/utils/fetch-aemo-data.ts
601
+ import path6 from "path";
602
+ import fs4 from "fs/promises";
603
+
604
+ // src/api.ts
605
+ import ky2 from "ky";
606
+ var api = ky2.create({
607
+ prefixUrl: process.env.PD4CASTR_API_URL ?? API_URL
608
+ });
609
+
610
+ // src/commands/fetch/utils/fetch-aemo-data.ts
611
+ async function fetchAEMOData(dataFetcher, authCtx) {
612
+ const queryPath = path6.resolve(getCWD(), dataFetcher.config.fetchQuery);
613
+ const querySQL = await fs4.readFile(queryPath, "utf8");
614
+ const headers = { Authorization: `Bearer ${authCtx.accessToken}` };
615
+ const payload2 = { query: querySQL, type: "AEMO_MMS" };
616
+ const result = await api.post("data-fetcher/run-query", { json: payload2, headers }).json();
617
+ return result;
618
+ }
619
+
620
+ // src/commands/fetch/utils/get-fetcher.ts
621
+ var DATA_FETCHER_FNS = {
622
+ AEMO_MMS: fetchAEMOData
623
+ };
624
+ function getFetcher(type) {
625
+ const fetcher = DATA_FETCHER_FNS[type];
626
+ if (!fetcher) {
627
+ throw new Error(`Unsupported data fetcher type: ${type}`);
628
+ }
629
+ return fetcher;
630
+ }
631
+
632
+ // src/commands/fetch/utils/write-test-data.ts
633
+ import path7 from "path";
634
+ import fs5 from "fs/promises";
635
+ async function writeTestData(output, modelInput) {
636
+ const outputDir = path7.resolve(getCWD(), TEST_INPUT_DATA_DIR);
637
+ await fs5.mkdir(outputDir, { recursive: true });
638
+ const testDataFilename = getTestDataFilename(modelInput);
639
+ const outputPath = path7.resolve(outputDir, testDataFilename);
640
+ await fs5.writeFile(outputPath, JSON.stringify(output, void 0, 2));
641
+ }
642
+
643
+ // src/commands/fetch/handle-action.ts
644
+ var FETCHABLE_DATA_FETCHER_TYPES = /* @__PURE__ */ new Set(["AEMO_MMS"]);
645
+ async function handleAction4() {
646
+ const spinner = ora4("Starting data fetch...").start();
647
+ try {
648
+ const authCtx = await getAuth();
649
+ const projectConfig = await loadProjectConfig();
650
+ for (const modelInput of projectConfig.modelInputs) {
651
+ const dataFetcher = projectConfig.dataFetchers.find(
652
+ (dataFetcher2) => dataFetcher2.key === modelInput.dataFetcher
653
+ );
654
+ if (!dataFetcher) {
655
+ spinner.warn(
656
+ `\`${modelInput.key}\` - input has no data fetcher, skipping`
657
+ );
658
+ continue;
659
+ }
660
+ if (!FETCHABLE_DATA_FETCHER_TYPES.has(dataFetcher.type)) {
661
+ spinner.warn(
662
+ `\`${dataFetcher.key}\` (${dataFetcher.type}) - unsupported, skipping`
663
+ );
664
+ continue;
665
+ }
666
+ spinner.start(
667
+ `\`${dataFetcher.key}\` (${dataFetcher.type}) - fetching...`
668
+ );
669
+ const fetchData = getFetcher(dataFetcher.type);
670
+ const output = await fetchData(dataFetcher, authCtx);
671
+ await writeTestData(output, modelInput);
672
+ spinner.succeed(`\`${dataFetcher.key}\` (${dataFetcher.type}) - fetched`);
673
+ }
674
+ } catch (error) {
675
+ if (error instanceof ZodError3) {
676
+ spinner.fail("Config validation failed");
677
+ logZodIssues(error);
678
+ } else if (error instanceof Error) {
679
+ spinner.fail(error.message);
680
+ }
681
+ process.exit(1);
682
+ }
683
+ }
684
+
685
+ // src/commands/fetch/index.ts
686
+ function registerFetchCommand(program2) {
687
+ program2.command("fetch").description("Fetches test data from configured data fetchers.").option(
688
+ "-i, --input-data <path>",
689
+ "The path to the input data directory",
690
+ TEST_INPUT_DATA_DIR
691
+ ).action(handleAction4);
692
+ }
693
+
694
+ // src/index.ts
695
+ registerInitCommand(program);
696
+ registerLoginCommand(program);
697
+ registerTestCommand(program);
698
+ registerFetchCommand(program);
699
+ await program.parseAsync(process.argv);
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@pd4castr/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI tool for creating, testing, and publishing pd4castr models",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "pd4castr": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
+ "cli": "node dist/index.js",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "test:coverage": "vitest run --coverage",
20
+ "lint": "eslint .",
21
+ "lint:fix": "eslint . --fix",
22
+ "format": "prettier --write .",
23
+ "format:check": "prettier --check .",
24
+ "type-check": "tsc --noEmit",
25
+ "prepublishOnly": "yarn build"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "pd4castr"
30
+ ],
31
+ "license": "UNLICENSED",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/pipelabs/pd4castr-cli.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/pipelabs/pd4castr-cli/issues"
38
+ },
39
+ "homepage": "https://github.com/pipelabs/pd4castr-cli#readme",
40
+ "devDependencies": {
41
+ "@types/express": "4.17.21",
42
+ "@types/node": "24.1.0",
43
+ "@types/supertest": "6.0.3",
44
+ "@typescript-eslint/eslint-plugin": "8.38.0",
45
+ "@typescript-eslint/parser": "8.38.0",
46
+ "eslint": "9.32.0",
47
+ "eslint-config-prettier": "10.1.8",
48
+ "eslint-plugin-unicorn": "60.0.0",
49
+ "eslint-plugin-vitest": "0.5.4",
50
+ "jest-extended": "6.0.0",
51
+ "memfs": "4.23.0",
52
+ "msw": "2.10.4",
53
+ "prettier": "3.6.2",
54
+ "supertest": "7.1.4",
55
+ "tsup": "8.5.0",
56
+ "typescript": "5.8.3",
57
+ "typescript-eslint": "8.38.0",
58
+ "vitest": "3.2.4"
59
+ },
60
+ "dependencies": {
61
+ "@inquirer/prompts": "7.7.1",
62
+ "auth0": "4.27.0",
63
+ "commander": "14.0.0",
64
+ "execa": "9.6.0",
65
+ "express": "4.21.2",
66
+ "ky": "1.8.2",
67
+ "lilconfig": "3.1.3",
68
+ "ora": "8.2.0",
69
+ "tiged": "2.12.7",
70
+ "tiny-invariant": "1.3.3",
71
+ "zod": "4.0.14"
72
+ },
73
+ "engines": {
74
+ "node": ">=20.0.0"
75
+ }
76
+ }