@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 +7 -0
- package/README.md +1 -1
- package/dist/services/integration-api/spec/file-watch.spec.d.ts +1 -0
- package/dist/services/integration-api/spec/file-watch.spec.js +81 -0
- package/dist/services/integration-api/struct/file.js +17 -5
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
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;
|
package/oclif.manifest.json
CHANGED