@speedkit/cli 4.4.1 → 4.5.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,17 @@
1
+ # [4.5.0](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.4.2...v4.5.0) (2026-05-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * **wizard:** template ([f92d756](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/f92d756636b646dbd6160f0bd8e0405a22c1c7c1))
7
+
8
+ ## [4.4.2](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.4.1...v4.4.2) (2026-05-13)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **onboarding:** detect atomic file writes and shut down gracefully ([a62f5a9](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/a62f5a9c134cd675ced842920e418b0a935f76fc))
14
+
1
15
  ## [4.4.1](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.4.0...v4.4.1) (2026-05-13)
2
16
 
3
17
 
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.4.1 linux-x64 node-v22.22.2
24
+ @speedkit/cli/4.5.0 linux-x64 node-v22.22.3
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -13,6 +13,18 @@ export default class Onboarding extends Command {
13
13
  async run() {
14
14
  const { args: { customerPath }, flags: { configName, domain, local, verboseLevel, ignoreContentSecurityPolicy, useTestConfig, debuggingPort, headless, }, } = await this.parse(Onboarding);
15
15
  const service = await new OnboardingServiceFactory(new OnboardingContext(customerPath, configName, domain, local, ignoreContentSecurityPolicy, this.config.root, verboseLevel, useTestConfig, debuggingPort, headless), new CliConfig(this.config).load(customerPath)).getService();
16
- await service.run();
16
+ const shutdown = new AbortController();
17
+ const onSignal = () => shutdown.abort();
18
+ // `once`: a second Ctrl+C falls through to Node's default handler and
19
+ // force-quits if cleanup hangs.
20
+ process.once("SIGINT", onSignal);
21
+ process.once("SIGTERM", onSignal);
22
+ try {
23
+ await service.run(shutdown.signal);
24
+ }
25
+ finally {
26
+ process.off("SIGINT", onSignal);
27
+ process.off("SIGTERM", onSignal);
28
+ }
17
29
  }
18
30
  }
@@ -105,7 +105,9 @@ import { ShopifyPlugin } from "speed-kit-config/ShopifyPlugin";
105
105
  },
106
106
  {{/unless}}
107
107
  {{#if addSSR}}
108
- customDevice: customDevice(),
108
+ customDevice: () => {
109
+ return customDevice();
110
+ },
109
111
  {{/if}}
110
112
  {{/if}}
111
113
  {{#if addSSR}}
@@ -70,7 +70,9 @@ export interface ConfigFileInterface extends FileInterface {
70
70
  updateFile(): Promise<void>;
71
71
  updateIfChanged(): Promise<void>;
72
72
  isUpdated(): boolean;
73
- onChange(callback?: (file: FileInterface) => Promise<void>): void;
73
+ onChange(callback?: (file: FileInterface) => Promise<void>, options?: {
74
+ signal?: AbortSignal;
75
+ }): void;
74
76
  delete(): Promise<void>;
75
77
  }
76
78
  export interface ModuleInterface extends ConfigFileInterface, FileInterface {
@@ -14,6 +14,8 @@ export declare class File implements ConfigFileInterface, FileInterface {
14
14
  isUpdated(): boolean;
15
15
  updateFile(): Promise<void>;
16
16
  updateIfChanged(): Promise<void>;
17
- onChange(callback: (file: FileInterface) => Promise<void>): Promise<void>;
17
+ onChange(callback: (file: FileInterface) => Promise<void>, options?: {
18
+ signal?: AbortSignal;
19
+ }): void;
18
20
  delete(): Promise<void>;
19
21
  }
@@ -1,4 +1,5 @@
1
- import { readFile, unlink, watch, writeFile } from "node:fs/promises";
1
+ import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { watch as chokidarWatch } from "chokidar";
2
3
  import { FILE_TYPE, } from "../integration-api-model.js";
3
4
  export class File {
4
5
  name;
@@ -41,15 +42,32 @@ export class File {
41
42
  await this.updateFile();
42
43
  }
43
44
  }
44
- async onChange(callback) {
45
- const watcher = watch(this.path, { encoding: "utf-8" });
46
- for await (const info of watcher) {
47
- if (info.eventType !== "change") {
48
- continue;
45
+ onChange(callback, options = {}) {
46
+ const { signal } = options;
47
+ if (signal?.aborted) {
48
+ return;
49
+ }
50
+ // chokidar over node's fs.watch: handles atomic write-then-rename
51
+ // (Claude Code, VS Code's atomicSave, vim writebackup, …) which fs.watch
52
+ // misses because inotify is inode-bound and the new file gets a new inode.
53
+ const watcher = chokidarWatch(this.path, {
54
+ ignoreInitial: true,
55
+ atomic: true,
56
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
57
+ });
58
+ const handle = async () => {
59
+ const content = await readFile(this.path, "utf-8");
60
+ if (content === this.content) {
61
+ return;
49
62
  }
50
- this.setContent(await readFile(this.path, "utf-8"));
63
+ this.setContent(content);
51
64
  await callback(this);
52
- }
65
+ };
66
+ watcher.on("change", handle);
67
+ watcher.on("add", handle);
68
+ signal?.addEventListener("abort", () => void watcher.close(), {
69
+ once: true,
70
+ });
53
71
  }
54
72
  async delete() {
55
73
  await unlink(this.path);
@@ -21,7 +21,7 @@ export declare class FileWatcher {
21
21
  * @param page
22
22
  * @param serviceWorkerReinstallCallback
23
23
  */
24
- addCallbackOnFileChange(page: Page, serviceWorkerReinstallCallback: (file: ConfigFileInterface) => Promise<void>): void;
24
+ addCallbackOnFileChange(page: Page, serviceWorkerReinstallCallback: (file: ConfigFileInterface) => Promise<void>, signal: AbortSignal): void;
25
25
  /**
26
26
  * changes on these files will trigger
27
27
  * - a rebuild of the serviceWorker
@@ -26,11 +26,11 @@ export class FileWatcher {
26
26
  * @param page
27
27
  * @param serviceWorkerReinstallCallback
28
28
  */
29
- addCallbackOnFileChange(page, serviceWorkerReinstallCallback) {
29
+ addCallbackOnFileChange(page, serviceWorkerReinstallCallback, signal) {
30
30
  const installResource = this.files.getByName(INTEGRATION_FILES.BUILD.INSTALL);
31
- this.handleServiceWorkerDependencies(page, installResource, serviceWorkerReinstallCallback);
32
- this.handleGenericConfigFiles(page, installResource);
33
- this.handleDynamicStyles(page);
31
+ this.handleServiceWorkerDependencies(page, installResource, serviceWorkerReinstallCallback, signal);
32
+ this.handleGenericConfigFiles(page, installResource, signal);
33
+ this.handleDynamicStyles(page, signal);
34
34
  }
35
35
  /**
36
36
  * changes on these files will trigger
@@ -44,7 +44,7 @@ export class FileWatcher {
44
44
  * @param serviceWorkerReinstallCallback
45
45
  * @private
46
46
  */
47
- handleServiceWorkerDependencies(page, installResource, serviceWorkerReinstallCallback) {
47
+ handleServiceWorkerDependencies(page, installResource, serviceWorkerReinstallCallback, signal) {
48
48
  // add watcher to serviceWorker related files
49
49
  const serviceWorkerDependencies = [
50
50
  INTEGRATION_FILES.CONFIG.SPEED_KIT,
@@ -65,7 +65,7 @@ export class FileWatcher {
65
65
  if (result.success === false) {
66
66
  this.cli.writeError(`error after fileChange: ${file.name}: ${result.error}`);
67
67
  }
68
- });
68
+ }, { signal });
69
69
  }
70
70
  }
71
71
  /**
@@ -78,7 +78,7 @@ export class FileWatcher {
78
78
  * @param installResource
79
79
  * @private
80
80
  */
81
- handleGenericConfigFiles(page, installResource) {
81
+ handleGenericConfigFiles(page, installResource, signal) {
82
82
  // add watcher to configFiles
83
83
  const generalConfigFiles = [
84
84
  INTEGRATION_FILES.CONFIG.DYNAMIC_BLOCKS,
@@ -99,7 +99,7 @@ export class FileWatcher {
99
99
  if (result.success === false) {
100
100
  this.cli.writeError(`error after fileChange: ${file.name}: ${result.error}`);
101
101
  }
102
- });
102
+ }, { signal });
103
103
  }
104
104
  }
105
105
  /**
@@ -112,7 +112,7 @@ export class FileWatcher {
112
112
  * @param page
113
113
  * @private
114
114
  */
115
- handleDynamicStyles(page) {
115
+ handleDynamicStyles(page, signal) {
116
116
  if (!this.files.hasFile(INTEGRATION_FILES.CONFIG.DYNAMIC_STYLES)) {
117
117
  return;
118
118
  }
@@ -124,7 +124,7 @@ export class FileWatcher {
124
124
  if (result.success === false) {
125
125
  this.cli.writeError(`error after fileChange: ${file.name}: ${result.error}`);
126
126
  }
127
- });
127
+ }, { signal });
128
128
  }
129
129
  /**
130
130
  * fileWatcherCallback that will reinstall the serviceWorker
@@ -17,9 +17,12 @@ export declare class OnboardingService {
17
17
  private reinstallOnNextNavigate;
18
18
  constructor(browserContext: BrowserContext, customerConfig: CustomerConfig, fetchEventHandler: FetchEventHandler, fileWatcher: FileWatcher, cli: CliService, developmentToolsMessages: DevtoolsExtensionApi);
19
19
  /**
20
- * prepare puppeteerBrowser, add eventListeners and start
20
+ * Prepare the puppeteer browser, wire event listeners, navigate to the
21
+ * install page, then block until either the caller aborts the signal
22
+ * (e.g. SIGINT in the command) or the browser disconnects. On exit, close
23
+ * any still-open resources. File watchers close themselves via `signal`.
21
24
  */
22
- run(): Promise<void>;
25
+ run(externalSignal: AbortSignal): Promise<void>;
23
26
  private prepareBrowser;
24
27
  private preparePage;
25
28
  private findPage;
@@ -21,16 +21,26 @@ export class OnboardingService {
21
21
  this.developmentToolsMessages = developmentToolsMessages;
22
22
  }
23
23
  /**
24
- * prepare puppeteerBrowser, add eventListeners and start
24
+ * Prepare the puppeteer browser, wire event listeners, navigate to the
25
+ * install page, then block until either the caller aborts the signal
26
+ * (e.g. SIGINT in the command) or the browser disconnects. On exit, close
27
+ * any still-open resources. File watchers close themselves via `signal`.
25
28
  */
26
- async run() {
29
+ async run(externalSignal) {
27
30
  this.cli.startAction("ONBOARDING:PREPARE:BROWSER", `prepare browser ${this.cli.style.bold(this.browserContext.browserVersionString)}`);
28
- const browser = await this.prepareBrowser();
29
- const page = await this.preparePage(browser);
31
+ const localController = new AbortController();
32
+ const shutdownSignal = AbortSignal.any([
33
+ externalSignal,
34
+ localController.signal,
35
+ ]);
36
+ const browser = await this.prepareBrowser(localController);
37
+ const page = await this.preparePage(browser, shutdownSignal);
30
38
  this.cli.successAction("ONBOARDING:PREPARE:BROWSER");
31
39
  await this.navigate(page, `${this.customerConfig.scope}speed-kit-install.html?url=${this.customerConfig.scope}`);
40
+ await waitForAbort(shutdownSignal);
41
+ await safe(browser.close());
32
42
  }
33
- async prepareBrowser() {
43
+ async prepareBrowser(localController) {
34
44
  const browser = await puppeteerExtra.default.launch(this.browserContext.getPuppeteerLaunchOptions());
35
45
  for (const target of browser.targets()) {
36
46
  if (target.type() === "browser") {
@@ -39,14 +49,14 @@ export class OnboardingService {
39
49
  }
40
50
  browser.on(BROWSER_EVENTS.DISCONNECTED, () => {
41
51
  this.cli.writeError("Browser disconnected, shutting down");
42
- process.exit(-1);
52
+ localController.abort();
43
53
  });
44
54
  return browser;
45
55
  }
46
- async preparePage(browser) {
56
+ async preparePage(browser, shutdownSignal) {
47
57
  const page = await this.findPage(browser);
48
58
  await safe(this.prepareCleanState(page));
49
- this.registerFileWatcher(page);
59
+ this.registerFileWatcher(page, shutdownSignal);
50
60
  this.registerReinstallSpeedKitOnNavigate(page);
51
61
  this.developmentToolsMessages.addMessage({
52
62
  id: "cli",
@@ -106,10 +116,10 @@ export class OnboardingService {
106
116
  this.reinstallOnNextNavigate = false;
107
117
  await this.navigate(page, target);
108
118
  }
109
- registerFileWatcher(page) {
119
+ registerFileWatcher(page, signal) {
110
120
  this.fileWatcher.addCallbackOnFileChange(page, async () => {
111
121
  this.reinstallOnNextNavigate = true;
112
- });
122
+ }, signal);
113
123
  }
114
124
  registerReinstallSpeedKitOnNavigate(page) {
115
125
  page.on("framenavigated", async (frame) => {
@@ -127,3 +137,11 @@ export class OnboardingService {
127
137
  await safe(page.goto(goto.toString()));
128
138
  }
129
139
  }
140
+ function waitForAbort(signal) {
141
+ if (signal.aborted) {
142
+ return Promise.resolve();
143
+ }
144
+ return new Promise((resolve) => {
145
+ signal.addEventListener("abort", () => resolve(), { once: true });
146
+ });
147
+ }
@@ -751,5 +751,5 @@
751
751
  ]
752
752
  }
753
753
  },
754
- "version": "4.4.1"
754
+ "version": "4.5.0"
755
755
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.4.1",
4
+ "version": "4.5.0",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"
@@ -94,6 +94,7 @@
94
94
  "@oclif/plugin-warn-if-update-available": "^3.1.61",
95
95
  "baqend": "^4.2.5",
96
96
  "chalk": "^5.6.2",
97
+ "chokidar": "^5.0.0",
97
98
  "cli-progress": "^3.12.0",
98
99
  "clipboardy": "^5.3.1",
99
100
  "deepmerge": "^4.3.1",