@speedkit/cli 4.13.0 → 4.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ # [4.15.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.14.0...v4.15.0) (2026-07-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * **post-deploy:** post deployment job ([9c1cf71](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/9c1cf71ec608b78bbcd422ef3ebda992084c32ee))
7
+ * **post-deploy:** post deployment job check ([72fc000](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/72fc000472728f2c2accca3a025363bcdaa713b0))
8
+
9
+ # [4.14.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.13.0...v4.14.0) (2026-06-29)
10
+
11
+
12
+ ### Features
13
+
14
+ * implement autoPrewarmService ([102d893](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/102d8931b985ef270d6dd9cadc879158d867b9ba))
15
+
1
16
  # [4.13.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.12.0...v4.13.0) (2026-06-25)
2
17
 
3
18
 
package/README.md CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @speedkit/cli
21
21
  $ sk COMMAND
22
22
  running command...
23
23
  $ sk (--version)
24
- @speedkit/cli/4.13.0 linux-x64 node-v22.23.1
24
+ @speedkit/cli/4.15.0 linux-x64 node-v22.23.1
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -65,6 +65,7 @@ By either...
65
65
 
66
66
  # Commands
67
67
  <!-- commands -->
68
+ * [`sk auto-prewarm CUSTOMERPATH`](#sk-auto-prewarm-customerpath)
68
69
  * [`sk autocomplete [SHELL]`](#sk-autocomplete-shell)
69
70
  * [`sk build-parameter-query CUSTOMERPATH`](#sk-build-parameter-query-customerpath)
70
71
  * [`sk build-prewarm-query CUSTOMERPATH`](#sk-build-prewarm-query-customerpath)
@@ -83,6 +84,37 @@ By either...
83
84
  * [`sk pull CUSTOMERPATH`](#sk-pull-customerpath)
84
85
  * [`sk wpt-launcher URLS TESTID`](#sk-wpt-launcher-urls-testid)
85
86
 
87
+ ## `sk auto-prewarm CUSTOMERPATH`
88
+
89
+ Build and execute the prewarm query for a customer config and start a prewarm job from the resulting URLs — no CSV needed
90
+
91
+ ```
92
+ USAGE
93
+ $ sk auto-prewarm CUSTOMERPATH [-c <value>] [-p <value> | -v <value>] [-k] [-q] [--verbose]
94
+
95
+ ARGUMENTS
96
+ CUSTOMERPATH The customer config path
97
+
98
+ FLAGS
99
+ -c, --configName=<value> [default: production] The costumer config name
100
+ -k, --keepParameterSorting Prevents alphabetical sorting of the URL parameters
101
+ -p, --variationPath=<value> path to variations.csv
102
+ -q, --quiet less output non interactive
103
+ -v, --variation=<value> [default: default]
104
+ --verbose Log information about failed requests
105
+
106
+ DESCRIPTION
107
+ Build and execute the prewarm query for a customer config and start a prewarm job from the resulting URLs — no CSV
108
+ needed
109
+
110
+ EXAMPLES
111
+ $ sk auto-prewarm <customerPath> -c <configName>
112
+
113
+ $ sk auto-prewarm customers/decathlon.de -c production
114
+
115
+ $ sk auto-prewarm customers/decathlon.de -v MOBILE,DESKTOP
116
+ ```
117
+
86
118
  ## `sk autocomplete [SHELL]`
87
119
 
88
120
  Display autocomplete installation instructions.
@@ -143,13 +175,14 @@ Build a prewarm query that can be executed in Athena
143
175
 
144
176
  ```
145
177
  USAGE
146
- $ sk build-prewarm-query CUSTOMERPATH [-c <value>] [-q]
178
+ $ sk build-prewarm-query CUSTOMERPATH [-c <value>] [-q] [-e]
147
179
 
148
180
  ARGUMENTS
149
181
  CUSTOMERPATH The customer config path
150
182
 
151
183
  FLAGS
152
184
  -c, --configName=<value> [default: production] The costumer config name
185
+ -e, --execute Execute the query directly and write the result to a <app>-<date>-prewarm.csv file
153
186
  -q, --quiet Output only the raw query without clipboard copy or styling
154
187
 
155
188
  DESCRIPTION
@@ -0,0 +1,19 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class AutoPrewarm extends Command {
3
+ static args: {
4
+ customerPath: import("@oclif/core/interfaces").Arg<string, {
5
+ exists?: boolean;
6
+ }>;
7
+ };
8
+ static description: string;
9
+ static examples: string[];
10
+ static flags: {
11
+ variation: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
+ variationPath: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ keepParameterSorting: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
+ verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ configName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
17
+ };
18
+ run(): Promise<void>;
19
+ }
@@ -0,0 +1,53 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { CLI_CONFIG_NAME, CLI_CUSTOMER_CONFIG, CLIParameters, CLIParametersChar, CLIParametersExample, } from "../models/cli-parameters.js";
3
+ import { CliConfig } from "../helpers/cli-config.js";
4
+ import { AutoPreWarmFactory } from "../services/prewarm/index.js";
5
+ export default class AutoPrewarm extends Command {
6
+ static args = CLI_CUSTOMER_CONFIG;
7
+ static description = "Build and execute the prewarm query for a customer config and start a prewarm job from the resulting URLs — no CSV needed";
8
+ static examples = [
9
+ `$ sk auto-prewarm <${CLIParameters.CustomerPath}> -${CLIParametersChar.ConfigName} <${CLIParameters.ConfigName}>`,
10
+ `$ sk auto-prewarm ${CLIParametersExample.CustomerPath} -${CLIParametersChar.ConfigName} ${CLIParametersExample.ConfigName}`,
11
+ `$ sk auto-prewarm ${CLIParametersExample.CustomerPath} -${CLIParametersChar.Variation} ${CLIParametersExample.Variations}`,
12
+ ];
13
+ static flags = {
14
+ ...CLI_CONFIG_NAME,
15
+ [CLIParameters.Variation]: Flags.string({
16
+ char: CLIParametersChar.Variation,
17
+ default: "default",
18
+ }),
19
+ [CLIParameters.VariationPath]: Flags.file({
20
+ char: CLIParametersChar.VariationPath,
21
+ exclusive: [CLIParameters.Variation],
22
+ description: "path to variations.csv",
23
+ }),
24
+ [CLIParameters.KeepParameterSorting]: Flags.boolean({
25
+ char: CLIParametersChar.KeepParameterSorting,
26
+ default: false,
27
+ description: "Prevents alphabetical sorting of the URL parameters",
28
+ }),
29
+ [CLIParameters.Quiet]: Flags.boolean({
30
+ char: CLIParametersChar.Quiet,
31
+ default: false,
32
+ description: "less output non interactive",
33
+ }),
34
+ [CLIParameters.Verbose]: Flags.boolean({
35
+ default: false,
36
+ description: "Log information about failed requests",
37
+ }),
38
+ };
39
+ async run() {
40
+ const { args: { customerPath }, flags: { configName, variation, variationPath, keepParameterSorting, quiet, verbose, }, } = await this.parse(AutoPrewarm);
41
+ const service = await new AutoPreWarmFactory({
42
+ customerPath,
43
+ configName,
44
+ variation,
45
+ variationPath,
46
+ sortParameters: !keepParameterSorting,
47
+ quiet,
48
+ verbose,
49
+ isWindows: this.config.windows,
50
+ }, new CliConfig(this.config).load()).getService();
51
+ await service.run();
52
+ }
53
+ }
@@ -9,6 +9,7 @@ export default class BuildPrewarmQuery extends Command {
9
9
  static examples: string[];
10
10
  static flags: {
11
11
  quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ execute: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
13
  configName: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
14
  };
14
15
  run(): Promise<void>;
@@ -17,14 +17,20 @@ export default class BuildPrewarmQuery extends Command {
17
17
  description: "Output only the raw query without clipboard copy or styling",
18
18
  default: false,
19
19
  }),
20
+ execute: Flags.boolean({
21
+ char: "e",
22
+ description: "Execute the query directly and write the result to a <app>-<date>-prewarm.csv file",
23
+ default: false,
24
+ }),
20
25
  };
21
26
  async run() {
22
- const { args: { customerPath }, flags: { configName, quiet }, } = await this.parse(BuildPrewarmQuery);
27
+ const { args: { customerPath }, flags: { configName, quiet, execute }, } = await this.parse(BuildPrewarmQuery);
23
28
  const service = await new QueryBuilderFactory({
24
29
  configName,
25
30
  customerPath,
26
31
  isWindows: this.config.windows,
27
32
  quiet,
33
+ execute,
28
34
  }, new CliConfig(this.config).load()).buildService(QueryTpe.prewarm);
29
35
  await service.run();
30
36
  }
@@ -0,0 +1,9 @@
1
+ import { EntityManager } from "baqend";
2
+ import { PostDeployCheckInterface } from "../../deploy-service-model.js";
3
+ import { CliService } from "../../../cli/index.js";
4
+ export declare class RefreshDeploymentJobCheck implements PostDeployCheckInterface {
5
+ private entityManager;
6
+ private cli;
7
+ constructor(entityManager: EntityManager, cli: CliService);
8
+ check(): Promise<void>;
9
+ }
@@ -0,0 +1,31 @@
1
+ import { Set } from "baqend";
2
+ import ApplicationError from "../../../error-handling/error/application-error.js";
3
+ export class RefreshDeploymentJobCheck {
4
+ entityManager;
5
+ cli;
6
+ constructor(entityManager, cli) {
7
+ this.entityManager = entityManager;
8
+ this.cli = cli;
9
+ }
10
+ async check() {
11
+ const jobs = await this.entityManager["jobs.Definition"]
12
+ .find()
13
+ .resultList();
14
+ const requiredJobs = ["HTML", "Landing Page", "CSS, JS, Font"];
15
+ const foundJobNames = new Set(jobs.map((job) => job?.data?.name || " "));
16
+ const missingRequiredJobs = requiredJobs.filter((job) => !foundJobNames.has(job));
17
+ // Deployment jobs: soft warning only
18
+ const deploymentJobsMissing = jobs.filter((job) => job.type === "Deploymentjob" && job.created !== true);
19
+ if (deploymentJobsMissing.length > 0) {
20
+ this.cli.writeWarning(" Deployment jobs are missing. These are normally created on first deployment run.");
21
+ }
22
+ if (missingRequiredJobs.length > 0) {
23
+ throw new ApplicationError("MISSING JOBS", [
24
+ `Missing required jobs: ${missingRequiredJobs.join(", ")}`,
25
+ 'To create missing jobs, follow the steps here:",\n' +
26
+ ' "https://www.notion.so/baqend/How-to-create-missing-Jobs-306de49f8a9980f397c2ebe693da4aac?v=fcdbfc994dca4aa6be42228b37609d48&source=copy_link"\n' +
27
+ " ",
28
+ ]);
29
+ }
30
+ }
31
+ }
@@ -14,6 +14,7 @@ import { SplitChangeCheck } from "./checks/pre-deploy/split-change-check.js";
14
14
  import { ConfigApiContext, ConfigApiServiceFactory, } from "../config-api/index.js";
15
15
  import { DeprecatedDeploymentDetectionCheck } from "./checks/post-deploy/deprecated-deployment-detection-check.js";
16
16
  import { DeviceDetectionChangedCheck } from "./checks/pre-deploy/device-detection-changed-check.js";
17
+ import { RefreshDeploymentJobCheck } from "./checks/post-deploy/refresh-deployment-job-check.js";
17
18
  export default class DeployServiceFactory {
18
19
  context;
19
20
  cliConfig;
@@ -45,7 +46,7 @@ export default class DeployServiceFactory {
45
46
  ], cliService, [
46
47
  new SplitChangeCheck(cliService, files, configApi, customer, configName, this.context.isProduction),
47
48
  new DeviceDetectionChangedCheck(cliService, files, configApi, customer, configName, this.context.isProduction),
48
- ], this.getPostDeployChecks(files, cliService, configApi, customer.app), this.context.dryRun);
49
+ ], await this.getPostDeployChecks(files, cliService, configApi, customer.app), this.context.dryRun);
49
50
  }
50
51
  getConfigApi(app) {
51
52
  const configApiContext = new ConfigApiContext(app);
@@ -56,11 +57,12 @@ export default class DeployServiceFactory {
56
57
  const integrationApi = new IntegrationApiFactory(integrationApiContext, this.cliConfig).buildService();
57
58
  return integrationApi.run();
58
59
  }
59
- getPostDeployChecks(files, cliService, configApi, app) {
60
+ async getPostDeployChecks(files, cliService, configApi, app) {
60
61
  return [
61
62
  new SaveFilesCheck(files, cliService),
62
63
  new RevalidatedModuleCheck(configApi, cliService, app),
63
64
  new DeprecatedDeploymentDetectionCheck(files, cliService, configApi, app),
65
+ new RefreshDeploymentJobCheck(await configApi.client.getEntityManager(), cliService),
64
66
  ];
65
67
  }
66
68
  }
@@ -0,0 +1,25 @@
1
+ import { UserCliConfig } from "../../helpers/cli-config.js";
2
+ import { PreWarmService } from "./pre-warm-service.js";
3
+ export interface AutoPreWarmContext {
4
+ readonly customerPath: string;
5
+ readonly configName: string;
6
+ readonly variation?: string;
7
+ readonly variationPath?: string;
8
+ readonly sortParameters?: boolean;
9
+ readonly quiet?: boolean;
10
+ readonly verbose?: boolean;
11
+ readonly isWindows?: boolean;
12
+ }
13
+ /**
14
+ * Wires a {@link PreWarmService} that gets its URLs from a freshly built and
15
+ * executed prewarm query instead of a CSV file. Reads the app from the customer
16
+ * config, builds the prewarm query via {@link QueryBuilderFactory}, runs it
17
+ * against Athena (Baqend native path, AWS fallback), and feeds the resulting
18
+ * URLs straight into the prewarm job.
19
+ */
20
+ export declare class AutoPreWarmFactory {
21
+ private context;
22
+ private userConfig;
23
+ constructor(context: AutoPreWarmContext, userConfig: UserCliConfig);
24
+ getService(): Promise<PreWarmService>;
25
+ }
@@ -0,0 +1,42 @@
1
+ import { CliContext, CliServiceFactory } from "../cli/index.js";
2
+ import { QueryBuilderFactory } from "../query-builder/query-builder-factory.js";
3
+ import { QueryTpe } from "../query-builder/query-builder-model.js";
4
+ import { ConfigApiContext, ConfigApiServiceFactory, } from "../config-api/index.js";
5
+ import { AssetApiClient } from "./assets/asset-api-client.js";
6
+ import { CsvReader } from "./csv-reader.js";
7
+ import { PreWarmService } from "./pre-warm-service.js";
8
+ import { resolveVariations } from "./pre-warm-factory.js";
9
+ /**
10
+ * Wires a {@link PreWarmService} that gets its URLs from a freshly built and
11
+ * executed prewarm query instead of a CSV file. Reads the app from the customer
12
+ * config, builds the prewarm query via {@link QueryBuilderFactory}, runs it
13
+ * against Athena (Baqend native path, AWS fallback), and feeds the resulting
14
+ * URLs straight into the prewarm job.
15
+ */
16
+ export class AutoPreWarmFactory {
17
+ context;
18
+ userConfig;
19
+ constructor(context, userConfig) {
20
+ this.context = context;
21
+ this.userConfig = userConfig;
22
+ }
23
+ async getService() {
24
+ const cli = new CliServiceFactory(new CliContext(this.context.quiet)).getService();
25
+ const queryService = await new QueryBuilderFactory({
26
+ configName: this.context.configName,
27
+ customerPath: this.context.customerPath,
28
+ isWindows: this.context.isWindows,
29
+ quiet: this.context.quiet,
30
+ execute: true,
31
+ }, this.userConfig).buildService(QueryTpe.prewarm);
32
+ const app = queryService.app;
33
+ const rows = await queryService.execute();
34
+ const urls = rows.map((row) => row.url).filter(Boolean);
35
+ const configApi = new ConfigApiServiceFactory(new ConfigApiContext(app)).getService();
36
+ const entityManager = await configApi.client.getEntityManager();
37
+ const assetApiClient = new AssetApiClient(entityManager.token, app, this.context.sortParameters);
38
+ const csvReader = new CsvReader();
39
+ const variations = resolveVariations(csvReader, this.context.variation, this.context.variationPath);
40
+ return new PreWarmService(assetApiClient, cli, urls, variations, this.context.verbose);
41
+ }
42
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./pre-warm-service.js";
2
2
  export * from "./pre-warm-factory.js";
3
+ export * from "./auto-pre-warm-factory.js";
3
4
  export * from "./pre-warm-model.js";
@@ -1,3 +1,4 @@
1
1
  export * from "./pre-warm-service.js";
2
2
  export * from "./pre-warm-factory.js";
3
+ export * from "./auto-pre-warm-factory.js";
3
4
  export * from "./pre-warm-model.js";
@@ -1,4 +1,5 @@
1
1
  import { PreWarmContext, PreWarmService } from "./index.js";
2
+ import { CsvReader } from "./csv-reader.js";
2
3
  export declare class PreWarmFactory {
3
4
  private context;
4
5
  constructor(context: PreWarmContext);
@@ -6,3 +7,10 @@ export declare class PreWarmFactory {
6
7
  private getUrls;
7
8
  private getVariations;
8
9
  }
10
+ /**
11
+ * Resolve the list of variations to prewarm: a `variationPath` CSV takes
12
+ * precedence over a comma-separated `variation` string, falling back to the
13
+ * default variation. Shared by {@link PreWarmFactory} and the `auto-prewarm`
14
+ * factory.
15
+ */
16
+ export declare function resolveVariations(csvReader: CsvReader, variation?: string, variationPath?: string): string[];
@@ -22,13 +22,22 @@ export class PreWarmFactory {
22
22
  return csvReader.readPath(this.context.path);
23
23
  }
24
24
  getVariations(csvReader) {
25
- let variations = DEFAULT_VARIATION;
26
- if (this.context.variation) {
27
- variations = this.context.variation.split(VARIATION_SEPARATOR);
28
- }
29
- if (this.context.variationPath) {
30
- variations = csvReader.readPath(this.context.variationPath);
31
- }
32
- return variations;
25
+ return resolveVariations(csvReader, this.context.variation, this.context.variationPath);
33
26
  }
34
27
  }
28
+ /**
29
+ * Resolve the list of variations to prewarm: a `variationPath` CSV takes
30
+ * precedence over a comma-separated `variation` string, falling back to the
31
+ * default variation. Shared by {@link PreWarmFactory} and the `auto-prewarm`
32
+ * factory.
33
+ */
34
+ export function resolveVariations(csvReader, variation, variationPath) {
35
+ let variations = DEFAULT_VARIATION;
36
+ if (variation) {
37
+ variations = variation.split(VARIATION_SEPARATOR);
38
+ }
39
+ if (variationPath) {
40
+ variations = csvReader.readPath(variationPath);
41
+ }
42
+ return variations;
43
+ }
@@ -2,6 +2,7 @@ import { QueryBuilderService } from "./query-builder-service.js";
2
2
  import { IntegrationApiContext, IntegrationApiFactory, } from "../integration-api/index.js";
3
3
  import { INTEGRATION_FILES } from "../../models/files.js";
4
4
  import { CliServiceFactory } from "../cli/index.js";
5
+ import { AthenaServiceFactory } from "../athena/index.js";
5
6
  import { safe } from "../../helpers/safe.js";
6
7
  import { ParseConfigSpeedKitError } from "./error/parse-config-speed-kit-error.js";
7
8
  import { evaluateLocalSpeedKitConfig } from "../../helpers/evaluate-speed-kit-config.js";
@@ -25,8 +26,13 @@ export class QueryBuilderFactory {
25
26
  queryType,
26
27
  isWindows: this.context.isWindows,
27
28
  quiet: this.context.quiet,
29
+ execute: this.context.execute,
28
30
  };
29
- return new QueryBuilderService(serviceContext, speedKitConfig, cli);
31
+ // The Athena service is only needed when the query is executed directly.
32
+ const athenaService = this.context.execute
33
+ ? await new AthenaServiceFactory(app).getService()
34
+ : undefined;
35
+ return new QueryBuilderService(serviceContext, speedKitConfig, cli, athenaService);
30
36
  }
31
37
  async getIntegrationFiles() {
32
38
  const integrationApiContext = new IntegrationApiContext(this.context.customerPath, this.context.configName, ["config_SpeedKit"]);
@@ -1,6 +1,7 @@
1
1
  export interface QueryBuilderContext {
2
2
  isWindows?: boolean;
3
3
  quiet?: boolean;
4
+ execute?: boolean;
4
5
  queryType: QueryTpe;
5
6
  app: string;
6
7
  }
@@ -9,6 +10,7 @@ export interface QueryBuilderFactoryContext {
9
10
  readonly configName: string;
10
11
  readonly isWindows?: boolean;
11
12
  readonly quiet?: boolean;
13
+ readonly execute?: boolean;
12
14
  }
13
15
  export declare enum QueryTpe {
14
16
  prewarm = "prewarm",
@@ -1,11 +1,28 @@
1
1
  import { QueryBuilderContext, SpeedKitConfig } from "./query-builder-model.js";
2
2
  import { CliServiceInterface } from "../cli/index.js";
3
+ import { AthenaService } from "../athena/index.js";
3
4
  export declare class QueryBuilderService {
4
5
  private context;
5
6
  private speedKitConfig;
6
7
  private cli;
7
- constructor(context: QueryBuilderContext, speedKitConfig: SpeedKitConfig, cli: CliServiceInterface);
8
+ private athenaService?;
9
+ constructor(context: QueryBuilderContext, speedKitConfig: SpeedKitConfig, cli: CliServiceInterface, athenaService?: AthenaService);
10
+ /** The app the query targets, as read from the customer config. */
11
+ get app(): string;
8
12
  run(): Promise<void>;
13
+ /**
14
+ * Build and execute the query against Athena (Baqend native path, AWS
15
+ * fallback) and return the raw rows. Used by callers that consume the result
16
+ * directly instead of exporting it (e.g. `auto-prewarm`).
17
+ */
18
+ execute<T>(): Promise<T[]>;
19
+ private runQuery;
20
+ /**
21
+ * Execute the built query against Athena (Baqend native path, AWS fallback)
22
+ * and write the rows to a `<app>-<YYYY-MM-DD>-<queryType>.csv` file in the
23
+ * current working directory.
24
+ */
25
+ private executeAndExport;
9
26
  query(): string;
10
27
  private buildQuery;
11
28
  }
@@ -1,18 +1,28 @@
1
1
  import { getSpeedKitConfigVersion } from "../../helpers/get-parsed-config.js";
2
2
  import { getBlacklistedFilter, getParameterFilter, getParameterReplaceFilter, getStrippedParameters, getWhitelistedFilter, } from "../../helpers/build-query-helper.js";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
3
5
  import clipboardy from "clipboardy";
6
+ import json2Csv from "json2csv";
4
7
  import { QueryTpe, } from "./query-builder-model.js";
5
8
  import { Prewarm } from "./query/prewarm.js";
6
9
  import { Parameter } from "./query/parameter.js";
10
+ import { AthenaBucket } from "../athena/index.js";
7
11
  import { safe } from "../../helpers/safe.js";
8
12
  export class QueryBuilderService {
9
13
  context;
10
14
  speedKitConfig;
11
15
  cli;
12
- constructor(context, speedKitConfig, cli) {
16
+ athenaService;
17
+ constructor(context, speedKitConfig, cli, athenaService) {
13
18
  this.context = context;
14
19
  this.speedKitConfig = speedKitConfig;
15
20
  this.cli = cli;
21
+ this.athenaService = athenaService;
22
+ }
23
+ /** The app the query targets, as read from the customer config. */
24
+ get app() {
25
+ return this.context.app;
16
26
  }
17
27
  async run() {
18
28
  this.cli.startAction(`QUERY:BUILD:${this.context.queryType}`, `build ${this.context.queryType}-query`);
@@ -31,6 +41,10 @@ export class QueryBuilderService {
31
41
  return;
32
42
  }
33
43
  this.cli.successAction(`QUERY:BUILD:${this.context.queryType}`);
44
+ if (this.context.execute) {
45
+ await this.executeAndExport(result.data);
46
+ return;
47
+ }
34
48
  if (this.context.quiet) {
35
49
  process.stdout.write(result.data);
36
50
  return;
@@ -43,6 +57,42 @@ export class QueryBuilderService {
43
57
  this.cli.writeSuccess("Result has been copied to the clipboard 📋");
44
58
  }
45
59
  }
60
+ /**
61
+ * Build and execute the query against Athena (Baqend native path, AWS
62
+ * fallback) and return the raw rows. Used by callers that consume the result
63
+ * directly instead of exporting it (e.g. `auto-prewarm`).
64
+ */
65
+ async execute() {
66
+ return this.runQuery(this.query());
67
+ }
68
+ async runQuery(query) {
69
+ if (!this.athenaService) {
70
+ throw new Error("No Athena service available to execute the query");
71
+ }
72
+ return this.athenaService.getResult(query, [], undefined, AthenaBucket.RumPi);
73
+ }
74
+ /**
75
+ * Execute the built query against Athena (Baqend native path, AWS fallback)
76
+ * and write the rows to a `<app>-<YYYY-MM-DD>-<queryType>.csv` file in the
77
+ * current working directory.
78
+ */
79
+ async executeAndExport(query) {
80
+ this.cli.startAction("QUERY:EXECUTE", "execute query and export csv");
81
+ const result = await safe(this.runQuery(query));
82
+ if (result.success !== true) {
83
+ this.cli.failAction("QUERY:EXECUTE", result.error);
84
+ this.cli.writeError(result.error);
85
+ return;
86
+ }
87
+ const rows = result.data;
88
+ const date = new Date().toISOString().split("T")[0];
89
+ const fileName = `${this.context.app}-${date}-${this.context.queryType}.csv`;
90
+ const filePath = join(process.cwd(), fileName);
91
+ const csv = rows.length > 0 ? json2Csv.parse(rows) : "";
92
+ await writeFile(filePath, csv, "utf8");
93
+ this.cli.successAction("QUERY:EXECUTE");
94
+ this.cli.writeSuccess(`Wrote ${rows.length} rows to ${filePath} 📄`);
95
+ }
46
96
  query() {
47
97
  const parameters = getStrippedParameters(this.speedKitConfig);
48
98
  const parameterReplaceFilter = getParameterReplaceFilter(parameters, getSpeedKitConfigVersion(this.speedKitConfig));
@@ -1,5 +1,86 @@
1
1
  {
2
2
  "commands": {
3
+ "auto-prewarm": {
4
+ "aliases": [],
5
+ "args": {
6
+ "customerPath": {
7
+ "description": "The customer config path",
8
+ "name": "customerPath",
9
+ "required": true
10
+ }
11
+ },
12
+ "description": "Build and execute the prewarm query for a customer config and start a prewarm job from the resulting URLs — no CSV needed",
13
+ "examples": [
14
+ "$ sk auto-prewarm <customerPath> -c <configName>",
15
+ "$ sk auto-prewarm customers/decathlon.de -c production",
16
+ "$ sk auto-prewarm customers/decathlon.de -v MOBILE,DESKTOP"
17
+ ],
18
+ "flags": {
19
+ "configName": {
20
+ "char": "c",
21
+ "description": "The costumer config name",
22
+ "name": "configName",
23
+ "required": false,
24
+ "default": "production",
25
+ "hasDynamicHelp": false,
26
+ "multiple": false,
27
+ "type": "option"
28
+ },
29
+ "variation": {
30
+ "char": "v",
31
+ "name": "variation",
32
+ "default": "default",
33
+ "hasDynamicHelp": false,
34
+ "multiple": false,
35
+ "type": "option"
36
+ },
37
+ "variationPath": {
38
+ "char": "p",
39
+ "description": "path to variations.csv",
40
+ "exclusive": [
41
+ "variation"
42
+ ],
43
+ "name": "variationPath",
44
+ "hasDynamicHelp": false,
45
+ "multiple": false,
46
+ "type": "option"
47
+ },
48
+ "keepParameterSorting": {
49
+ "char": "k",
50
+ "description": "Prevents alphabetical sorting of the URL parameters",
51
+ "name": "keepParameterSorting",
52
+ "allowNo": false,
53
+ "type": "boolean"
54
+ },
55
+ "quiet": {
56
+ "char": "q",
57
+ "description": "less output non interactive",
58
+ "name": "quiet",
59
+ "allowNo": false,
60
+ "type": "boolean"
61
+ },
62
+ "verbose": {
63
+ "description": "Log information about failed requests",
64
+ "name": "verbose",
65
+ "allowNo": false,
66
+ "type": "boolean"
67
+ }
68
+ },
69
+ "hasDynamicHelp": false,
70
+ "hiddenAliases": [],
71
+ "id": "auto-prewarm",
72
+ "pluginAlias": "@speedkit/cli",
73
+ "pluginName": "@speedkit/cli",
74
+ "pluginType": "core",
75
+ "strict": true,
76
+ "enableJsonFlag": false,
77
+ "isESM": true,
78
+ "relativePath": [
79
+ "dist",
80
+ "commands",
81
+ "auto-prewarm.js"
82
+ ]
83
+ },
3
84
  "build-parameter-query": {
4
85
  "aliases": [],
5
86
  "args": {
@@ -72,6 +153,13 @@
72
153
  "name": "quiet",
73
154
  "allowNo": false,
74
155
  "type": "boolean"
156
+ },
157
+ "execute": {
158
+ "char": "e",
159
+ "description": "Execute the query directly and write the result to a <app>-<date>-prewarm.csv file",
160
+ "name": "execute",
161
+ "allowNo": false,
162
+ "type": "boolean"
75
163
  }
76
164
  },
77
165
  "hasDynamicHelp": false,
@@ -759,5 +847,5 @@
759
847
  ]
760
848
  }
761
849
  },
762
- "version": "4.13.0"
850
+ "version": "4.15.0"
763
851
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.13.0",
4
+ "version": "4.15.0",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"