@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.
- package/CHANGELOG.md +15 -0
- package/README.md +75 -1
- package/dist/commands/browser/clean.d.ts +9 -0
- package/dist/commands/browser/clean.js +43 -0
- package/dist/commands/browser/list.d.ts +6 -0
- package/dist/commands/browser/list.js +29 -0
- package/dist/commands/browser/update.d.ts +6 -0
- package/dist/commands/browser/update.js +32 -0
- package/dist/commands/config/setup.d.ts +10 -0
- package/dist/commands/config/setup.js +28 -0
- package/dist/helpers/cli-config.d.ts +19 -0
- package/dist/helpers/cli-config.js +70 -4
- package/dist/helpers/cli-config.spec.d.ts +1 -0
- package/dist/helpers/cli-config.spec.js +70 -0
- package/dist/helpers/environment.d.ts +15 -0
- package/dist/helpers/environment.js +40 -0
- package/dist/helpers/environment.spec.d.ts +1 -0
- package/dist/helpers/environment.spec.js +28 -0
- package/dist/hooks/init/first-run.d.ts +8 -0
- package/dist/hooks/init/first-run.js +36 -0
- package/dist/services/customer-config/templates/config_SpeedKit.js.hbs +11 -0
- package/dist/services/setup/browser/browser-detector.d.ts +27 -0
- package/dist/services/setup/browser/browser-detector.js +47 -0
- package/dist/services/setup/browser/browser-manager.d.ts +43 -0
- package/dist/services/setup/browser/browser-manager.js +101 -0
- package/dist/services/setup/browser/browser-setup-service.d.ts +28 -0
- package/dist/services/setup/browser/browser-setup-service.js +136 -0
- package/dist/services/setup/difftool/difftool-detector.d.ts +14 -0
- package/dist/services/setup/difftool/difftool-detector.js +27 -0
- package/dist/services/setup/difftool/difftool-setup-service.d.ts +15 -0
- package/dist/services/setup/difftool/difftool-setup-service.js +46 -0
- package/dist/services/setup/index.d.ts +10 -0
- package/dist/services/setup/index.js +10 -0
- package/dist/services/setup/os/detector-helper.d.ts +29 -0
- package/dist/services/setup/os/detector-helper.js +65 -0
- package/dist/services/setup/os/tool-registry.d.ts +38 -0
- package/dist/services/setup/os/tool-registry.js +194 -0
- package/dist/services/setup/os/tool-registry.spec.d.ts +1 -0
- package/dist/services/setup/os/tool-registry.spec.js +26 -0
- package/dist/services/setup/setup-context.d.ts +14 -0
- package/dist/services/setup/setup-context.js +15 -0
- package/dist/services/setup/setup-service-factory.d.ts +10 -0
- package/dist/services/setup/setup-service-factory.js +37 -0
- package/dist/services/setup/setup-service.d.ts +20 -0
- package/dist/services/setup/setup-service.js +37 -0
- package/oclif.manifest.json +122 -1
- 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.
|
|
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,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,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
|
|
69
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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;
|