@speedkit/cli 4.20.4 → 4.20.5

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,10 @@
1
+ ## [4.20.5](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.20.4...v4.20.5) (2026-08-10)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **onboarding:** keep watching a config file after git replaces it ([ef2571a](https://gitlab.orestes.info/baqend/speed-kit-cli/commit/ef2571a5fa07862382c338096e3311c107f20a74))
7
+
1
8
  ## [4.20.4](https://gitlab.orestes.info/baqend/speed-kit-cli/compare/v4.20.3...v4.20.4) (2026-08-10)
2
9
 
3
10
 
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.20.4 linux-x64 node-v22.23.2
24
+ @speedkit/cli/4.20.5 linux-x64 node-v22.23.2
25
25
  $ sk --help [COMMAND]
26
26
  USAGE
27
27
  $ sk COMMAND
@@ -0,0 +1,81 @@
1
+ import { expect } from "chai";
2
+ import { describe, it, afterEach } from "mocha";
3
+ import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ // via the model barrel, not struct/file.js directly: the struct modules form an
7
+ // import cycle that only resolves in this order.
8
+ import { File } from "../integration-api-model.js";
9
+ // chokidar needs a real filesystem, so these run against a temp dir rather than memfs.
10
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ /** Wait until the change callback has fired at least `count` times. */
12
+ async function waitForCalls(getCalls, count, timeoutMs = 5000) {
13
+ const deadline = Date.now() + timeoutMs;
14
+ while (Date.now() < deadline) {
15
+ if (getCalls() >= count)
16
+ return;
17
+ await delay(50);
18
+ }
19
+ }
20
+ describe("File onChange", function () {
21
+ // real filesystem events plus chokidar's write-finish debounce
22
+ this.timeout(20000);
23
+ let dir;
24
+ let controller;
25
+ afterEach(() => {
26
+ controller?.abort();
27
+ controller = undefined;
28
+ if (dir) {
29
+ rmSync(dir, { recursive: true, force: true });
30
+ dir = undefined;
31
+ }
32
+ });
33
+ it("keeps reporting edits after the file is replaced instead of written in place", async () => {
34
+ dir = mkdtempSync(join(tmpdir(), "sk-file-watch-"));
35
+ const path = join(dir, "config_documentHandler.js");
36
+ writeFileSync(path, "v0");
37
+ const file = new File("config_documentHandler", path, "v0");
38
+ controller = new AbortController();
39
+ let calls = 0;
40
+ file.onChange(async () => {
41
+ calls += 1;
42
+ }, { signal: controller.signal });
43
+ await delay(700); // let the watcher attach
44
+ writeFileSync(path, "v1");
45
+ await waitForCalls(() => calls, 1);
46
+ expect(calls, "an in-place write is reported").to.be.at.least(1);
47
+ // git checkout / reset / rebase / stash / pull replace the file rather than
48
+ // writing into it. A single-file watch stops firing for good at this point.
49
+ const callsBeforeReplace = calls;
50
+ unlinkSync(path);
51
+ writeFileSync(path, "v2");
52
+ await waitForCalls(() => calls, callsBeforeReplace + 1);
53
+ expect(calls, "the replacement itself is reported").to.be.at.least(callsBeforeReplace + 1);
54
+ // The regression: every edit after the replacement used to be dropped, so
55
+ // onboarding kept serving the previous build until it was restarted.
56
+ const callsAfterReplace = calls;
57
+ writeFileSync(path, "v3");
58
+ await waitForCalls(() => calls, callsAfterReplace + 1);
59
+ expect(calls, "an edit after the replacement is still reported").to.be.at.least(callsAfterReplace + 1);
60
+ });
61
+ it("ignores changes to sibling files in the same directory", async () => {
62
+ dir = mkdtempSync(join(tmpdir(), "sk-file-watch-"));
63
+ const path = join(dir, "config_documentHandler.js");
64
+ const sibling = join(dir, "config_SpeedKit.js");
65
+ writeFileSync(path, "v0");
66
+ writeFileSync(sibling, "other");
67
+ const file = new File("config_documentHandler", path, "v0");
68
+ controller = new AbortController();
69
+ let calls = 0;
70
+ file.onChange(async () => {
71
+ calls += 1;
72
+ }, { signal: controller.signal });
73
+ await delay(700);
74
+ writeFileSync(sibling, "other-changed");
75
+ await delay(900);
76
+ expect(calls, "a sibling write is not reported").to.equal(0);
77
+ writeFileSync(path, "v1");
78
+ await waitForCalls(() => calls, 1);
79
+ expect(calls, "the watched file is still reported").to.be.at.least(1);
80
+ });
81
+ });
@@ -1,4 +1,5 @@
1
1
  import { readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
2
3
  import { watch as chokidarWatch } from "chokidar";
3
4
  import { FILE_TYPE, } from "../integration-api-model.js";
4
5
  export class File {
@@ -47,15 +48,26 @@ export class File {
47
48
  if (signal?.aborted) {
48
49
  return;
49
50
  }
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, {
51
+ // Watch the containing directory, not the file: a single-file watch stops
52
+ // firing for good once the file is *replaced* rather than written in place,
53
+ // which is what git does on checkout/reset/rebase/stash/pull. The replace
54
+ // itself still arrives, so the breakage is silent — every later edit is
55
+ // dropped, no rebuild and no cache clear run, and onboarding keeps serving
56
+ // the previous build until it is restarted.
57
+ // depth 0 keeps this to the directory's own entries, and chokidar's atomic
58
+ // option still folds write-then-rename (Claude Code, VS Code atomicSave,
59
+ // vim writebackup, …) into a single change event.
60
+ const target = resolve(this.path);
61
+ const watcher = chokidarWatch(dirname(target), {
54
62
  ignoreInitial: true,
55
63
  atomic: true,
64
+ depth: 0,
56
65
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
57
66
  });
58
- const handle = async () => {
67
+ const handle = async (changedPath) => {
68
+ if (resolve(changedPath) !== target) {
69
+ return;
70
+ }
59
71
  const content = await readFile(this.path, "utf-8");
60
72
  if (content === this.content) {
61
73
  return;
@@ -1001,5 +1001,5 @@
1001
1001
  ]
1002
1002
  }
1003
1003
  },
1004
- "version": "4.20.4"
1004
+ "version": "4.20.5"
1005
1005
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@speedkit/cli",
3
3
  "description": "Speed Kit CLI",
4
- "version": "4.20.4",
4
+ "version": "4.20.5",
5
5
  "author": {
6
6
  "name": "Baqend.com",
7
7
  "email": "info@baqend.com"