@pd4castr/cli 0.0.3 → 0.0.5

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 (2) hide show
  1. package/dist/index.js +429 -423
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -45,97 +45,271 @@ var __callDispose = (stack, error, hasError) => {
45
45
  return next();
46
46
  };
47
47
 
48
- // src/program.ts
49
- import { Command } from "commander";
48
+ // src/constants.ts
49
+ var AUTH0_DOMAIN = "pdview.au.auth0.com";
50
+ var AUTH0_CLIENT_ID = "Q5tQNF57cQlVXnVsqnU0hhgy92rVb03W";
51
+ var AUTH0_AUDIENCE = "https://api.pd4castr.com.au";
52
+ var GLOBAL_CONFIG_FILE = ".pd4castr";
53
+ var PROJECT_CONFIG_FILE = ".pd4castrrc.json";
54
+ var API_URL = "https://pd4castr-api.pipelabs.app";
55
+ var TEST_INPUT_DATA_DIR = "test_input";
56
+ var TEST_OUTPUT_DATA_DIR = "test_output";
57
+ var TEST_OUTPUT_FILENAME = `output-${Date.now()}.json`;
50
58
 
51
- // package.json
52
- var package_default = {
53
- name: "@pd4castr/cli",
54
- version: "0.0.3",
55
- description: "CLI tool for creating, testing, and publishing pd4castr models",
56
- main: "dist/index.js",
57
- type: "module",
58
- bin: {
59
- pd4castr: "dist/index.js"
60
- },
61
- files: [
62
- "dist/**/*"
63
- ],
64
- scripts: {
65
- build: "tsup",
66
- dev: "tsup --watch",
67
- cli: "node dist/index.js",
68
- test: "vitest run",
69
- "test:watch": "vitest",
70
- "test:coverage": "vitest run --coverage",
71
- lint: "eslint .",
72
- "lint:fix": "eslint . --fix",
73
- format: "prettier --write .",
74
- "format:check": "prettier --check .",
75
- "type-check": "tsc --noEmit",
76
- prepublishOnly: "yarn build"
77
- },
78
- keywords: [
79
- "cli",
80
- "pd4castr"
81
- ],
82
- license: "UNLICENSED",
83
- repository: {
84
- type: "git",
85
- url: "git+https://github.com/pipelabs/pd4castr-cli.git"
86
- },
87
- bugs: {
88
- url: "https://github.com/pipelabs/pd4castr-cli/issues"
89
- },
90
- homepage: "https://github.com/pipelabs/pd4castr-cli#readme",
91
- devDependencies: {
92
- "@types/express": "4.17.21",
93
- "@types/node": "24.1.0",
94
- "@types/supertest": "6.0.3",
95
- "@typescript-eslint/eslint-plugin": "8.38.0",
96
- "@typescript-eslint/parser": "8.38.0",
97
- eslint: "9.32.0",
98
- "eslint-config-prettier": "10.1.8",
99
- "eslint-plugin-unicorn": "60.0.0",
100
- "eslint-plugin-vitest": "0.5.4",
101
- "jest-extended": "6.0.0",
102
- memfs: "4.23.0",
103
- msw: "2.10.4",
104
- prettier: "3.6.2",
105
- supertest: "7.1.4",
106
- tsup: "8.5.0",
107
- typescript: "5.8.3",
108
- "typescript-eslint": "8.38.0",
109
- vitest: "3.2.4"
110
- },
111
- dependencies: {
112
- "@inquirer/prompts": "7.7.1",
113
- auth0: "4.27.0",
114
- commander: "14.0.0",
115
- execa: "9.6.0",
116
- express: "4.21.2",
117
- ky: "1.8.2",
118
- lilconfig: "3.1.3",
119
- ora: "8.2.0",
120
- slugify: "1.6.6",
121
- tiged: "2.12.7",
122
- "tiny-invariant": "1.3.3",
123
- zod: "4.0.14"
124
- },
125
- engines: {
126
- node: ">=20.0.0"
59
+ // src/commands/fetch/handle-action.ts
60
+ import path5 from "path";
61
+ import ora from "ora";
62
+ import { ZodError } from "zod";
63
+
64
+ // src/config/load-project-config.ts
65
+ import path from "path";
66
+ import { lilconfig } from "lilconfig";
67
+
68
+ // src/schemas/project-config-schema.ts
69
+ import { z } from "zod";
70
+ var aemoDataFetcherSchema = z.object({
71
+ type: z.literal("AEMO_MMS"),
72
+ checkInterval: z.number().int().min(60),
73
+ config: z.object({
74
+ checkQuery: z.string(),
75
+ fetchQuery: z.string()
76
+ })
77
+ });
78
+ var dataFetcherSchema = z.discriminatedUnion("type", [aemoDataFetcherSchema]);
79
+ var modelInputSchema = z.object({
80
+ key: z.string(),
81
+ inputSource: z.string().optional(),
82
+ trigger: z.enum(["WAIT_FOR_LATEST_FILE", "USE_MOST_RECENT_FILE"]),
83
+ fetcher: dataFetcherSchema.optional()
84
+ });
85
+ var modelOutputSchema = z.object({
86
+ name: z.string(),
87
+ seriesKey: z.boolean(),
88
+ colour: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional()
89
+ });
90
+ var projectConfigSchema = z.object({
91
+ $$id: z.string().nullable(),
92
+ $$modelGroupID: z.string().nullable(),
93
+ $$revision: z.number().int().min(0).default(0),
94
+ $$dockerImage: z.string().nullable(),
95
+ name: z.string(),
96
+ metadata: z.record(z.string(), z.any()).optional(),
97
+ forecastVariable: z.enum(["PRICE"]),
98
+ timeHorizon: z.enum(["ACTUAL", "DAY_AHEAD", "WEEK_AHEAD"]),
99
+ inputs: z.array(modelInputSchema),
100
+ outputs: z.array(modelOutputSchema)
101
+ });
102
+
103
+ // src/config/parse-project-config.ts
104
+ function parseProjectConfig(config) {
105
+ return projectConfigSchema.parse(config);
106
+ }
107
+
108
+ // src/config/load-project-config.ts
109
+ async function loadProjectConfig() {
110
+ const result = await lilconfig("pd4castr", {
111
+ searchPlaces: [PROJECT_CONFIG_FILE]
112
+ }).search();
113
+ if (!result?.config) {
114
+ throw new Error(
115
+ "No config found (docs: https://github.com/pipelabs/pd4castr-model-examples/blob/main/docs/005-config.md)."
116
+ );
117
+ }
118
+ const config = parseProjectConfig(result.config);
119
+ const projectRoot = path.dirname(result.filepath);
120
+ return {
121
+ config,
122
+ projectRoot
123
+ };
124
+ }
125
+
126
+ // src/utils/create-link.ts
127
+ var ESC = "\x1B";
128
+ var OSC = `${ESC}]`;
129
+ var SEP = ";";
130
+ function createLink(text, url) {
131
+ const start = `${OSC}8${SEP}${SEP}${url}${ESC}\\`;
132
+ const end = `${OSC}8${SEP}${SEP}${ESC}\\`;
133
+ return `${start}${text}${end}`;
134
+ }
135
+
136
+ // src/utils/get-auth.ts
137
+ import invariant from "tiny-invariant";
138
+
139
+ // src/commands/login/utils/is-authed.ts
140
+ function isAuthed(config) {
141
+ const isTokenExpired = config.accessTokenExpiresAt && config.accessTokenExpiresAt <= Date.now();
142
+ return Boolean(config.accessToken) && !isTokenExpired;
143
+ }
144
+
145
+ // src/config/load-global-config.ts
146
+ import fs2 from "fs/promises";
147
+ import os from "os";
148
+ import path2 from "path";
149
+
150
+ // src/schemas/global-config-schema.ts
151
+ import { z as z2 } from "zod";
152
+ var globalConfigSchema = z2.object({
153
+ accessToken: z2.string().optional(),
154
+ accessTokenExpiresAt: z2.number().optional()
155
+ });
156
+
157
+ // src/utils/is-existing-path.ts
158
+ import fs from "fs/promises";
159
+ async function isExistingPath(path12) {
160
+ try {
161
+ await fs.access(path12);
162
+ return true;
163
+ } catch {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ // src/config/load-global-config.ts
169
+ async function loadGlobalConfig() {
170
+ const configPath = path2.join(os.homedir(), GLOBAL_CONFIG_FILE);
171
+ const configExists = await isExistingPath(configPath);
172
+ if (!configExists) {
173
+ await fs2.writeFile(configPath, JSON.stringify({}));
174
+ return {};
175
+ }
176
+ try {
177
+ const configFileContents = await fs2.readFile(configPath, "utf8");
178
+ const config = JSON.parse(configFileContents);
179
+ return globalConfigSchema.parse(config);
180
+ } catch {
181
+ return {};
127
182
  }
183
+ }
184
+
185
+ // src/utils/get-auth.ts
186
+ async function getAuth() {
187
+ const config = await loadGlobalConfig();
188
+ if (!isAuthed(config)) {
189
+ throw new Error("Not authenticated. Please run `pd4castr login` to login.");
190
+ }
191
+ invariant(config.accessToken, "Access token is required");
192
+ invariant(config.accessTokenExpiresAt, "Access token expiry is required");
193
+ return {
194
+ accessToken: config.accessToken,
195
+ expiresAt: config.accessTokenExpiresAt
196
+ };
197
+ }
198
+
199
+ // src/utils/log-zod-issues.ts
200
+ function logZodIssues(error) {
201
+ for (const issue of error.issues) {
202
+ console.log(` \u2718 ${issue.path.join(".")} - ${issue.message}`);
203
+ }
204
+ }
205
+
206
+ // src/commands/fetch/utils/fetch-aemo-data.ts
207
+ import fs3 from "fs/promises";
208
+ import path3 from "path";
209
+
210
+ // src/api.ts
211
+ import ky from "ky";
212
+ var api = ky.create({
213
+ prefixUrl: process.env.PD4CASTR_API_URL ?? API_URL
214
+ });
215
+
216
+ // src/commands/fetch/utils/fetch-aemo-data.ts
217
+ async function fetchAEMOData(dataFetcher, authCtx, ctx) {
218
+ const queryPath = path3.resolve(
219
+ ctx.projectRoot,
220
+ dataFetcher.config.fetchQuery
221
+ );
222
+ const querySQL = await fs3.readFile(queryPath, "utf8");
223
+ const headers = { Authorization: `Bearer ${authCtx.accessToken}` };
224
+ const payload2 = { query: querySQL, type: "AEMO_MMS" };
225
+ const result = await api.post("data-fetcher/query", { json: payload2, headers }).json();
226
+ return result;
227
+ }
228
+
229
+ // src/commands/fetch/utils/get-fetcher.ts
230
+ var DATA_FETCHER_FNS = {
231
+ AEMO_MMS: fetchAEMOData
128
232
  };
233
+ function getFetcher(type) {
234
+ const fetcher = DATA_FETCHER_FNS[type];
235
+ if (!fetcher) {
236
+ throw new Error(`Unsupported data fetcher type: ${type}`);
237
+ }
238
+ return fetcher;
239
+ }
129
240
 
130
- // src/program.ts
131
- var program = new Command();
132
- program.name("pd4castr").description("CLI tool for pd4castr").version(package_default.version);
241
+ // src/commands/fetch/utils/write-test-data.ts
242
+ import fs4 from "fs/promises";
243
+ import path4 from "path";
244
+
245
+ // src/utils/get-input-filename.ts
246
+ function getInputFilename(modelInput) {
247
+ return `${modelInput.key}.json`;
248
+ }
249
+
250
+ // src/commands/fetch/utils/write-test-data.ts
251
+ async function writeTestData(inputData, modelInput, inputDataDir, ctx) {
252
+ const inputDir = path4.resolve(ctx.projectRoot, inputDataDir);
253
+ await fs4.mkdir(inputDir, { recursive: true });
254
+ const inputFilename = getInputFilename(modelInput);
255
+ const inputPath = path4.resolve(inputDir, inputFilename);
256
+ await fs4.writeFile(inputPath, JSON.stringify(inputData, void 0, 2));
257
+ }
258
+
259
+ // src/commands/fetch/handle-action.ts
260
+ var FETCHABLE_DATA_FETCHER_TYPES = /* @__PURE__ */ new Set(["AEMO_MMS"]);
261
+ async function handleAction(options) {
262
+ const spinner = ora("Starting data fetch...").start();
263
+ try {
264
+ const authCtx = await getAuth();
265
+ const ctx = await loadProjectConfig();
266
+ for (const input2 of ctx.config.inputs) {
267
+ if (!input2.fetcher) {
268
+ spinner.warn(`\`${input2.key}\` - no data fetcher defined, skipping`);
269
+ continue;
270
+ }
271
+ if (!FETCHABLE_DATA_FETCHER_TYPES.has(input2.fetcher.type)) {
272
+ spinner.warn(
273
+ `\`${input2.key}\` (${input2.fetcher.type}) - unsupported, skipping`
274
+ );
275
+ continue;
276
+ }
277
+ spinner.start(`\`${input2.key}\` (${input2.fetcher.type}) - fetching...`);
278
+ const fetchData = getFetcher(input2.fetcher.type);
279
+ const output = await fetchData(input2.fetcher, authCtx, ctx);
280
+ await writeTestData(output, input2, options.inputDir, ctx);
281
+ spinner.succeed(`\`${input2.key}\` (${input2.fetcher.type}) - fetched`);
282
+ }
283
+ const inputPath = path5.resolve(ctx.projectRoot, options.inputDir);
284
+ const link = createLink("Click here", `file://${inputPath}`);
285
+ console.log(`
286
+ ${link} to view fetched data
287
+ `);
288
+ } catch (error) {
289
+ if (error instanceof ZodError) {
290
+ spinner.fail("Config validation failed");
291
+ logZodIssues(error);
292
+ } else if (error instanceof Error) {
293
+ spinner.fail(error.message);
294
+ }
295
+ process.exit(1);
296
+ }
297
+ }
298
+
299
+ // src/commands/fetch/index.ts
300
+ function registerFetchCommand(program2) {
301
+ program2.command("fetch").description("Fetches test data from configured data fetchers.").option(
302
+ "-i, --input-dir <path>",
303
+ "The input test data directory",
304
+ TEST_INPUT_DATA_DIR
305
+ ).action(handleAction);
306
+ }
133
307
 
134
308
  // src/commands/init/handle-action.ts
135
- import path from "path";
309
+ import path6 from "path";
136
310
  import * as inquirer from "@inquirer/prompts";
311
+ import ora2 from "ora";
137
312
  import tiged from "tiged";
138
- import ora from "ora";
139
313
 
140
314
  // src/commands/init/constants.ts
141
315
  var templates = {
@@ -156,17 +330,6 @@ function getTemplatePath(template) {
156
330
  return `https://github.com/${template.repo}/${template.path}`;
157
331
  }
158
332
 
159
- // src/utils/is-existing-path.ts
160
- import fs from "fs/promises";
161
- async function isExistingPath(path12) {
162
- try {
163
- await fs.access(path12);
164
- return true;
165
- } catch {
166
- return false;
167
- }
168
- }
169
-
170
333
  // src/commands/init/utils/validate-name.ts
171
334
  async function validateName(value) {
172
335
  const exists = await isExistingPath(`./${value}`);
@@ -177,7 +340,7 @@ async function validateName(value) {
177
340
  }
178
341
 
179
342
  // src/commands/init/handle-action.ts
180
- async function handleAction() {
343
+ async function handleAction2() {
181
344
  const projectName = await inquirer.input({
182
345
  message: "Name your new model project",
183
346
  default: "my-model",
@@ -190,7 +353,7 @@ async function handleAction() {
190
353
  value: template2.name
191
354
  }))
192
355
  });
193
- const spinner = ora("Fetching template...").start();
356
+ const spinner = ora2("Fetching template...").start();
194
357
  try {
195
358
  await fetchTemplate(template, projectName);
196
359
  spinner.succeed("Template fetched successfully");
@@ -208,104 +371,36 @@ async function fetchTemplate(template, projectName) {
208
371
  disableCache: true,
209
372
  force: true
210
373
  });
211
- const destination = path.join(process.cwd(), projectName);
374
+ const destination = path6.join(process.cwd(), projectName);
212
375
  await fetcher.clone(destination);
213
376
  }
214
377
 
215
378
  // src/commands/init/index.ts
216
379
  function registerInitCommand(program2) {
217
- program2.command("init").description("Initialize a new model using a template.").action(handleAction);
380
+ program2.command("init").description("Initialize a new model using a template.").action(handleAction2);
218
381
  }
219
382
 
220
383
  // src/commands/login/handle-action.ts
221
- import ora2 from "ora";
222
- import { ZodError } from "zod";
223
-
224
- // src/config/load-global-config.ts
225
- import fs2 from "fs/promises";
226
- import path2 from "path";
227
- import os from "os";
228
-
229
- // src/constants.ts
230
- var AUTH0_DOMAIN = "pdview.au.auth0.com";
231
- var AUTH0_CLIENT_ID = "Q5tQNF57cQlVXnVsqnU0hhgy92rVb03W";
232
- var AUTH0_AUDIENCE = "https://api.pd4castr.com.au";
233
- var GLOBAL_CONFIG_FILE = ".pd4castr";
234
- var PROJECT_CONFIG_FILE = ".pd4castrrc.json";
235
- var API_URL = "https://pd4castr-api.pipelabs.app";
236
- var TEST_INPUT_DATA_DIR = "test_input";
237
- var TEST_OUTPUT_DATA_DIR = "test_output";
238
- var TEST_OUTPUT_FILENAME = `output-${Date.now()}.json`;
239
-
240
- // src/schemas/global-config-schema.ts
241
- import { z } from "zod";
242
- var globalConfigSchema = z.object({
243
- accessToken: z.string().optional(),
244
- accessTokenExpiresAt: z.number().optional()
245
- });
246
-
247
- // src/config/load-global-config.ts
248
- async function loadGlobalConfig() {
249
- const configPath = path2.join(os.homedir(), GLOBAL_CONFIG_FILE);
250
- const configExists = await isExistingPath(configPath);
251
- if (!configExists) {
252
- await fs2.writeFile(configPath, JSON.stringify({}));
253
- return {};
254
- }
255
- try {
256
- const configFileContents = await fs2.readFile(configPath, "utf8");
257
- const config = JSON.parse(configFileContents);
258
- return globalConfigSchema.parse(config);
259
- } catch {
260
- return {};
261
- }
262
- }
384
+ import ora3 from "ora";
385
+ import { ZodError as ZodError2 } from "zod";
263
386
 
264
387
  // src/config/update-global-config.ts
265
- import fs3 from "fs/promises";
266
- import path3 from "path";
388
+ import fs5 from "fs/promises";
267
389
  import os2 from "os";
390
+ import path7 from "path";
268
391
  async function updateGlobalConfig(updatedConfig) {
269
- const configPath = path3.join(os2.homedir(), GLOBAL_CONFIG_FILE);
270
- await fs3.writeFile(configPath, JSON.stringify(updatedConfig, void 0, 2));
392
+ const configPath = path7.join(os2.homedir(), GLOBAL_CONFIG_FILE);
393
+ await fs5.writeFile(configPath, JSON.stringify(updatedConfig, void 0, 2));
271
394
  }
272
395
 
273
- // src/utils/log-zod-issues.ts
274
- function logZodIssues(error) {
275
- for (const issue of error.issues) {
276
- console.log(` \u2718 ${issue.path.join(".")} - ${issue.message}`);
277
- }
278
- }
279
-
280
- // src/commands/login/utils/is-authed.ts
281
- function isAuthed(config) {
282
- const isTokenExpired = config.accessTokenExpiresAt && config.accessTokenExpiresAt <= Date.now();
283
- return Boolean(config.accessToken) && !isTokenExpired;
284
- }
396
+ // src/commands/login/utils/complete-auth-flow.ts
397
+ import { HTTPError } from "ky";
285
398
 
286
399
  // src/commands/login/auth0-api.ts
287
- import ky from "ky";
288
- var auth0API = ky.create({ prefixUrl: `https://${AUTH0_DOMAIN}` });
289
-
290
- // src/commands/login/utils/start-auth-flow.ts
291
- var payload = {
292
- client_id: AUTH0_CLIENT_ID,
293
- audience: AUTH0_AUDIENCE,
294
- scope: "openid profile"
295
- };
296
- async function startAuthFlow() {
297
- const codeResponse = await auth0API.post("oauth/device/code", { json: payload }).json();
298
- const authContext = {
299
- deviceCode: codeResponse.device_code,
300
- verificationURL: codeResponse.verification_uri_complete,
301
- userCode: codeResponse.user_code,
302
- checkInterval: codeResponse.interval
303
- };
304
- return authContext;
305
- }
400
+ import ky2 from "ky";
401
+ var auth0API = ky2.create({ prefixUrl: `https://${AUTH0_DOMAIN}` });
306
402
 
307
403
  // src/commands/login/utils/complete-auth-flow.ts
308
- import { HTTPError } from "ky";
309
404
  var FAILED_AUTH_ERRORS = /* @__PURE__ */ new Set(["expired_token", "access_denied"]);
310
405
  async function completeAuthFlow(authCtx) {
311
406
  const payload2 = {
@@ -341,9 +436,26 @@ async function completeAuthFlow(authCtx) {
341
436
  return fetchAuthResponse();
342
437
  }
343
438
 
439
+ // src/commands/login/utils/start-auth-flow.ts
440
+ var payload = {
441
+ client_id: AUTH0_CLIENT_ID,
442
+ audience: AUTH0_AUDIENCE,
443
+ scope: "openid email"
444
+ };
445
+ async function startAuthFlow() {
446
+ const codeResponse = await auth0API.post("oauth/device/code", { json: payload }).json();
447
+ const authContext = {
448
+ deviceCode: codeResponse.device_code,
449
+ verificationURL: codeResponse.verification_uri_complete,
450
+ userCode: codeResponse.user_code,
451
+ checkInterval: codeResponse.interval
452
+ };
453
+ return authContext;
454
+ }
455
+
344
456
  // src/commands/login/handle-action.ts
345
- async function handleAction2() {
346
- const spinner = ora2("Logging in to the pd4castr API...").start();
457
+ async function handleAction3() {
458
+ const spinner = ora3("Logging in to the pd4castr API...").start();
347
459
  try {
348
460
  const globalConfig = await loadGlobalConfig();
349
461
  if (isAuthed(globalConfig)) {
@@ -358,111 +470,39 @@ async function handleAction2() {
358
470
  spinner.info(`Your login code is:
359
471
  ${authCtx.userCode}
360
472
  `);
361
- spinner.start("Waiting for login to complete...");
362
- const authPayload = await completeAuthFlow(authCtx);
363
- const updatedGlobalConfig = {
364
- ...globalConfig,
365
- accessToken: authPayload.accessToken,
366
- accessTokenExpiresAt: authPayload.expiresAt
367
- };
368
- await updateGlobalConfig(updatedGlobalConfig);
369
- spinner.succeed("Successfully logged in to the pd4castr API");
370
- } catch (error) {
371
- if (error instanceof ZodError) {
372
- spinner.fail("Config validation failed");
373
- logZodIssues(error);
374
- } else if (error instanceof Error) {
375
- spinner.fail(error.message);
376
- }
377
- process.exit(1);
378
- }
379
- }
380
-
381
- // src/commands/login/index.ts
382
- function registerLoginCommand(program2) {
383
- program2.command("login").description("Logs in to the pd4castr API.").action(handleAction2);
384
- }
385
-
386
- // src/commands/test/handle-action.ts
387
- import ora3 from "ora";
388
- import express from "express";
389
- import path8 from "path";
390
- import slugify from "slugify";
391
- import { ExecaError } from "execa";
392
- import { ZodError as ZodError2 } from "zod";
393
-
394
- // src/config/load-project-config.ts
395
- import path4 from "path";
396
- import { lilconfig } from "lilconfig";
397
-
398
- // src/schemas/project-config-schema.ts
399
- import { z as z2 } from "zod";
400
- var aemoDataFetcherSchema = z2.object({
401
- type: z2.literal("AEMO_MMS"),
402
- checkInterval: z2.number().int().min(60),
403
- config: z2.object({
404
- checkQuery: z2.string(),
405
- fetchQuery: z2.string()
406
- })
407
- });
408
- var dataFetcherSchema = z2.discriminatedUnion("type", [aemoDataFetcherSchema]);
409
- var modelInputSchema = z2.object({
410
- key: z2.string(),
411
- inputSource: z2.string().optional(),
412
- trigger: z2.enum(["WAIT_FOR_LATEST_FILE", "USE_MOST_RECENT_FILE"]),
413
- fetcher: dataFetcherSchema.optional()
414
- });
415
- var modelOutputSchema = z2.object({
416
- name: z2.string(),
417
- seriesKey: z2.boolean(),
418
- colour: z2.string().regex(/^#[0-9A-Fa-f]{6}$/).optional()
419
- });
420
- var projectConfigSchema = z2.object({
421
- $$id: z2.string().nullable(),
422
- $$modelGroupID: z2.string().nullable(),
423
- $$revision: z2.number().int().min(0).default(0),
424
- $$dockerImage: z2.string().nullable(),
425
- name: z2.string(),
426
- metadata: z2.record(z2.string(), z2.any()).optional(),
427
- forecastVariable: z2.enum(["PRICE"]),
428
- timeHorizon: z2.enum(["ACTUAL", "DAY_AHEAD", "WEEK_AHEAD"]),
429
- inputs: z2.array(modelInputSchema),
430
- outputs: z2.array(modelOutputSchema)
431
- });
432
-
433
- // src/config/parse-project-config.ts
434
- function parseProjectConfig(config) {
435
- return projectConfigSchema.parse(config);
436
- }
437
-
438
- // src/config/load-project-config.ts
439
- async function loadProjectConfig() {
440
- const result = await lilconfig("pd4castr", {
441
- searchPlaces: [PROJECT_CONFIG_FILE]
442
- }).search();
443
- if (!result?.config) {
444
- throw new Error(
445
- "No config found (docs: https://github.com/pipelabs/pd4castr-model-examples/blob/main/docs/005-config.md)."
446
- );
473
+ spinner.start("Waiting for login to complete...");
474
+ const authPayload = await completeAuthFlow(authCtx);
475
+ const updatedGlobalConfig = {
476
+ ...globalConfig,
477
+ accessToken: authPayload.accessToken,
478
+ accessTokenExpiresAt: authPayload.expiresAt
479
+ };
480
+ await updateGlobalConfig(updatedGlobalConfig);
481
+ spinner.succeed("Successfully logged in to the pd4castr API");
482
+ } catch (error) {
483
+ if (error instanceof ZodError2) {
484
+ spinner.fail("Config validation failed");
485
+ logZodIssues(error);
486
+ } else if (error instanceof Error) {
487
+ spinner.fail(error.message);
488
+ }
489
+ process.exit(1);
447
490
  }
448
- const config = parseProjectConfig(result.config);
449
- const projectRoot = path4.dirname(result.filepath);
450
- return {
451
- config,
452
- projectRoot
453
- };
454
491
  }
455
492
 
456
- // src/utils/create-link.ts
457
- var ESC = "\x1B";
458
- var OSC = `${ESC}]`;
459
- var SEP = ";";
460
- function createLink(text, url) {
461
- const start = `${OSC}8${SEP}${SEP}${url}${ESC}\\`;
462
- const end = `${OSC}8${SEP}${SEP}${ESC}\\`;
463
- return `${start}${text}${end}`;
493
+ // src/commands/login/index.ts
494
+ function registerLoginCommand(program2) {
495
+ program2.command("login").description("Logs in to the pd4castr API.").action(handleAction3);
464
496
  }
465
497
 
498
+ // src/commands/test/handle-action.ts
499
+ import path11 from "path";
500
+ import { ExecaError } from "execa";
501
+ import express from "express";
502
+ import ora4 from "ora";
503
+ import slugify from "slugify";
504
+ import { ZodError as ZodError3 } from "zod";
505
+
466
506
  // src/commands/test/utils/build-docker-image.ts
467
507
  import { execa } from "execa";
468
508
  async function buildDockerImage(dockerImage, ctx) {
@@ -476,15 +516,29 @@ async function buildDockerImage(dockerImage, ctx) {
476
516
  }
477
517
  }
478
518
 
519
+ // src/commands/test/utils/check-input-files.ts
520
+ import path8 from "path";
521
+ async function checkInputFiles(inputFiles, inputDataPath, ctx) {
522
+ for (const inputFile of inputFiles) {
523
+ const filePath = path8.join(ctx.projectRoot, inputDataPath, inputFile);
524
+ const exists = await isExistingPath(filePath);
525
+ if (!exists) {
526
+ throw new Error(
527
+ `Input data not found (${inputFile}) - did you need to run \`pd4castr fetch\`?`
528
+ );
529
+ }
530
+ }
531
+ }
532
+
479
533
  // src/commands/test/utils/create-input-handler.ts
480
- import path5 from "path";
534
+ import path9 from "path";
481
535
  function createInputHandler(inputFilesPath, modelIOChecks, ctx) {
482
536
  return (req, res) => {
483
537
  if (!modelIOChecks.isValidInput(req.params.filename)) {
484
538
  return res.status(404).json({ error: "File not found" });
485
539
  }
486
540
  modelIOChecks.trackInputHandled(req.params.filename);
487
- const filePath = path5.join(
541
+ const filePath = path9.join(
488
542
  ctx.projectRoot,
489
543
  inputFilesPath,
490
544
  req.params.filename
@@ -494,39 +548,20 @@ function createInputHandler(inputFilesPath, modelIOChecks, ctx) {
494
548
  }
495
549
 
496
550
  // src/commands/test/utils/create-output-handler.ts
497
- import path6 from "path";
498
- import fs4 from "fs/promises";
551
+ import fs6 from "fs/promises";
552
+ import path10 from "path";
499
553
  function createOutputHandler(modelIOChecks, ctx) {
500
554
  return async (req, res) => {
501
555
  modelIOChecks.trackOutputHandled();
502
- const outputPath = path6.join(ctx.projectRoot, TEST_OUTPUT_DATA_DIR);
503
- await fs4.mkdir(outputPath, { recursive: true });
504
- const outputFilePath = path6.join(outputPath, TEST_OUTPUT_FILENAME);
556
+ const outputPath = path10.join(ctx.projectRoot, TEST_OUTPUT_DATA_DIR);
557
+ await fs6.mkdir(outputPath, { recursive: true });
558
+ const outputFilePath = path10.join(outputPath, TEST_OUTPUT_FILENAME);
505
559
  const outputData = JSON.stringify(req.body, null, 2);
506
- await fs4.writeFile(outputFilePath, outputData, "utf8");
560
+ await fs6.writeFile(outputFilePath, outputData, "utf8");
507
561
  return res.status(200).json({ success: true });
508
562
  };
509
563
  }
510
564
 
511
- // src/commands/test/utils/check-input-files.ts
512
- import path7 from "path";
513
- async function checkInputFiles(inputFiles, inputDataPath, ctx) {
514
- for (const inputFile of inputFiles) {
515
- const filePath = path7.join(ctx.projectRoot, inputDataPath, inputFile);
516
- const exists = await isExistingPath(filePath);
517
- if (!exists) {
518
- throw new Error(
519
- `Input data not found (${inputFile}) - did you need to run \`pd4castr fetch\`?`
520
- );
521
- }
522
- }
523
- }
524
-
525
- // src/utils/get-input-filename.ts
526
- function getInputFilename(modelInput) {
527
- return `${modelInput.key}.json`;
528
- }
529
-
530
565
  // src/commands/test/utils/get-input-files.ts
531
566
  function getInputFiles(config) {
532
567
  const inputFiles = config.inputs.map((input2) => getInputFilename(input2));
@@ -588,7 +623,6 @@ async function runModelContainer(dockerImage, webserverURL, ctx) {
588
623
  const args = [
589
624
  "run",
590
625
  "--rm",
591
- "--network=host",
592
626
  ...envs.flatMap((env) => ["--env", env]),
593
627
  dockerImage
594
628
  ];
@@ -614,10 +648,10 @@ async function startWebServer(app, port) {
614
648
  }
615
649
 
616
650
  // src/commands/test/handle-action.ts
617
- async function handleAction3(options) {
651
+ async function handleAction4(options) {
618
652
  var _stack = [];
619
653
  try {
620
- const spinner = ora3("Starting model tests...").info();
654
+ const spinner = ora4("Starting model tests...").info();
621
655
  const app = express();
622
656
  const webServer = __using(_stack, await startWebServer(app, options.port));
623
657
  try {
@@ -638,13 +672,13 @@ async function handleAction3(options) {
638
672
  ctx
639
673
  );
640
674
  const handleOutput = createOutputHandler(modelIOChecks, ctx);
641
- const inputPath = path8.join(ctx.projectRoot, options.inputDir);
675
+ const inputPath = path11.join(ctx.projectRoot, options.inputDir);
642
676
  app.use(express.json());
643
677
  app.use("/data", express.static(inputPath));
644
678
  app.get("/input/:filename", handleInput);
645
679
  app.put("/output", handleOutput);
646
680
  spinner.start("Running model container");
647
- const webserverURL = `http://localhost:${options.port}`;
681
+ const webserverURL = `http://host.docker.internal:${options.port}`;
648
682
  await runModelContainer(dockerImage, webserverURL, ctx);
649
683
  spinner.succeed("Model run complete");
650
684
  for (const inputFile of inputFiles) {
@@ -659,7 +693,7 @@ async function handleAction3(options) {
659
693
  spinner.fail("Model I/O test failed");
660
694
  }
661
695
  if (modelIOChecks.isOutputHandled()) {
662
- const outputPath = path8.join(
696
+ const outputPath = path11.join(
663
697
  ctx.projectRoot,
664
698
  TEST_OUTPUT_DATA_DIR,
665
699
  TEST_OUTPUT_FILENAME
@@ -671,7 +705,7 @@ ${clickHereLink} to view output (${fileLink})
671
705
  `);
672
706
  }
673
707
  } catch (error) {
674
- if (error instanceof ZodError2) {
708
+ if (error instanceof ZodError3) {
675
709
  spinner.fail("Config validation failed");
676
710
  logZodIssues(error);
677
711
  } else if (error instanceof Error) {
@@ -697,123 +731,95 @@ function registerTestCommand(program2) {
697
731
  "-i, --input-dir <path>",
698
732
  "The input test data directory",
699
733
  TEST_INPUT_DATA_DIR
700
- ).option("-p, --port <port>", "The port to run the webserver on", "9800").action(handleAction3);
734
+ ).option("-p, --port <port>", "The port to run the webserver on", "9800").action(handleAction4);
701
735
  }
702
736
 
703
- // src/commands/fetch/handle-action.ts
704
- import ora4 from "ora";
705
- import path11 from "path";
706
- import { ZodError as ZodError3 } from "zod";
737
+ // src/program.ts
738
+ import { Command } from "commander";
707
739
 
708
- // src/utils/get-auth.ts
709
- import invariant from "tiny-invariant";
710
- async function getAuth() {
711
- const config = await loadGlobalConfig();
712
- if (!isAuthed(config)) {
713
- throw new Error("Not authenticated. Please run `pd4castr login` to login.");
740
+ // package.json
741
+ var package_default = {
742
+ name: "@pd4castr/cli",
743
+ version: "0.0.5",
744
+ description: "CLI tool for creating, testing, and publishing pd4castr models",
745
+ main: "dist/index.js",
746
+ type: "module",
747
+ bin: {
748
+ pd4castr: "dist/index.js"
749
+ },
750
+ files: [
751
+ "dist/**/*"
752
+ ],
753
+ scripts: {
754
+ build: "tsup",
755
+ dev: "tsup --watch",
756
+ cli: "node dist/index.js",
757
+ test: "vitest run",
758
+ "test:watch": "vitest",
759
+ "test:coverage": "vitest run --coverage",
760
+ lint: "eslint .",
761
+ "lint:fix": "eslint . --fix",
762
+ format: "prettier --write .",
763
+ "format:check": "prettier --check .",
764
+ "type-check": "tsc --noEmit",
765
+ prepublishOnly: "yarn build"
766
+ },
767
+ keywords: [
768
+ "cli",
769
+ "pd4castr"
770
+ ],
771
+ license: "UNLICENSED",
772
+ repository: {
773
+ type: "git",
774
+ url: "git+https://github.com/pipelabs/pd4castr-cli.git"
775
+ },
776
+ bugs: {
777
+ url: "https://github.com/pipelabs/pd4castr-cli/issues"
778
+ },
779
+ homepage: "https://github.com/pipelabs/pd4castr-cli#readme",
780
+ devDependencies: {
781
+ "@types/express": "4.17.21",
782
+ "@types/node": "24.1.0",
783
+ "@types/supertest": "6.0.3",
784
+ "@typescript-eslint/eslint-plugin": "8.38.0",
785
+ "@typescript-eslint/parser": "8.38.0",
786
+ eslint: "9.32.0",
787
+ "eslint-config-prettier": "10.1.8",
788
+ "eslint-plugin-simple-import-sort": "12.1.1",
789
+ "eslint-plugin-unicorn": "60.0.0",
790
+ "eslint-plugin-vitest": "0.5.4",
791
+ "jest-extended": "6.0.0",
792
+ memfs: "4.23.0",
793
+ msw: "2.10.4",
794
+ prettier: "3.6.2",
795
+ supertest: "7.1.4",
796
+ tsup: "8.5.0",
797
+ typescript: "5.8.3",
798
+ "typescript-eslint": "8.38.0",
799
+ vitest: "3.2.4"
800
+ },
801
+ dependencies: {
802
+ "@inquirer/prompts": "7.7.1",
803
+ auth0: "4.27.0",
804
+ commander: "14.0.0",
805
+ execa: "9.6.0",
806
+ express: "4.21.2",
807
+ ky: "1.8.2",
808
+ lilconfig: "3.1.3",
809
+ ora: "8.2.0",
810
+ slugify: "1.6.6",
811
+ tiged: "2.12.7",
812
+ "tiny-invariant": "1.3.3",
813
+ zod: "4.0.14"
814
+ },
815
+ engines: {
816
+ node: ">=20.0.0"
714
817
  }
715
- invariant(config.accessToken, "Access token is required");
716
- invariant(config.accessTokenExpiresAt, "Access token expiry is required");
717
- return {
718
- accessToken: config.accessToken,
719
- expiresAt: config.accessTokenExpiresAt
720
- };
721
- }
722
-
723
- // src/commands/fetch/utils/fetch-aemo-data.ts
724
- import path9 from "path";
725
- import fs5 from "fs/promises";
726
-
727
- // src/api.ts
728
- import ky2 from "ky";
729
- var api = ky2.create({
730
- prefixUrl: process.env.PD4CASTR_API_URL ?? API_URL
731
- });
732
-
733
- // src/commands/fetch/utils/fetch-aemo-data.ts
734
- async function fetchAEMOData(dataFetcher, authCtx, ctx) {
735
- const queryPath = path9.resolve(
736
- ctx.projectRoot,
737
- dataFetcher.config.fetchQuery
738
- );
739
- const querySQL = await fs5.readFile(queryPath, "utf8");
740
- const headers = { Authorization: `Bearer ${authCtx.accessToken}` };
741
- const payload2 = { query: querySQL, type: "AEMO_MMS" };
742
- const result = await api.post("data-fetcher/query", { json: payload2, headers }).json();
743
- return result;
744
- }
745
-
746
- // src/commands/fetch/utils/get-fetcher.ts
747
- var DATA_FETCHER_FNS = {
748
- AEMO_MMS: fetchAEMOData
749
818
  };
750
- function getFetcher(type) {
751
- const fetcher = DATA_FETCHER_FNS[type];
752
- if (!fetcher) {
753
- throw new Error(`Unsupported data fetcher type: ${type}`);
754
- }
755
- return fetcher;
756
- }
757
-
758
- // src/commands/fetch/utils/write-test-data.ts
759
- import path10 from "path";
760
- import fs6 from "fs/promises";
761
- async function writeTestData(inputData, modelInput, inputDataDir, ctx) {
762
- const inputDir = path10.resolve(ctx.projectRoot, inputDataDir);
763
- await fs6.mkdir(inputDir, { recursive: true });
764
- const inputFilename = getInputFilename(modelInput);
765
- const inputPath = path10.resolve(inputDir, inputFilename);
766
- await fs6.writeFile(inputPath, JSON.stringify(inputData, void 0, 2));
767
- }
768
-
769
- // src/commands/fetch/handle-action.ts
770
- var FETCHABLE_DATA_FETCHER_TYPES = /* @__PURE__ */ new Set(["AEMO_MMS"]);
771
- async function handleAction4(options) {
772
- const spinner = ora4("Starting data fetch...").start();
773
- try {
774
- const authCtx = await getAuth();
775
- const ctx = await loadProjectConfig();
776
- for (const input2 of ctx.config.inputs) {
777
- if (!input2.fetcher) {
778
- spinner.warn(`\`${input2.key}\` - no data fetcher defined, skipping`);
779
- continue;
780
- }
781
- if (!FETCHABLE_DATA_FETCHER_TYPES.has(input2.fetcher.type)) {
782
- spinner.warn(
783
- `\`${input2.key}\` (${input2.fetcher.type}) - unsupported, skipping`
784
- );
785
- continue;
786
- }
787
- spinner.start(`\`${input2.key}\` (${input2.fetcher.type}) - fetching...`);
788
- const fetchData = getFetcher(input2.fetcher.type);
789
- const output = await fetchData(input2.fetcher, authCtx, ctx);
790
- await writeTestData(output, input2, options.inputDir, ctx);
791
- spinner.succeed(`\`${input2.key}\` (${input2.fetcher.type}) - fetched`);
792
- }
793
- const inputPath = path11.resolve(ctx.projectRoot, options.inputDir);
794
- const link = createLink("Click here", `file://${inputPath}`);
795
- console.log(`
796
- ${link} to view fetched data
797
- `);
798
- } catch (error) {
799
- if (error instanceof ZodError3) {
800
- spinner.fail("Config validation failed");
801
- logZodIssues(error);
802
- } else if (error instanceof Error) {
803
- spinner.fail(error.message);
804
- }
805
- process.exit(1);
806
- }
807
- }
808
819
 
809
- // src/commands/fetch/index.ts
810
- function registerFetchCommand(program2) {
811
- program2.command("fetch").description("Fetches test data from configured data fetchers.").option(
812
- "-i, --input-dir <path>",
813
- "The input test data directory",
814
- TEST_INPUT_DATA_DIR
815
- ).action(handleAction4);
816
- }
820
+ // src/program.ts
821
+ var program = new Command();
822
+ program.name("pd4castr").description("CLI tool for pd4castr").version(package_default.version);
817
823
 
818
824
  // src/index.ts
819
825
  registerInitCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pd4castr/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "CLI tool for creating, testing, and publishing pd4castr models",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -45,6 +45,7 @@
45
45
  "@typescript-eslint/parser": "8.38.0",
46
46
  "eslint": "9.32.0",
47
47
  "eslint-config-prettier": "10.1.8",
48
+ "eslint-plugin-simple-import-sort": "12.1.1",
48
49
  "eslint-plugin-unicorn": "60.0.0",
49
50
  "eslint-plugin-vitest": "0.5.4",
50
51
  "jest-extended": "6.0.0",