@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
|
@@ -137,6 +137,17 @@ import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
|
|
|
137
137
|
split: CURRENT_HOST_CONFIG.split,
|
|
138
138
|
splitTestId: CURRENT_HOST_CONFIG.splitTestId,
|
|
139
139
|
disabled: !CURRENT_HOST_CONFIG.enabled,
|
|
140
|
+
// TODO: walk the live checkout + login flows and pin exact prefixes (SSO/PSP returns, separate reg pages). See /disabled-scopes.
|
|
141
|
+
disabledScopes: [
|
|
142
|
+
"/cart",
|
|
143
|
+
"/checkout",
|
|
144
|
+
{{#if isShopify}}
|
|
145
|
+
"/account", // login/register/recover/orders
|
|
146
|
+
"/services/login_with_shop", // Shop Pay auto-login transfer (platform route, all locales)
|
|
147
|
+
{{else}}
|
|
148
|
+
"/login",
|
|
149
|
+
{{/if}}
|
|
150
|
+
],
|
|
140
151
|
enabledSites: [
|
|
141
152
|
{
|
|
142
153
|
pathname: [
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Platform } from "../os/tool-registry.js";
|
|
2
|
+
export type BrowserSource = "system" | "puppeteer";
|
|
3
|
+
export interface BrowserOption {
|
|
4
|
+
/** stable id used as the select value */
|
|
5
|
+
id: string;
|
|
6
|
+
/** display name incl. version/build when known */
|
|
7
|
+
name: string;
|
|
8
|
+
executablePath: string;
|
|
9
|
+
source: BrowserSource;
|
|
10
|
+
/** puppeteer buildId, only for puppeteer-managed browsers */
|
|
11
|
+
buildId?: string;
|
|
12
|
+
/** puppeteer Browser id (e.g. "chrome"), only for puppeteer-managed */
|
|
13
|
+
browser?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare class BrowserDetector {
|
|
16
|
+
private readonly platform;
|
|
17
|
+
private readonly home;
|
|
18
|
+
private readonly cachePath;
|
|
19
|
+
private readonly env;
|
|
20
|
+
constructor(platform: Platform, home: string, cachePath: string, env?: NodeJS.ProcessEnv);
|
|
21
|
+
/** Browsers installed system-wide, discovered via the OS tool registry. */
|
|
22
|
+
detectSystemBrowsers(): BrowserOption[];
|
|
23
|
+
/** Browsers previously downloaded into the puppeteer cache. */
|
|
24
|
+
detectPuppeteerBrowsers(): BrowserOption[];
|
|
25
|
+
/** All detected browsers, puppeteer-managed first then system. */
|
|
26
|
+
detectAll(): BrowserOption[];
|
|
27
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Cache } from "@puppeteer/browsers";
|
|
2
|
+
import { BROWSERS } from "../os/tool-registry.js";
|
|
3
|
+
import { detectTools } from "../os/detector-helper.js";
|
|
4
|
+
export class BrowserDetector {
|
|
5
|
+
platform;
|
|
6
|
+
home;
|
|
7
|
+
cachePath;
|
|
8
|
+
env;
|
|
9
|
+
constructor(platform, home, cachePath, env = process.env) {
|
|
10
|
+
this.platform = platform;
|
|
11
|
+
this.home = home;
|
|
12
|
+
this.cachePath = cachePath;
|
|
13
|
+
this.env = env;
|
|
14
|
+
}
|
|
15
|
+
/** Browsers installed system-wide, discovered via the OS tool registry. */
|
|
16
|
+
detectSystemBrowsers() {
|
|
17
|
+
return detectTools(BROWSERS, this.platform, this.home, this.env).map((tool) => ({
|
|
18
|
+
id: `system:${tool.id}`,
|
|
19
|
+
name: tool.name,
|
|
20
|
+
executablePath: tool.executablePath,
|
|
21
|
+
source: "system",
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
/** Browsers previously downloaded into the puppeteer cache. */
|
|
25
|
+
detectPuppeteerBrowsers() {
|
|
26
|
+
const cache = new Cache(this.cachePath);
|
|
27
|
+
let installed;
|
|
28
|
+
try {
|
|
29
|
+
installed = cache.getInstalledBrowsers();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return installed.map((browser) => ({
|
|
35
|
+
id: `puppeteer:${browser.browser}@${browser.buildId}`,
|
|
36
|
+
name: `${browser.browser} ${browser.buildId} (puppeteer)`,
|
|
37
|
+
executablePath: browser.executablePath,
|
|
38
|
+
source: "puppeteer",
|
|
39
|
+
buildId: browser.buildId,
|
|
40
|
+
browser: browser.browser,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
/** All detected browsers, puppeteer-managed first then system. */
|
|
44
|
+
detectAll() {
|
|
45
|
+
return [...this.detectPuppeteerBrowsers(), ...this.detectSystemBrowsers()];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { InstalledBrowser } from "@puppeteer/browsers";
|
|
2
|
+
import { CliServiceInterface } from "../../cli/index.js";
|
|
3
|
+
export type BrowserChannel = "stable" | "experimental";
|
|
4
|
+
export interface InstalledBrowserInfo {
|
|
5
|
+
browser: string;
|
|
6
|
+
buildId: string;
|
|
7
|
+
executablePath: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thin wrapper around `@puppeteer/browsers` for installing, listing and
|
|
11
|
+
* removing Chrome builds. Kept UI-agnostic apart from an optional progress
|
|
12
|
+
* spinner, so it can be reused by the wizard and the `sk browser` commands.
|
|
13
|
+
*/
|
|
14
|
+
export declare class BrowserManager {
|
|
15
|
+
private readonly cacheDir;
|
|
16
|
+
private readonly cli?;
|
|
17
|
+
constructor(cacheDir: string, cli?: CliServiceInterface);
|
|
18
|
+
/**
|
|
19
|
+
* Resolves the concrete buildId for a channel (e.g. "stable" -> "139.0...").
|
|
20
|
+
*/
|
|
21
|
+
resolveBuildId(channel: BrowserChannel): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Installs Chrome for the given channel and returns the installed build.
|
|
24
|
+
*/
|
|
25
|
+
installChannel(channel: BrowserChannel): Promise<InstalledBrowser>;
|
|
26
|
+
/**
|
|
27
|
+
* Installs a specific Chrome buildId and returns the installed build.
|
|
28
|
+
*/
|
|
29
|
+
installBuildId(buildId: string): Promise<InstalledBrowser>;
|
|
30
|
+
/**
|
|
31
|
+
* Lists all browsers currently present in the puppeteer cache.
|
|
32
|
+
*/
|
|
33
|
+
list(): InstalledBrowserInfo[];
|
|
34
|
+
/**
|
|
35
|
+
* Removes a single browser build from the puppeteer cache.
|
|
36
|
+
*/
|
|
37
|
+
remove(browser: string, buildId: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Removes all browser builds from the puppeteer cache.
|
|
40
|
+
*/
|
|
41
|
+
removeAll(): Promise<void>;
|
|
42
|
+
private getPlatform;
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Browser, detectBrowserPlatform, install, resolveBuildId, uninstall, Cache, } from "@puppeteer/browsers";
|
|
2
|
+
/**
|
|
3
|
+
* Maps the wizard's user-facing channel to a puppeteer build tag.
|
|
4
|
+
* "experimental" follows Chrome Canary; "stable" follows the stable channel.
|
|
5
|
+
*/
|
|
6
|
+
function channelToTag(channel) {
|
|
7
|
+
return channel === "experimental" ? "canary" : "stable";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thin wrapper around `@puppeteer/browsers` for installing, listing and
|
|
11
|
+
* removing Chrome builds. Kept UI-agnostic apart from an optional progress
|
|
12
|
+
* spinner, so it can be reused by the wizard and the `sk browser` commands.
|
|
13
|
+
*/
|
|
14
|
+
export class BrowserManager {
|
|
15
|
+
cacheDir;
|
|
16
|
+
cli;
|
|
17
|
+
constructor(cacheDir, cli) {
|
|
18
|
+
this.cacheDir = cacheDir;
|
|
19
|
+
this.cli = cli;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolves the concrete buildId for a channel (e.g. "stable" -> "139.0...").
|
|
23
|
+
*/
|
|
24
|
+
async resolveBuildId(channel) {
|
|
25
|
+
const platform = this.getPlatform();
|
|
26
|
+
return resolveBuildId(Browser.CHROME, platform, channelToTag(channel));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Installs Chrome for the given channel and returns the installed build.
|
|
30
|
+
*/
|
|
31
|
+
async installChannel(channel) {
|
|
32
|
+
const buildId = await this.resolveBuildId(channel);
|
|
33
|
+
return this.installBuildId(buildId);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Installs a specific Chrome buildId and returns the installed build.
|
|
37
|
+
*/
|
|
38
|
+
async installBuildId(buildId) {
|
|
39
|
+
const platform = this.getPlatform();
|
|
40
|
+
const actionName = "browser-install";
|
|
41
|
+
this.cli?.startAction(actionName, `Downloading Chrome ${buildId}…`);
|
|
42
|
+
try {
|
|
43
|
+
const installed = await install({
|
|
44
|
+
browser: Browser.CHROME,
|
|
45
|
+
buildId,
|
|
46
|
+
cacheDir: this.cacheDir,
|
|
47
|
+
platform,
|
|
48
|
+
});
|
|
49
|
+
this.cli?.successAction(actionName, `Installed Chrome ${buildId}`);
|
|
50
|
+
return installed;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
this.cli?.failAction(actionName, `Failed to install Chrome ${buildId}: ${error?.message ?? error}`);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Lists all browsers currently present in the puppeteer cache.
|
|
59
|
+
*/
|
|
60
|
+
list() {
|
|
61
|
+
const cache = new Cache(this.cacheDir);
|
|
62
|
+
let installed;
|
|
63
|
+
try {
|
|
64
|
+
installed = cache.getInstalledBrowsers();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
return installed.map((browser) => ({
|
|
70
|
+
browser: browser.browser,
|
|
71
|
+
buildId: browser.buildId,
|
|
72
|
+
executablePath: browser.executablePath,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Removes a single browser build from the puppeteer cache.
|
|
77
|
+
*/
|
|
78
|
+
async remove(browser, buildId) {
|
|
79
|
+
await uninstall({
|
|
80
|
+
browser: browser,
|
|
81
|
+
buildId,
|
|
82
|
+
cacheDir: this.cacheDir,
|
|
83
|
+
platform: this.getPlatform(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Removes all browser builds from the puppeteer cache.
|
|
88
|
+
*/
|
|
89
|
+
async removeAll() {
|
|
90
|
+
for (const installed of this.list()) {
|
|
91
|
+
await this.remove(installed.browser, installed.buildId);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
getPlatform() {
|
|
95
|
+
const platform = detectBrowserPlatform();
|
|
96
|
+
if (!platform) {
|
|
97
|
+
throw new Error("Unable to detect browser platform for this OS");
|
|
98
|
+
}
|
|
99
|
+
return platform;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CliServiceInterface } from "../../cli/index.js";
|
|
2
|
+
import { CliConfig, UserCliConfig } from "../../../helpers/cli-config.js";
|
|
3
|
+
import { BrowserDetector } from "./browser-detector.js";
|
|
4
|
+
import { BrowserManager } from "./browser-manager.js";
|
|
5
|
+
export declare class BrowserSetupService {
|
|
6
|
+
private readonly cli;
|
|
7
|
+
private readonly cliConfig;
|
|
8
|
+
private readonly userConfig;
|
|
9
|
+
private readonly detector;
|
|
10
|
+
private readonly manager;
|
|
11
|
+
constructor(cli: CliServiceInterface, cliConfig: CliConfig, userConfig: UserCliConfig, detector: BrowserDetector, manager: BrowserManager);
|
|
12
|
+
/**
|
|
13
|
+
* Interactive browser selection: pick a detected browser or download one via
|
|
14
|
+
* puppeteer, then persist the choice.
|
|
15
|
+
*/
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Checks — at most once per reminder interval — whether the puppeteer-managed
|
|
19
|
+
* browser has a newer build and offers to update it. Only applies to browsers
|
|
20
|
+
* we installed (i.e. a channel is configured). Safe to call on every command;
|
|
21
|
+
* callers must ensure the session is interactive first.
|
|
22
|
+
*/
|
|
23
|
+
remindIfDue(): Promise<void>;
|
|
24
|
+
private installAndPersist;
|
|
25
|
+
private askReminderDays;
|
|
26
|
+
private isReminderDue;
|
|
27
|
+
private nowIso;
|
|
28
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
2
|
+
export class BrowserSetupService {
|
|
3
|
+
cli;
|
|
4
|
+
cliConfig;
|
|
5
|
+
userConfig;
|
|
6
|
+
detector;
|
|
7
|
+
manager;
|
|
8
|
+
constructor(cli, cliConfig, userConfig, detector, manager) {
|
|
9
|
+
this.cli = cli;
|
|
10
|
+
this.cliConfig = cliConfig;
|
|
11
|
+
this.userConfig = userConfig;
|
|
12
|
+
this.detector = detector;
|
|
13
|
+
this.manager = manager;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Interactive browser selection: pick a detected browser or download one via
|
|
17
|
+
* puppeteer, then persist the choice.
|
|
18
|
+
*/
|
|
19
|
+
async run() {
|
|
20
|
+
const detected = this.detector.detectAll();
|
|
21
|
+
const choices = [
|
|
22
|
+
...detected.map((browser) => ({
|
|
23
|
+
name: `${browser.name} ${this.cli.style.grey(browser.executablePath)}`,
|
|
24
|
+
value: browser.id,
|
|
25
|
+
})),
|
|
26
|
+
{
|
|
27
|
+
name: "Download Chrome (stable) via puppeteer",
|
|
28
|
+
value: "install:stable",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "Download Chrome (experimental / canary) via puppeteer",
|
|
32
|
+
value: "install:experimental",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const currentDefault = detected.find((browser) => browser.executablePath === this.userConfig.chromePath)?.id;
|
|
36
|
+
const selected = await this.cli.select("Which browser should Speed Kit use?", choices, currentDefault);
|
|
37
|
+
if (selected.startsWith("install:")) {
|
|
38
|
+
const channel = selected.slice("install:".length);
|
|
39
|
+
await this.installAndPersist(channel);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const chosen = detected.find((browser) => browser.id === selected);
|
|
43
|
+
if (!chosen) {
|
|
44
|
+
this.cli.writeError("Selected browser could not be resolved.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
this.cliConfig.save({
|
|
48
|
+
chromePath: chosen.executablePath,
|
|
49
|
+
// a manually selected browser is not update-managed by the wizard, so
|
|
50
|
+
// clear the channel to disable the update reminder for it
|
|
51
|
+
browserChannel: "",
|
|
52
|
+
browserBuildId: chosen.buildId ?? "",
|
|
53
|
+
});
|
|
54
|
+
this.cli.writeSuccess(`Using ${chosen.name} (${chosen.executablePath}).`);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Checks — at most once per reminder interval — whether the puppeteer-managed
|
|
58
|
+
* browser has a newer build and offers to update it. Only applies to browsers
|
|
59
|
+
* we installed (i.e. a channel is configured). Safe to call on every command;
|
|
60
|
+
* callers must ensure the session is interactive first.
|
|
61
|
+
*/
|
|
62
|
+
async remindIfDue() {
|
|
63
|
+
const channel = this.userConfig.browserChannel;
|
|
64
|
+
if (channel !== "stable" && channel !== "experimental") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!this.isReminderDue()) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let latestBuildId;
|
|
71
|
+
try {
|
|
72
|
+
latestBuildId = await this.manager.resolveBuildId(channel);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// offline / resolution failure: try again next interval
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// stamp the check regardless so we don't re-check until the next interval
|
|
79
|
+
this.cliConfig.save({ lastBrowserUpdateCheck: this.nowIso() });
|
|
80
|
+
if (latestBuildId === this.userConfig.browserBuildId) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (this.userConfig.browserAutoUpdate) {
|
|
84
|
+
await this.installAndPersist(channel);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const update = await this.cli.confirm(`A newer Chrome ${channel} build (${latestBuildId}) is available. Update now?`, true);
|
|
88
|
+
if (update) {
|
|
89
|
+
await this.installAndPersist(channel);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await this.askReminderDays();
|
|
93
|
+
}
|
|
94
|
+
async installAndPersist(channel) {
|
|
95
|
+
const installed = await this.manager.installChannel(channel);
|
|
96
|
+
this.cliConfig.save({
|
|
97
|
+
chromePath: installed.executablePath,
|
|
98
|
+
browserChannel: channel,
|
|
99
|
+
browserBuildId: installed.buildId,
|
|
100
|
+
lastBrowserUpdateCheck: this.nowIso(),
|
|
101
|
+
});
|
|
102
|
+
const autoUpdate = await this.cli.confirm("Automatically update the browser when a newer build is available?", this.userConfig.browserAutoUpdate);
|
|
103
|
+
this.cliConfig.save({ browserAutoUpdate: autoUpdate });
|
|
104
|
+
if (!autoUpdate) {
|
|
105
|
+
await this.askReminderDays();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async askReminderDays() {
|
|
109
|
+
const answer = await this.cli.prompt("Remind me to check for browser updates in how many days?", {
|
|
110
|
+
defaultAnswer: String(this.userConfig.updateReminderDays || 3),
|
|
111
|
+
validator: (value) => /^\d+$/.test(value.trim()) ? "" : "Enter a whole number of days",
|
|
112
|
+
});
|
|
113
|
+
const days = Number.parseInt(answer, 10);
|
|
114
|
+
if (!Number.isNaN(days) && days > 0) {
|
|
115
|
+
this.cliConfig.save({
|
|
116
|
+
updateReminderDays: days,
|
|
117
|
+
lastBrowserUpdateCheck: this.nowIso(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
isReminderDue() {
|
|
122
|
+
const days = this.userConfig.updateReminderDays || 3;
|
|
123
|
+
const last = this.userConfig.lastBrowserUpdateCheck;
|
|
124
|
+
if (!last) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
const lastMs = new Date(last).getTime();
|
|
128
|
+
if (Number.isNaN(lastMs)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return (Date.now() - lastMs) / MS_PER_DAY >= days;
|
|
132
|
+
}
|
|
133
|
+
nowIso() {
|
|
134
|
+
return new Date().toISOString();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Platform } from "../os/tool-registry.js";
|
|
2
|
+
export interface DetectedEditor {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
executablePath: string;
|
|
6
|
+
diffExecTemplate: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class DiffToolDetector {
|
|
9
|
+
private readonly platform;
|
|
10
|
+
private readonly home;
|
|
11
|
+
private readonly env;
|
|
12
|
+
constructor(platform: Platform, home: string, env?: NodeJS.ProcessEnv);
|
|
13
|
+
detect(): DetectedEditor[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EDITORS } from "../os/tool-registry.js";
|
|
2
|
+
import { detectTool } from "../os/detector-helper.js";
|
|
3
|
+
export class DiffToolDetector {
|
|
4
|
+
platform;
|
|
5
|
+
home;
|
|
6
|
+
env;
|
|
7
|
+
constructor(platform, home, env = process.env) {
|
|
8
|
+
this.platform = platform;
|
|
9
|
+
this.home = home;
|
|
10
|
+
this.env = env;
|
|
11
|
+
}
|
|
12
|
+
detect() {
|
|
13
|
+
const detected = [];
|
|
14
|
+
for (const editor of EDITORS) {
|
|
15
|
+
const result = detectTool(editor, this.platform, this.home, this.env);
|
|
16
|
+
if (result) {
|
|
17
|
+
detected.push({
|
|
18
|
+
id: editor.id,
|
|
19
|
+
name: editor.name,
|
|
20
|
+
executablePath: result.executablePath,
|
|
21
|
+
diffExecTemplate: editor.diffExecTemplate,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return detected;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CliServiceInterface } from "../../cli/index.js";
|
|
2
|
+
import { CliConfig, UserCliConfig } from "../../../helpers/cli-config.js";
|
|
3
|
+
import { DiffToolDetector } from "./difftool-detector.js";
|
|
4
|
+
export declare class DiffToolSetupService {
|
|
5
|
+
private readonly cli;
|
|
6
|
+
private readonly cliConfig;
|
|
7
|
+
private readonly userConfig;
|
|
8
|
+
private readonly detector;
|
|
9
|
+
constructor(cli: CliServiceInterface, cliConfig: CliConfig, userConfig: UserCliConfig, detector: DiffToolDetector);
|
|
10
|
+
/**
|
|
11
|
+
* Detects installed editors/diff tools, lets the user pick one, and persists
|
|
12
|
+
* it as diffExec + diffExecTemplate + diffTool.
|
|
13
|
+
*/
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class DiffToolSetupService {
|
|
2
|
+
cli;
|
|
3
|
+
cliConfig;
|
|
4
|
+
userConfig;
|
|
5
|
+
detector;
|
|
6
|
+
constructor(cli, cliConfig, userConfig, detector) {
|
|
7
|
+
this.cli = cli;
|
|
8
|
+
this.cliConfig = cliConfig;
|
|
9
|
+
this.userConfig = userConfig;
|
|
10
|
+
this.detector = detector;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Detects installed editors/diff tools, lets the user pick one, and persists
|
|
14
|
+
* it as diffExec + diffExecTemplate + diffTool.
|
|
15
|
+
*/
|
|
16
|
+
async run() {
|
|
17
|
+
const detected = this.detector.detect();
|
|
18
|
+
if (detected.length === 0) {
|
|
19
|
+
this.cli.writeWarning("No supported diff tool (IntelliJ / VS Code family) was found. " +
|
|
20
|
+
"You can set diffExec manually in the config later.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const choices = [
|
|
24
|
+
...detected.map((editor) => ({
|
|
25
|
+
name: `${editor.name} ${this.cli.style.grey(editor.executablePath)}`,
|
|
26
|
+
value: editor.id,
|
|
27
|
+
})),
|
|
28
|
+
{ name: "Skip — configure manually later", value: "skip" },
|
|
29
|
+
];
|
|
30
|
+
const selected = await this.cli.select("Which diff tool should Speed Kit use?", choices, this.userConfig.diffTool || undefined);
|
|
31
|
+
if (selected === "skip") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const chosen = detected.find((editor) => editor.id === selected);
|
|
35
|
+
if (!chosen) {
|
|
36
|
+
this.cli.writeError("Selected diff tool could not be resolved.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.cliConfig.save({
|
|
40
|
+
diffTool: chosen.id,
|
|
41
|
+
diffExec: chosen.executablePath,
|
|
42
|
+
diffExecTemplate: chosen.diffExecTemplate,
|
|
43
|
+
});
|
|
44
|
+
this.cli.writeSuccess(`Using ${chosen.name} for diffs.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./setup-context.js";
|
|
2
|
+
export * from "./setup-service.js";
|
|
3
|
+
export * from "./setup-service-factory.js";
|
|
4
|
+
export * from "./browser/browser-detector.js";
|
|
5
|
+
export * from "./browser/browser-manager.js";
|
|
6
|
+
export * from "./browser/browser-setup-service.js";
|
|
7
|
+
export * from "./difftool/difftool-detector.js";
|
|
8
|
+
export * from "./difftool/difftool-setup-service.js";
|
|
9
|
+
export * from "./os/tool-registry.js";
|
|
10
|
+
export * from "./os/detector-helper.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./setup-context.js";
|
|
2
|
+
export * from "./setup-service.js";
|
|
3
|
+
export * from "./setup-service-factory.js";
|
|
4
|
+
export * from "./browser/browser-detector.js";
|
|
5
|
+
export * from "./browser/browser-manager.js";
|
|
6
|
+
export * from "./browser/browser-setup-service.js";
|
|
7
|
+
export * from "./difftool/difftool-detector.js";
|
|
8
|
+
export * from "./difftool/difftool-setup-service.js";
|
|
9
|
+
export * from "./os/tool-registry.js";
|
|
10
|
+
export * from "./os/detector-helper.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { DetectableTool, Platform } from "./tool-registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Result of locating a tool on the current system.
|
|
4
|
+
*/
|
|
5
|
+
export interface DetectedTool {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
/** absolute path to the executable/launcher that was found */
|
|
9
|
+
executablePath: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a launcher command on PATH, returning its absolute path or null.
|
|
13
|
+
* Uses `where` on Windows and `which` elsewhere.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveOnPath(command: string, platform: Platform): string | null;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the first candidate path that exists on disk, after expanding
|
|
18
|
+
* `~`/`%VAR%` tokens, or null when none exist.
|
|
19
|
+
*/
|
|
20
|
+
export declare function findExistingCandidatePath(candidates: string[], home: string, env?: NodeJS.ProcessEnv): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Detects a single tool: first via its known candidate paths (preferred, as it
|
|
23
|
+
* yields an absolute executable), then falling back to a PATH lookup.
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectTool(tool: DetectableTool, platform: Platform, home: string, env?: NodeJS.ProcessEnv): DetectedTool | null;
|
|
26
|
+
/**
|
|
27
|
+
* Detects every tool in the list, dropping the ones that are not installed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectTools(tools: DetectableTool[], platform: Platform, home: string, env?: NodeJS.ProcessEnv): DetectedTool[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { expandCandidatePath, } from "./tool-registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a launcher command on PATH, returning its absolute path or null.
|
|
6
|
+
* Uses `where` on Windows and `which` elsewhere.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveOnPath(command, platform) {
|
|
9
|
+
const finder = platform === "win32" ? "where" : "which";
|
|
10
|
+
try {
|
|
11
|
+
const output = execFileSync(finder, [command], {
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
})
|
|
14
|
+
.toString()
|
|
15
|
+
.trim();
|
|
16
|
+
// `where` may return multiple lines; take the first hit
|
|
17
|
+
const first = output.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
18
|
+
if (first && existsSync(first.trim())) {
|
|
19
|
+
return first.trim();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// not found on PATH
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns the first candidate path that exists on disk, after expanding
|
|
29
|
+
* `~`/`%VAR%` tokens, or null when none exist.
|
|
30
|
+
*/
|
|
31
|
+
export function findExistingCandidatePath(candidates, home, env = process.env) {
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
const expanded = expandCandidatePath(candidate, home, env);
|
|
34
|
+
if (existsSync(expanded)) {
|
|
35
|
+
return expanded;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detects a single tool: first via its known candidate paths (preferred, as it
|
|
42
|
+
* yields an absolute executable), then falling back to a PATH lookup.
|
|
43
|
+
*/
|
|
44
|
+
export function detectTool(tool, platform, home, env = process.env) {
|
|
45
|
+
const candidatePaths = tool.paths[platform] ?? [];
|
|
46
|
+
const fromPath = findExistingCandidatePath(candidatePaths, home, env);
|
|
47
|
+
if (fromPath) {
|
|
48
|
+
return { id: tool.id, name: tool.name, executablePath: fromPath };
|
|
49
|
+
}
|
|
50
|
+
for (const command of tool.commands) {
|
|
51
|
+
const resolved = resolveOnPath(command, platform);
|
|
52
|
+
if (resolved) {
|
|
53
|
+
return { id: tool.id, name: tool.name, executablePath: resolved };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Detects every tool in the list, dropping the ones that are not installed.
|
|
60
|
+
*/
|
|
61
|
+
export function detectTools(tools, platform, home, env = process.env) {
|
|
62
|
+
return tools
|
|
63
|
+
.map((tool) => detectTool(tool, platform, home, env))
|
|
64
|
+
.filter((result) => result !== null);
|
|
65
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-specific registry of the browsers and diff/editor tools the first-run
|
|
3
|
+
* wizard knows how to detect. Detection strategies differ per platform, so the
|
|
4
|
+
* candidate executable paths and PATH launcher names are grouped by platform.
|
|
5
|
+
*
|
|
6
|
+
* This module holds only data + pure helpers; the actual filesystem probing
|
|
7
|
+
* lives in the browser/diff-tool detectors.
|
|
8
|
+
*/
|
|
9
|
+
export type Platform = "darwin" | "win32" | "linux";
|
|
10
|
+
export interface DetectableTool {
|
|
11
|
+
/** stable identifier persisted to config, e.g. "chrome", "code" */
|
|
12
|
+
id: string;
|
|
13
|
+
/** human-readable name shown in prompts */
|
|
14
|
+
name: string;
|
|
15
|
+
/** launcher names to resolve on PATH (`which`/`where`) */
|
|
16
|
+
commands: string[];
|
|
17
|
+
/** absolute candidate paths per platform (support `~` and `%VAR%` tokens) */
|
|
18
|
+
paths: Partial<Record<Platform, string[]>>;
|
|
19
|
+
}
|
|
20
|
+
export interface DetectableEditor extends DetectableTool {
|
|
21
|
+
/**
|
|
22
|
+
* diff-service template for this tool, using the `$exec`/`$file1`/`$file2`
|
|
23
|
+
* placeholders understood by DiffService.
|
|
24
|
+
*/
|
|
25
|
+
diffExecTemplate: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const BROWSERS: DetectableTool[];
|
|
28
|
+
export declare const EDITORS: DetectableEditor[];
|
|
29
|
+
/**
|
|
30
|
+
* Normalizes an unknown platform string into one of the supported platforms.
|
|
31
|
+
* Falls back to "linux" for the various *nix variants Node may report.
|
|
32
|
+
*/
|
|
33
|
+
export declare function normalizePlatform(platform: string): Platform;
|
|
34
|
+
/**
|
|
35
|
+
* Expands `~` (home) and Windows `%VAR%` tokens in a candidate path. Unknown
|
|
36
|
+
* `%VAR%` tokens are left untouched so a later existence check simply misses.
|
|
37
|
+
*/
|
|
38
|
+
export declare function expandCandidatePath(raw: string, home: string, env?: NodeJS.ProcessEnv): string;
|