@speedkit/cli 4.17.1 → 4.19.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +75 -1
  3. package/dist/commands/browser/clean.d.ts +9 -0
  4. package/dist/commands/browser/clean.js +43 -0
  5. package/dist/commands/browser/list.d.ts +6 -0
  6. package/dist/commands/browser/list.js +29 -0
  7. package/dist/commands/browser/update.d.ts +6 -0
  8. package/dist/commands/browser/update.js +32 -0
  9. package/dist/commands/config/setup.d.ts +10 -0
  10. package/dist/commands/config/setup.js +28 -0
  11. package/dist/helpers/cli-config.d.ts +19 -0
  12. package/dist/helpers/cli-config.js +70 -4
  13. package/dist/helpers/cli-config.spec.d.ts +1 -0
  14. package/dist/helpers/cli-config.spec.js +70 -0
  15. package/dist/helpers/environment.d.ts +15 -0
  16. package/dist/helpers/environment.js +40 -0
  17. package/dist/helpers/environment.spec.d.ts +1 -0
  18. package/dist/helpers/environment.spec.js +28 -0
  19. package/dist/hooks/init/first-run.d.ts +8 -0
  20. package/dist/hooks/init/first-run.js +36 -0
  21. package/dist/services/customer-config/templates/config_SpeedKit.js.hbs +11 -0
  22. package/dist/services/setup/browser/browser-detector.d.ts +27 -0
  23. package/dist/services/setup/browser/browser-detector.js +47 -0
  24. package/dist/services/setup/browser/browser-manager.d.ts +43 -0
  25. package/dist/services/setup/browser/browser-manager.js +101 -0
  26. package/dist/services/setup/browser/browser-setup-service.d.ts +28 -0
  27. package/dist/services/setup/browser/browser-setup-service.js +136 -0
  28. package/dist/services/setup/difftool/difftool-detector.d.ts +14 -0
  29. package/dist/services/setup/difftool/difftool-detector.js +27 -0
  30. package/dist/services/setup/difftool/difftool-setup-service.d.ts +15 -0
  31. package/dist/services/setup/difftool/difftool-setup-service.js +46 -0
  32. package/dist/services/setup/index.d.ts +10 -0
  33. package/dist/services/setup/index.js +10 -0
  34. package/dist/services/setup/os/detector-helper.d.ts +29 -0
  35. package/dist/services/setup/os/detector-helper.js +65 -0
  36. package/dist/services/setup/os/tool-registry.d.ts +38 -0
  37. package/dist/services/setup/os/tool-registry.js +194 -0
  38. package/dist/services/setup/os/tool-registry.spec.d.ts +1 -0
  39. package/dist/services/setup/os/tool-registry.spec.js +26 -0
  40. package/dist/services/setup/setup-context.d.ts +14 -0
  41. package/dist/services/setup/setup-context.js +15 -0
  42. package/dist/services/setup/setup-service-factory.d.ts +10 -0
  43. package/dist/services/setup/setup-service-factory.js +37 -0
  44. package/dist/services/setup/setup-service.d.ts +20 -0
  45. package/dist/services/setup/setup-service.js +37 -0
  46. package/oclif.manifest.json +122 -1
  47. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ # [4.19.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.18.0...v4.19.0) (2026-08-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * **customer-config:** default /services/login_with_shop for Shopify, tighten comment ([8299dfb](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/8299dfba0248cc6e38ba86f722e14082b5c960ec))
7
+ * **customer-config:** scaffold native disabledScopes in config_SpeedKit template ([7535c7d](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/7535c7db670ff653be91f249cdaec664d08d9f05))
8
+
9
+ # [4.18.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.17.1...v4.18.0) (2026-08-03)
10
+
11
+
12
+ ### Features
13
+
14
+ * implement first run wizzard ([2757cb3](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/2757cb340d28b562b4728a6a98badd583a54e444))
15
+
1
16
  ## [4.17.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.17.0...v4.17.1) (2026-07-17)
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.17.1 linux-x64 node-v22.23.1
24
+ @speedkit/cli/4.19.0 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -68,9 +68,13 @@ By either...
68
68
  * [`sk auto-prewarm CUSTOMERPATH`](#sk-auto-prewarm-customerpath)
69
69
  * [`sk autocomplete [SHELL]`](#sk-autocomplete-shell)
70
70
  * [`sk autocomplete fish`](#sk-autocomplete-fish)
71
+ * [`sk browser clean`](#sk-browser-clean)
72
+ * [`sk browser list`](#sk-browser-list)
73
+ * [`sk browser update`](#sk-browser-update)
71
74
  * [`sk build-parameter-query CUSTOMERPATH`](#sk-build-parameter-query-customerpath)
72
75
  * [`sk build-prewarm-query CUSTOMERPATH`](#sk-build-prewarm-query-customerpath)
73
76
  * [`sk config edit`](#sk-config-edit)
77
+ * [`sk config setup`](#sk-config-setup)
74
78
  * [`sk deploy CUSTOMERPATH`](#sk-deploy-customerpath)
75
79
  * [`sk diff-url-parameters PAGEURL`](#sk-diff-url-parameters-pageurl)
76
80
  * [`sk document-handler CUSTOMERPATH`](#sk-document-handler-customerpath)
@@ -167,6 +171,56 @@ EXAMPLES
167
171
  $ sk autocomplete fish --print > sk.fish
168
172
  ```
169
173
 
174
+ ## `sk browser clean`
175
+
176
+ Remove puppeteer-managed browser builds from the cache
177
+
178
+ ```
179
+ USAGE
180
+ $ sk browser clean [-a]
181
+
182
+ FLAGS
183
+ -a, --all Remove all cached browser builds without prompting
184
+
185
+ DESCRIPTION
186
+ Remove puppeteer-managed browser builds from the cache
187
+
188
+ EXAMPLES
189
+ $ sk browser clean
190
+
191
+ $ sk browser clean --all
192
+ ```
193
+
194
+ ## `sk browser list`
195
+
196
+ List detected system browsers and puppeteer-managed builds
197
+
198
+ ```
199
+ USAGE
200
+ $ sk browser list
201
+
202
+ DESCRIPTION
203
+ List detected system browsers and puppeteer-managed builds
204
+
205
+ EXAMPLES
206
+ $ sk browser list
207
+ ```
208
+
209
+ ## `sk browser update`
210
+
211
+ Update the puppeteer-managed browser to the latest build of its channel
212
+
213
+ ```
214
+ USAGE
215
+ $ sk browser update
216
+
217
+ DESCRIPTION
218
+ Update the puppeteer-managed browser to the latest build of its channel
219
+
220
+ EXAMPLES
221
+ $ sk browser update
222
+ ```
223
+
170
224
  ## `sk build-parameter-query CUSTOMERPATH`
171
225
 
172
226
  Build a parameter query that can be executed in Athena
@@ -230,6 +284,26 @@ EXAMPLES
230
284
  $ sk config edit
231
285
  ```
232
286
 
287
+ ## `sk config setup`
288
+
289
+ Run the Speed Kit CLI setup wizard (choose browser and diff tool)
290
+
291
+ ```
292
+ USAGE
293
+ $ sk config setup [-f]
294
+
295
+ FLAGS
296
+ -f, --force Re-run even if setup was already completed
297
+
298
+ DESCRIPTION
299
+ Run the Speed Kit CLI setup wizard (choose browser and diff tool)
300
+
301
+ EXAMPLES
302
+ $ sk config setup
303
+
304
+ $ sk config setup --force
305
+ ```
306
+
233
307
  ## `sk deploy CUSTOMERPATH`
234
308
 
235
309
  Deploys the specified customer config while comparing to the live version before
@@ -0,0 +1,9 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class BrowserClean extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ all: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,43 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { CliServiceFactory } from "../../services/cli/index.js";
3
+ import { CliConfig } from "../../helpers/cli-config.js";
4
+ import { BrowserManager, } from "../../services/setup/index.js";
5
+ export default class BrowserClean extends Command {
6
+ static description = "Remove puppeteer-managed browser builds from the cache";
7
+ static examples = ["$ sk browser clean", "$ sk browser clean --all"];
8
+ static flags = {
9
+ all: Flags.boolean({
10
+ char: "a",
11
+ description: "Remove all cached browser builds without prompting",
12
+ }),
13
+ };
14
+ async run() {
15
+ const { flags } = await this.parse(BrowserClean);
16
+ const cli = new CliServiceFactory().getService();
17
+ const userConfig = new CliConfig(this.config).load();
18
+ const manager = new BrowserManager(userConfig.tempFolder, cli);
19
+ const installed = manager.list();
20
+ if (installed.length === 0) {
21
+ cli.writeWarning("No puppeteer-managed browsers to clean.");
22
+ return;
23
+ }
24
+ let toRemove;
25
+ if (flags.all) {
26
+ toRemove = installed;
27
+ }
28
+ else {
29
+ toRemove = await cli.multiSelect("Select browser builds to remove", installed.map((browser) => ({
30
+ name: `${browser.browser} ${browser.buildId}`,
31
+ value: browser,
32
+ })));
33
+ }
34
+ if (toRemove.length === 0) {
35
+ cli.comment("Nothing selected.");
36
+ return;
37
+ }
38
+ for (const browser of toRemove) {
39
+ await manager.remove(browser.browser, browser.buildId);
40
+ cli.writeSuccess(`Removed ${browser.browser} ${browser.buildId}`);
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class BrowserList extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,29 @@
1
+ import { Command } from "@oclif/core";
2
+ import { CliServiceFactory } from "../../services/cli/index.js";
3
+ import { CliConfig } from "../../helpers/cli-config.js";
4
+ import { BrowserDetector, normalizePlatform, } from "../../services/setup/index.js";
5
+ export default class BrowserList extends Command {
6
+ static description = "List detected system browsers and puppeteer-managed builds";
7
+ static examples = ["$ sk browser list"];
8
+ async run() {
9
+ const cli = new CliServiceFactory().getService();
10
+ const userConfig = new CliConfig(this.config).load();
11
+ const detector = new BrowserDetector(normalizePlatform(this.config.platform), this.config.home, userConfig.tempFolder);
12
+ const browsers = detector.detectAll();
13
+ if (browsers.length === 0) {
14
+ cli.writeWarning("No browsers detected.");
15
+ return;
16
+ }
17
+ cli.table(browsers.map((browser) => ({
18
+ name: browser.name,
19
+ source: browser.source,
20
+ path: browser.executablePath,
21
+ active: browser.executablePath === userConfig.chromePath ? "✔" : "",
22
+ })), {
23
+ name: { header: "Browser" },
24
+ source: { header: "Source" },
25
+ path: { header: "Path" },
26
+ active: { header: "In use" },
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class BrowserUpdate extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,32 @@
1
+ import { Command } from "@oclif/core";
2
+ import { CliServiceFactory } from "../../services/cli/index.js";
3
+ import { CliConfig } from "../../helpers/cli-config.js";
4
+ import { BrowserManager } from "../../services/setup/index.js";
5
+ export default class BrowserUpdate extends Command {
6
+ static description = "Update the puppeteer-managed browser to the latest build of its channel";
7
+ static examples = ["$ sk browser update"];
8
+ async run() {
9
+ const cli = new CliServiceFactory().getService();
10
+ const cliConfig = new CliConfig(this.config);
11
+ const userConfig = cliConfig.load();
12
+ const channel = userConfig.browserChannel;
13
+ if (channel !== "stable" && channel !== "experimental") {
14
+ cli.writeWarning("No puppeteer-managed browser configured. Run `sk config setup` first.");
15
+ return;
16
+ }
17
+ const manager = new BrowserManager(userConfig.tempFolder, cli);
18
+ const latest = await manager.resolveBuildId(channel);
19
+ if (latest === userConfig.browserBuildId) {
20
+ cli.writeSuccess(`Already on the latest ${channel} build (${latest}).`);
21
+ cliConfig.save({ lastBrowserUpdateCheck: new Date().toISOString() });
22
+ return;
23
+ }
24
+ const installed = await manager.installChannel(channel);
25
+ cliConfig.save({
26
+ chromePath: installed.executablePath,
27
+ browserBuildId: installed.buildId,
28
+ lastBrowserUpdateCheck: new Date().toISOString(),
29
+ });
30
+ cli.writeSuccess(`Updated to Chrome ${channel} ${installed.buildId}.`);
31
+ }
32
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from "@oclif/core";
2
+ export default class Setup extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ postInstall: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,28 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { SetupContext, SetupServiceFactory, } from "../../services/setup/index.js";
3
+ import { isNonInteractive } from "../../helpers/environment.js";
4
+ export default class Setup extends Command {
5
+ static description = "Run the Speed Kit CLI setup wizard (choose browser and diff tool)";
6
+ static examples = ["$ sk config setup", "$ sk config setup --force"];
7
+ static flags = {
8
+ force: Flags.boolean({
9
+ char: "f",
10
+ description: "Re-run even if setup was already completed",
11
+ }),
12
+ postInstall: Flags.boolean({
13
+ hidden: true,
14
+ description: "Internal flag used by the npm postinstall hook",
15
+ }),
16
+ };
17
+ async run() {
18
+ const { flags } = await this.parse(Setup);
19
+ const context = new SetupContext(flags.postInstall, flags.force);
20
+ const service = new SetupServiceFactory(this.config, context).getService();
21
+ // When triggered by postinstall, only run if it is safe and not yet done.
22
+ if (flags.postInstall &&
23
+ (isNonInteractive() || service.isFirstRunCompleted())) {
24
+ return;
25
+ }
26
+ await service.run();
27
+ }
28
+ }
@@ -5,9 +5,16 @@ export interface UserCliConfig {
5
5
  chromeUserProfilePath: string;
6
6
  diffExec: string;
7
7
  diffExecTemplate: string;
8
+ diffTool: string;
8
9
  ideaDirectory: string;
9
10
  slackToken: string;
10
11
  testApp: string;
12
+ browserChannel: string;
13
+ browserBuildId: string;
14
+ browserAutoUpdate: boolean;
15
+ updateReminderDays: number;
16
+ lastBrowserUpdateCheck: string;
17
+ firstRunCompleted: boolean;
11
18
  tempFolder: string;
12
19
  runtimeConfig: Config;
13
20
  nodeModulesPath: string;
@@ -19,9 +26,21 @@ export declare class CliConfig {
19
26
  load(customerPath?: string): UserCliConfig;
20
27
  private createEmptyConfigFile;
21
28
  private createEmptyUserConfig;
29
+ /**
30
+ * Persists the given settings into the user config.json, merging with what
31
+ * is already there. Runtime-only keys are stripped so they never leak into
32
+ * the file. Also reflects the change in the in-memory config so the running
33
+ * command sees it immediately.
34
+ */
35
+ save(partial: Partial<UserCliConfig>): void;
22
36
  private getConfigFilePath;
23
37
  private loadConfigFromConfigFile;
24
38
  private loadConfigFromEnvironment;
25
39
  private resolveExtensionConfigPaths;
26
40
  private readCliConfigFromFile;
41
+ /**
42
+ * Reads and parses config.json without any side effects. Returns null when
43
+ * the file is missing or cannot be parsed.
44
+ */
45
+ private readRawConfigFile;
27
46
  }
@@ -13,6 +13,12 @@ const environmentMapping = {
13
13
  testApp: "TEST_APP",
14
14
  };
15
15
  const CONFIG_FILE_NAME = "config.json";
16
+ // keys that only exist at runtime and must never be written to config.json
17
+ const RUNTIME_ONLY_KEYS = [
18
+ "tempFolder",
19
+ "runtimeConfig",
20
+ "nodeModulesPath",
21
+ ];
16
22
  export class CliConfig {
17
23
  config;
18
24
  userCliConfig = {
@@ -21,9 +27,16 @@ export class CliConfig {
21
27
  chromeUserProfilePath: "",
22
28
  diffExec: "",
23
29
  diffExecTemplate: "$exec $file1 $file2",
30
+ diffTool: "",
24
31
  ideaDirectory: "",
25
32
  slackToken: "",
26
33
  testApp: "",
34
+ browserChannel: "",
35
+ browserBuildId: "",
36
+ browserAutoUpdate: false,
37
+ updateReminderDays: 3,
38
+ lastBrowserUpdateCheck: "",
39
+ firstRunCompleted: false,
27
40
  tempFolder: "/tmp",
28
41
  runtimeConfig: {},
29
42
  };
@@ -51,11 +64,38 @@ export class CliConfig {
51
64
  chromePath: "",
52
65
  chromeUserProfilePath: "",
53
66
  diffExec: "",
67
+ diffExecTemplate: "$exec $file1 $file2",
68
+ diffTool: "",
54
69
  ideaDirectory: "",
55
70
  slackToken: "",
56
71
  testApp: "",
72
+ browserChannel: "",
73
+ browserBuildId: "",
74
+ browserAutoUpdate: false,
75
+ updateReminderDays: 3,
76
+ lastBrowserUpdateCheck: "",
77
+ firstRunCompleted: false,
57
78
  };
58
79
  }
80
+ /**
81
+ * Persists the given settings into the user config.json, merging with what
82
+ * is already there. Runtime-only keys are stripped so they never leak into
83
+ * the file. Also reflects the change in the in-memory config so the running
84
+ * command sees it immediately.
85
+ */
86
+ save(partial) {
87
+ const sanitized = {};
88
+ for (const [key, value] of Object.entries(partial)) {
89
+ if (!RUNTIME_ONLY_KEYS.includes(key)) {
90
+ sanitized[key] = value;
91
+ }
92
+ }
93
+ const existing = this.readRawConfigFile() ?? {};
94
+ const merged = { ...existing, ...sanitized };
95
+ fs.mkdirSync(path.resolve(this.config.configDir), { recursive: true });
96
+ fs.writeFileSync(this.getConfigFilePath(), JSON.stringify(merged, null, `\t`));
97
+ Object.assign(this.userCliConfig, sanitized);
98
+ }
59
99
  getConfigFilePath() {
60
100
  return path.resolve(this.config.configDir, CONFIG_FILE_NAME);
61
101
  }
@@ -65,9 +105,20 @@ export class CliConfig {
65
105
  return;
66
106
  }
67
107
  for (const configKey in this.userCliConfig) {
68
- if (configKey in configJson && configJson[configKey].length > 0) {
69
- this.userCliConfig[configKey] = configJson[configKey];
108
+ if (!(configKey in configJson)) {
109
+ continue;
110
+ }
111
+ const value = configJson[configKey];
112
+ // skip null/undefined and empty strings so file values only override
113
+ // defaults when they carry an actual setting (keeps booleans/numbers,
114
+ // unlike the previous `.length`-only check which dropped them)
115
+ if (value === undefined || value === null) {
116
+ continue;
70
117
  }
118
+ if (typeof value === "string" && value.length === 0) {
119
+ continue;
120
+ }
121
+ this.userCliConfig[configKey] = value;
71
122
  }
72
123
  }
73
124
  loadConfigFromEnvironment() {
@@ -98,7 +149,22 @@ export class CliConfig {
98
149
  this.createEmptyConfigFile();
99
150
  return;
100
151
  }
101
- const configFileContent = fs.readFileSync(this.getConfigFilePath(), "utf8");
102
- return JSON.parse(configFileContent);
152
+ return this.readRawConfigFile();
153
+ }
154
+ /**
155
+ * Reads and parses config.json without any side effects. Returns null when
156
+ * the file is missing or cannot be parsed.
157
+ */
158
+ readRawConfigFile() {
159
+ if (!fs.existsSync(this.getConfigFilePath())) {
160
+ return null;
161
+ }
162
+ try {
163
+ const configFileContent = fs.readFileSync(this.getConfigFilePath(), "utf8");
164
+ return JSON.parse(configFileContent);
165
+ }
166
+ catch {
167
+ return null;
168
+ }
103
169
  }
104
170
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { expect } from "chai";
2
+ import { describe, beforeEach, afterEach, it } from "mocha";
3
+ import { mkdtempSync, rmSync, readFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { CliConfig } from "./cli-config.js";
7
+ function fakeOclifConfig(dir) {
8
+ return {
9
+ configDir: dir,
10
+ cacheDir: dir,
11
+ home: "/home/tester",
12
+ windows: false,
13
+ };
14
+ }
15
+ describe("CliConfig save/load", () => {
16
+ let dir;
17
+ beforeEach(() => {
18
+ dir = mkdtempSync(path.join(tmpdir(), "sk-cli-config-"));
19
+ });
20
+ afterEach(() => {
21
+ rmSync(dir, { recursive: true, force: true });
22
+ });
23
+ it("persists and reloads boolean and number settings with their types", () => {
24
+ const cfg = new CliConfig(fakeOclifConfig(dir));
25
+ cfg.load();
26
+ cfg.save({
27
+ browserAutoUpdate: true,
28
+ updateReminderDays: 7,
29
+ firstRunCompleted: true,
30
+ browserChannel: "stable",
31
+ });
32
+ const reloaded = new CliConfig(fakeOclifConfig(dir)).load();
33
+ expect(reloaded.browserAutoUpdate).to.equal(true);
34
+ expect(reloaded.updateReminderDays).to.equal(7);
35
+ expect(reloaded.firstRunCompleted).to.equal(true);
36
+ expect(reloaded.browserChannel).to.equal("stable");
37
+ });
38
+ it("merges partial saves instead of overwriting the whole file", () => {
39
+ const cfg = new CliConfig(fakeOclifConfig(dir));
40
+ cfg.load();
41
+ cfg.save({ diffTool: "code", diffExec: "/usr/bin/code" });
42
+ cfg.save({ browserChannel: "experimental" });
43
+ const reloaded = new CliConfig(fakeOclifConfig(dir)).load();
44
+ expect(reloaded.diffTool).to.equal("code");
45
+ expect(reloaded.diffExec).to.equal("/usr/bin/code");
46
+ expect(reloaded.browserChannel).to.equal("experimental");
47
+ });
48
+ it("never writes runtime-only keys to config.json", () => {
49
+ const cfg = new CliConfig(fakeOclifConfig(dir));
50
+ cfg.load();
51
+ cfg.save({
52
+ firstRunCompleted: true,
53
+ // runtime-only keys must be stripped
54
+ tempFolder: "/should/not/be/persisted",
55
+ nodeModulesPath: "/should/not/be/persisted",
56
+ });
57
+ const raw = JSON.parse(readFileSync(path.join(dir, "config.json"), "utf8"));
58
+ expect(raw).to.not.have.property("tempFolder");
59
+ expect(raw).to.not.have.property("nodeModulesPath");
60
+ expect(raw).to.not.have.property("runtimeConfig");
61
+ expect(raw.firstRunCompleted).to.equal(true);
62
+ });
63
+ it("keeps defaults for a false boolean stored in the file", () => {
64
+ const cfg = new CliConfig(fakeOclifConfig(dir));
65
+ cfg.load();
66
+ cfg.save({ browserAutoUpdate: false });
67
+ const reloaded = new CliConfig(fakeOclifConfig(dir)).load();
68
+ expect(reloaded.browserAutoUpdate).to.equal(false);
69
+ });
70
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Environment detection helpers used to decide whether interactive flows
3
+ * (e.g. the first-run wizard) may prompt the user.
4
+ */
5
+ /**
6
+ * Detects common CI environments. Most CI providers set `CI=true`, and several
7
+ * set their own dedicated variable, so we check both.
8
+ */
9
+ export declare function isCI(env?: NodeJS.ProcessEnv): boolean;
10
+ /**
11
+ * True when we cannot safely prompt the user: running in CI, without an
12
+ * attached TTY (e.g. piped output, npm background install), or when the user
13
+ * explicitly opted out via SK_NON_INTERACTIVE.
14
+ */
15
+ export declare function isNonInteractive(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Environment detection helpers used to decide whether interactive flows
3
+ * (e.g. the first-run wizard) may prompt the user.
4
+ */
5
+ /**
6
+ * Detects common CI environments. Most CI providers set `CI=true`, and several
7
+ * set their own dedicated variable, so we check both.
8
+ */
9
+ export function isCI(env = process.env) {
10
+ if (env.CI !== undefined && env.CI !== "false" && env.CI !== "0") {
11
+ return true;
12
+ }
13
+ const ciMarkers = [
14
+ "CONTINUOUS_INTEGRATION",
15
+ "BUILD_NUMBER",
16
+ "GITLAB_CI",
17
+ "GITHUB_ACTIONS",
18
+ "JENKINS_URL",
19
+ "TEAMCITY_VERSION",
20
+ "TF_BUILD",
21
+ "BITBUCKET_BUILD_NUMBER",
22
+ "CIRCLECI",
23
+ "TRAVIS",
24
+ ];
25
+ return ciMarkers.some((marker) => Boolean(env[marker]));
26
+ }
27
+ /**
28
+ * True when we cannot safely prompt the user: running in CI, without an
29
+ * attached TTY (e.g. piped output, npm background install), or when the user
30
+ * explicitly opted out via SK_NON_INTERACTIVE.
31
+ */
32
+ export function isNonInteractive(env = process.env) {
33
+ if (env.SK_NON_INTERACTIVE !== undefined && env.SK_NON_INTERACTIVE !== "") {
34
+ return true;
35
+ }
36
+ if (isCI(env)) {
37
+ return true;
38
+ }
39
+ return !process.stdin.isTTY || !process.stdout.isTTY;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { expect } from "chai";
2
+ import { describe, it } from "mocha";
3
+ import { isCI, isNonInteractive } from "./environment.js";
4
+ describe("environment.isCI", () => {
5
+ it("is false for an empty environment", () => {
6
+ expect(isCI({})).to.equal(false);
7
+ });
8
+ it("is true when CI is set to a truthy value", () => {
9
+ expect(isCI({ CI: "true" })).to.equal(true);
10
+ expect(isCI({ CI: "1" })).to.equal(true);
11
+ });
12
+ it("treats CI=false / CI=0 as not CI", () => {
13
+ expect(isCI({ CI: "false" })).to.equal(false);
14
+ expect(isCI({ CI: "0" })).to.equal(false);
15
+ });
16
+ it("detects provider-specific markers", () => {
17
+ expect(isCI({ GITLAB_CI: "true" })).to.equal(true);
18
+ expect(isCI({ GITHUB_ACTIONS: "true" })).to.equal(true);
19
+ });
20
+ });
21
+ describe("environment.isNonInteractive", () => {
22
+ it("is true when SK_NON_INTERACTIVE is set", () => {
23
+ expect(isNonInteractive({ SK_NON_INTERACTIVE: "1" })).to.equal(true);
24
+ });
25
+ it("is true when running in CI", () => {
26
+ expect(isNonInteractive({ CI: "true" })).to.equal(true);
27
+ });
28
+ });
@@ -0,0 +1,8 @@
1
+ import { Hook } from "@oclif/core";
2
+ /**
3
+ * Runs before every command. When interactive it either nudges the user to run
4
+ * the setup wizard (first time) or performs the periodic browser update check.
5
+ * Any failure is swallowed so CLI housekeeping never blocks a real command.
6
+ */
7
+ declare const hook: Hook<"init">;
8
+ export default hook;
@@ -0,0 +1,36 @@
1
+ import { CliConfig } from "../../helpers/cli-config.js";
2
+ import { SetupServiceFactory } from "../../services/setup/index.js";
3
+ import { isNonInteractive } from "../../helpers/environment.js";
4
+ // commands the hook must never interfere with (setup itself, help, plugins)
5
+ const IGNORED_PREFIXES = ["config:setup", "help", "autocomplete", "plugins"];
6
+ /**
7
+ * Runs before every command. When interactive it either nudges the user to run
8
+ * the setup wizard (first time) or performs the periodic browser update check.
9
+ * Any failure is swallowed so CLI housekeeping never blocks a real command.
10
+ */
11
+ const hook = async function (opts) {
12
+ try {
13
+ if (isNonInteractive()) {
14
+ return;
15
+ }
16
+ const id = opts.id ?? "";
17
+ if (id === "" || IGNORED_PREFIXES.some((prefix) => id.startsWith(prefix))) {
18
+ return;
19
+ }
20
+ // opts.config is the runtime Config instance, typed here as the interface
21
+ const config = opts.config;
22
+ const cliConfig = new CliConfig(config);
23
+ const userConfig = cliConfig.load();
24
+ if (!userConfig.firstRunCompleted) {
25
+ this.log(`${opts.config.bin}: first-time setup has not run yet. ` +
26
+ `Run \`${opts.config.bin} config setup\` to choose a browser and diff tool.`);
27
+ return;
28
+ }
29
+ const service = new SetupServiceFactory(config).getService();
30
+ await service.remindBrowserUpdate();
31
+ }
32
+ catch {
33
+ // never block a command because of setup housekeeping
34
+ }
35
+ };
36
+ export default hook;